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.WebPChunk; 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-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 WebPChunk 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 WebPChunk chunk = WebPChunkType.makeChunk(type, payloadSize, bytes); 117 if (padding) { 118 skipBytes(is, 1); 119 } 120 121 sizeCount = SafeOperations.add(sizeCount, chunkSize); 122 return chunk; 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 * Read 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 @Override 160 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException { 161 pw.println("webp.dumpImageFile"); 162 try (ChunksReader reader = new ChunksReader(byteSource)) { 163 int offset = reader.getOffset(); 164 WebPChunk chunk = reader.readChunk(); 165 if (chunk == null) { 166 throw new ImagingException("No WebP chunks found"); 167 } 168 169 // TODO: this does not look too risky; a user could craft an image 170 // with millions of chunks, that are really expensive to dump, 171 // but that should result in a large image, where we can short- 172 // -circuit the operation somewhere else - if needed. 173 do { 174 chunk.dump(pw, offset); 175 176 offset = reader.getOffset(); 177 chunk = reader.readChunk(); 178 } while (chunk != null); 179 } 180 return true; 181 } 182 183 @Override 184 protected String[] getAcceptedExtensions() { 185 return ACCEPTED_EXTENSIONS; 186 } 187 188 @Override 189 protected ImageFormat[] getAcceptedTypes() { 190 return new ImageFormat[] { ImageFormats.WEBP }; 191 } 192 193 @Override 194 public BufferedImage getBufferedImage(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException { 195 throw new ImagingException("Reading WebP files is currently not supported"); 196 } 197 198 @Override 199 public String getDefaultExtension() { 200 return DEFAULT_EXTENSION; 201 } 202 203 @Override 204 public WebPImagingParameters getDefaultParameters() { 205 return new WebPImagingParameters(); 206 } 207 208 @Override 209 public byte[] getIccProfileBytes(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException { 210 try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.ICCP)) { 211 final WebPChunk chunk = reader.readChunk(); 212 return chunk == null ? null : chunk.getBytes(); 213 } 214 } 215 216 @Override 217 public ImageInfo getImageInfo(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException { 218 try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.VP8, WebPChunkType.VP8L, WebPChunkType.VP8X, WebPChunkType.ANMF)) { 219 String formatDetails; 220 int width; 221 int height; 222 int numberOfImages; 223 boolean hasAlpha = false; 224 ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB; 225 226 WebPChunk chunk = reader.readChunk(); 227 if (chunk instanceof WebPChunkVp8) { 228 formatDetails = "WebP/Lossy"; 229 numberOfImages = 1; 230 231 final WebPChunkVp8 vp8 = (WebPChunkVp8) chunk; 232 width = vp8.getWidth(); 233 height = vp8.getHeight(); 234 colorType = ImageInfo.ColorType.YCbCr; 235 } else if (chunk instanceof WebPChunkVp8l) { 236 formatDetails = "WebP/Lossless"; 237 numberOfImages = 1; 238 239 final WebPChunkVp8l vp8l = (WebPChunkVp8l) chunk; 240 width = vp8l.getImageWidth(); 241 height = vp8l.getImageHeight(); 242 } else if (chunk instanceof WebPChunkVp8x) { 243 final WebPChunkVp8x vp8x = (WebPChunkVp8x) chunk; 244 width = vp8x.getCanvasWidth(); 245 height = vp8x.getCanvasHeight(); 246 hasAlpha = ((WebPChunkVp8x) chunk).hasAlpha(); 247 248 if (vp8x.hasAnimation()) { 249 formatDetails = "WebP/Animation"; 250 251 numberOfImages = 0; 252 while ((chunk = reader.readChunk()) != null) { 253 if (chunk.getType() == WebPChunkType.ANMF.value) { 254 numberOfImages++; 255 } 256 } 257 258 } else { 259 numberOfImages = 1; 260 chunk = reader.readChunk(); 261 262 if (chunk == null) { 263 throw new ImagingException("Image has no content"); 264 } 265 266 if (chunk.getType() == WebPChunkType.ANMF.value) { 267 throw new ImagingException("Non animated image should not contain ANMF chunks"); 268 } 269 270 if (chunk.getType() == WebPChunkType.VP8.value) { 271 formatDetails = "WebP/Lossy (Extended)"; 272 colorType = ImageInfo.ColorType.YCbCr; 273 } else if (chunk.getType() == WebPChunkType.VP8L.value) { 274 formatDetails = "WebP/Lossless (Extended)"; 275 } else { 276 throw new ImagingException("Unknown WebP chunk type: " + chunk); 277 } 278 } 279 } else { 280 throw new ImagingException("Unknown WebP chunk type: " + chunk); 281 } 282 283 return new ImageInfo(formatDetails, 32, new ArrayList<>(), ImageFormats.WEBP, "webp", height, "image/webp", numberOfImages, -1, -1, -1, -1, width, 284 false, hasAlpha, false, colorType, ImageInfo.CompressionAlgorithm.UNKNOWN); 285 } 286 } 287 288 @Override 289 public Dimension getImageSize(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException { 290 try (ChunksReader reader = new ChunksReader(byteSource)) { 291 final WebPChunk chunk = reader.readChunk(); 292 if (chunk instanceof WebPChunkVp8) { 293 final WebPChunkVp8 vp8 = (WebPChunkVp8) chunk; 294 return new Dimension(vp8.getWidth(), vp8.getHeight()); 295 } 296 if (chunk instanceof WebPChunkVp8l) { 297 final WebPChunkVp8l vp8l = (WebPChunkVp8l) chunk; 298 return new Dimension(vp8l.getImageWidth(), vp8l.getImageHeight()); 299 } 300 if (chunk instanceof WebPChunkVp8x) { 301 final WebPChunkVp8x vp8x = (WebPChunkVp8x) chunk; 302 return new Dimension(vp8x.getCanvasWidth(), vp8x.getCanvasHeight()); 303 } 304 throw new ImagingException("Unknown WebP chunk type: " + chunk); 305 } 306 } 307 308 @Override 309 public WebPImageMetadata getMetadata(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException { 310 try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.EXIF)) { 311 final WebPChunk chunk = reader.readChunk(); 312 return chunk == null ? null : new WebPImageMetadata((TiffImageMetadata) new TiffImageParser().getMetadata(chunk.getBytes())); 313 } 314 } 315 316 @Override 317 public String getName() { 318 return "WebP-Custom"; 319 } 320 321 @Override 322 public String getXmpXml(final ByteSource byteSource, final XmpImagingParameters<WebPImagingParameters> params) throws ImagingException, IOException { 323 try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.XMP)) { 324 final WebPChunkXml chunk = (WebPChunkXml) reader.readChunk(); 325 return chunk == null ? null : chunk.getXml(); 326 } 327 } 328}