001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.imaging.formats.webp; 018 019import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes; 020import static org.apache.commons.imaging.common.BinaryFunctions.readBytes; 021import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes; 022 023import java.awt.Dimension; 024import java.awt.image.BufferedImage; 025import java.io.Closeable; 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.PrintWriter; 029import java.nio.ByteOrder; 030import java.util.ArrayList; 031 032import org.apache.commons.imaging.AbstractImageParser; 033import org.apache.commons.imaging.ImageFormat; 034import org.apache.commons.imaging.ImageFormats; 035import org.apache.commons.imaging.ImageInfo; 036import org.apache.commons.imaging.ImagingException; 037import org.apache.commons.imaging.bytesource.ByteSource; 038import org.apache.commons.imaging.common.XmpEmbeddable; 039import org.apache.commons.imaging.common.XmpImagingParameters; 040import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; 041import org.apache.commons.imaging.formats.tiff.TiffImageParser; 042import org.apache.commons.imaging.formats.webp.chunks.AbstractWebPChunk; 043import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8; 044import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8l; 045import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8x; 046import org.apache.commons.imaging.formats.webp.chunks.WebPChunkXml; 047import org.apache.commons.imaging.internal.SafeOperations; 048 049/** 050 * WebP image parser. 051 * 052 * @since 1.0.0-alpha4 053 */ 054public class WebPImageParser extends AbstractImageParser<WebPImagingParameters> implements XmpEmbeddable<WebPImagingParameters> { 055 056 private static final class ChunksReader implements Closeable { 057 private final InputStream is; 058 private final WebPChunkType[] chunkTypes; 059 private int sizeCount = 4; 060 private boolean firstChunk = true; 061 062 final int fileSize; 063 064 ChunksReader(final ByteSource byteSource) throws IOException, ImagingException { 065 this(byteSource, (WebPChunkType[]) null); 066 } 067 068 ChunksReader(final ByteSource byteSource, final WebPChunkType... chunkTypes) throws ImagingException, IOException { 069 this.is = byteSource.getInputStream(); 070 this.chunkTypes = chunkTypes; 071 this.fileSize = readFileHeader(is); 072 } 073 074 @Override 075 public void close() throws IOException { 076 is.close(); 077 } 078 079 int getOffset() { 080 return SafeOperations.add(sizeCount, 8); // File Header 081 } 082 083 AbstractWebPChunk readChunk() throws ImagingException, IOException { 084 while (sizeCount < fileSize) { 085 final int type = read4Bytes("Chunk Type", is, "Not a valid WebP file", ByteOrder.LITTLE_ENDIAN); 086 final int payloadSize = read4Bytes("Chunk Size", is, "Not a valid WebP file", ByteOrder.LITTLE_ENDIAN); 087 if (payloadSize < 0) { 088 throw new ImagingException("Chunk Payload is too long:" + payloadSize); 089 } 090 final boolean padding = payloadSize % 2 != 0; 091 final int chunkSize = SafeOperations.add(8, padding ? 1 : 0, payloadSize); 092 093 if (firstChunk) { 094 firstChunk = false; 095 if (type != WebPChunkType.VP8.value && type != WebPChunkType.VP8L.value && type != WebPChunkType.VP8X.value) { 096 throw new ImagingException("First Chunk must be VP8, VP8L or VP8X"); 097 } 098 } 099 100 if (chunkTypes != null) { 101 boolean skip = true; 102 for (final WebPChunkType t : chunkTypes) { 103 if (t.value == type) { 104 skip = false; 105 break; 106 } 107 } 108 if (skip) { 109 skipBytes(is, payloadSize + (padding ? 1 : 0)); 110 sizeCount = SafeOperations.add(sizeCount, chunkSize); 111 continue; 112 } 113 } 114 115 final byte[] bytes = readBytes("Chunk Payload", is, payloadSize); 116 final AbstractWebPChunk chunk = WebPChunkType.makeChunk(type, payloadSize, bytes); 117 if (padding) { 118 skipBytes(is, 1); 119 } 120 121 sizeCount = SafeOperations.add(sizeCount, chunkSize); 122 return chunk; // NOPMD How can we do this better? 123 } 124 125 if (firstChunk) { 126 throw new ImagingException("No WebP chunks found"); 127 } 128 return null; 129 } 130 } 131 132 private static final String DEFAULT_EXTENSION = ImageFormats.WEBP.getDefaultExtension(); 133 134 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.WEBP.getExtensions(); 135 136 /** 137 * Reads the file header of WebP file. 138 * 139 * @return file size in file header (including the WebP signature, excluding the TIFF signature and the file size field). 140 */ 141 private static int readFileHeader(final InputStream is) throws IOException, ImagingException { 142 final byte[] buffer = new byte[4]; 143 if (is.read(buffer) < 4 || !WebPConstants.RIFF_SIGNATURE.equals(buffer)) { 144 throw new ImagingException("Not a valid WebP file"); 145 } 146 147 final int fileSize = read4Bytes("File Size", is, "Not a valid WebP file", ByteOrder.LITTLE_ENDIAN); 148 if (fileSize < 0) { 149 throw new ImagingException("File size is too long:" + fileSize); 150 } 151 152 if (is.read(buffer) < 4 || !WebPConstants.WEBP_SIGNATURE.equals(buffer)) { 153 throw new ImagingException("Not a valid WebP file"); 154 } 155 156 return fileSize; 157 } 158 159 /** 160 * Constructs a new instance with the big-endian byte order. 161 */ 162 public WebPImageParser() { 163 // empty 164 } 165 166 @Override 167 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException { 168 pw.println("webp.dumpImageFile"); 169 try (ChunksReader reader = new ChunksReader(byteSource)) { 170 int offset = reader.getOffset(); 171 AbstractWebPChunk chunk = reader.readChunk(); 172 if (chunk == null) { 173 throw new ImagingException("No WebP chunks found"); 174 } 175 176 // TODO: this does not look too risky; a user could craft an image 177 // with millions of chunks, that are really expensive to dump, 178 // but that should result in a large image, where we can short- 179 // -circuit the operation somewhere else - if needed. 180 do { 181 chunk.dump(pw, offset); 182 183 offset = reader.getOffset(); 184 chunk = reader.readChunk(); 185 } while (chunk != null); 186 } 187 return true; 188 } 189 190 @Override 191 protected String[] getAcceptedExtensions() { 192 return ACCEPTED_EXTENSIONS; 193 } 194 195 @Override 196 protected ImageFormat[] getAcceptedTypes() { 197 return new ImageFormat[] { ImageFormats.WEBP }; 198 } 199 200 @Override 201 public BufferedImage getBufferedImage(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException { 202 throw new ImagingException("Reading WebP files is currently not supported"); 203 } 204 205 @Override 206 public String getDefaultExtension() { 207 return DEFAULT_EXTENSION; 208 } 209 210 @Override 211 public WebPImagingParameters getDefaultParameters() { 212 return new WebPImagingParameters(); 213 } 214 215 @Override 216 public byte[] getIccProfileBytes(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException { 217 try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.ICCP)) { 218 final AbstractWebPChunk chunk = reader.readChunk(); 219 return chunk == null ? null : chunk.getBytes(); 220 } 221 } 222 223 @Override 224 public ImageInfo getImageInfo(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException { 225 try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.VP8, WebPChunkType.VP8L, WebPChunkType.VP8X, WebPChunkType.ANMF)) { 226 final String formatDetails; 227 final int width; 228 final int height; 229 int numberOfImages; 230 boolean hasAlpha = false; 231 ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB; 232 233 AbstractWebPChunk chunk = reader.readChunk(); 234 if (chunk instanceof WebPChunkVp8) { 235 formatDetails = "WebP/Lossy"; 236 numberOfImages = 1; 237 238 final WebPChunkVp8 vp8 = (WebPChunkVp8) chunk; 239 width = vp8.getWidth(); 240 height = vp8.getHeight(); 241 colorType = ImageInfo.ColorType.YCbCr; 242 } else if (chunk instanceof WebPChunkVp8l) { 243 formatDetails = "WebP/Lossless"; 244 numberOfImages = 1; 245 246 final WebPChunkVp8l vp8l = (WebPChunkVp8l) chunk; 247 width = vp8l.getImageWidth(); 248 height = vp8l.getImageHeight(); 249 } else if (chunk instanceof WebPChunkVp8x) { 250 final WebPChunkVp8x vp8x = (WebPChunkVp8x) chunk; 251 width = vp8x.getCanvasWidth(); 252 height = vp8x.getCanvasHeight(); 253 hasAlpha = ((WebPChunkVp8x) chunk).hasAlpha(); 254 255 if (vp8x.hasAnimation()) { 256 formatDetails = "WebP/Animation"; 257 258 numberOfImages = 0; 259 while ((chunk = reader.readChunk()) != null) { 260 if (chunk.getType() == WebPChunkType.ANMF.value) { 261 numberOfImages++; 262 } 263 } 264 265 } else { 266 numberOfImages = 1; 267 chunk = reader.readChunk(); 268 269 if (chunk == null) { 270 throw new ImagingException("Image has no content"); 271 } 272 273 if (chunk.getType() == WebPChunkType.ANMF.value) { 274 throw new ImagingException("Non animated image should not contain ANMF chunks"); 275 } 276 277 if (chunk.getType() == WebPChunkType.VP8.value) { 278 formatDetails = "WebP/Lossy (Extended)"; 279 colorType = ImageInfo.ColorType.YCbCr; 280 } else if (chunk.getType() == WebPChunkType.VP8L.value) { 281 formatDetails = "WebP/Lossless (Extended)"; 282 } else { 283 throw new ImagingException("Unknown WebP chunk type: " + chunk); 284 } 285 } 286 } else { 287 throw new ImagingException("Unknown WebP chunk type: " + chunk); 288 } 289 290 return new ImageInfo(formatDetails, 32, new ArrayList<>(), ImageFormats.WEBP, "webp", height, "image/webp", numberOfImages, -1, -1, -1, -1, width, 291 false, hasAlpha, false, colorType, ImageInfo.CompressionAlgorithm.UNKNOWN); 292 } 293 } 294 295 @Override 296 public Dimension getImageSize(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException { 297 try (ChunksReader reader = new ChunksReader(byteSource)) { 298 final AbstractWebPChunk chunk = reader.readChunk(); 299 if (chunk instanceof WebPChunkVp8) { 300 final WebPChunkVp8 vp8 = (WebPChunkVp8) chunk; 301 return new Dimension(vp8.getWidth(), vp8.getHeight()); 302 } 303 if (chunk instanceof WebPChunkVp8l) { 304 final WebPChunkVp8l vp8l = (WebPChunkVp8l) chunk; 305 return new Dimension(vp8l.getImageWidth(), vp8l.getImageHeight()); 306 } 307 if (chunk instanceof WebPChunkVp8x) { 308 final WebPChunkVp8x vp8x = (WebPChunkVp8x) chunk; 309 return new Dimension(vp8x.getCanvasWidth(), vp8x.getCanvasHeight()); 310 } 311 throw new ImagingException("Unknown WebP chunk type: " + chunk); 312 } 313 } 314 315 @Override 316 public WebPImageMetadata getMetadata(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException { 317 try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.EXIF)) { 318 final AbstractWebPChunk chunk = reader.readChunk(); 319 return chunk == null ? null : new WebPImageMetadata((TiffImageMetadata) new TiffImageParser().getMetadata(chunk.getBytes())); 320 } 321 } 322 323 @Override 324 public String getName() { 325 return "WebP-Custom"; 326 } 327 328 @Override 329 public String getXmpXml(final ByteSource byteSource, final XmpImagingParameters<WebPImagingParameters> params) throws ImagingException, IOException { 330 try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.XMP)) { 331 final WebPChunkXml chunk = (WebPChunkXml) reader.readChunk(); 332 return chunk == null ? null : chunk.getXml(); 333 } 334 } 335}