1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
51
52
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);
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
138
139
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
170
171
172
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 }