View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.webp;
18  
19  import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
20  import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
21  import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes;
22  
23  import java.awt.Dimension;
24  import java.awt.image.BufferedImage;
25  import java.io.Closeable;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.PrintWriter;
29  import java.nio.ByteOrder;
30  import java.util.ArrayList;
31  
32  import org.apache.commons.imaging.AbstractImageParser;
33  import org.apache.commons.imaging.ImageFormat;
34  import org.apache.commons.imaging.ImageFormats;
35  import org.apache.commons.imaging.ImageInfo;
36  import org.apache.commons.imaging.ImagingException;
37  import org.apache.commons.imaging.bytesource.ByteSource;
38  import org.apache.commons.imaging.common.XmpEmbeddable;
39  import org.apache.commons.imaging.common.XmpImagingParameters;
40  import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
41  import org.apache.commons.imaging.formats.tiff.TiffImageParser;
42  import org.apache.commons.imaging.formats.webp.chunks.WebPChunk;
43  import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8;
44  import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8l;
45  import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8x;
46  import org.apache.commons.imaging.formats.webp.chunks.WebPChunkXml;
47  import org.apache.commons.imaging.internal.SafeOperations;
48  
49  /**
50   * WebP image parser.
51   *
52   * @since 1.0-alpha4
53   */
54  public class WebPImageParser extends AbstractImageParser<WebPImagingParameters> implements XmpEmbeddable<WebPImagingParameters> {
55  
56      private static final class ChunksReader implements Closeable {
57          private final InputStream is;
58          private final WebPChunkType[] chunkTypes;
59          private int sizeCount = 4;
60          private boolean firstChunk = true;
61  
62          final int fileSize;
63  
64          ChunksReader(final ByteSource byteSource) throws IOException, ImagingException {
65              this(byteSource, (WebPChunkType[]) null);
66          }
67  
68          ChunksReader(final ByteSource byteSource, final WebPChunkType... chunkTypes) throws ImagingException, IOException {
69              this.is = byteSource.getInputStream();
70              this.chunkTypes = chunkTypes;
71              this.fileSize = readFileHeader(is);
72          }
73  
74          @Override
75          public void close() throws IOException {
76              is.close();
77          }
78  
79          int getOffset() {
80              return SafeOperations.add(sizeCount, 8); // File Header
81          }
82  
83          WebPChunk readChunk() throws ImagingException, IOException {
84              while (sizeCount < fileSize) {
85                  final int type = read4Bytes("Chunk Type", is, "Not a valid WebP file", ByteOrder.LITTLE_ENDIAN);
86                  final int payloadSize = read4Bytes("Chunk Size", is, "Not a valid WebP file", ByteOrder.LITTLE_ENDIAN);
87                  if (payloadSize < 0) {
88                      throw new ImagingException("Chunk Payload is too long:" + payloadSize);
89                  }
90                  final boolean padding = payloadSize % 2 != 0;
91                  final int chunkSize = SafeOperations.add(8, padding ? 1 : 0, payloadSize);
92  
93                  if (firstChunk) {
94                      firstChunk = false;
95                      if (type != WebPChunkType.VP8.value && type != WebPChunkType.VP8L.value && type != WebPChunkType.VP8X.value) {
96                          throw new ImagingException("First Chunk must be VP8, VP8L or VP8X");
97                      }
98                  }
99  
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 }