    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}