From 361089259e1278be191521cd2b24b611919f7451 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 13 Jan 2023 09:39:42 +0800 Subject: [PATCH 1/2] [IMAGING-340] Add support to PNG 1.2 Extensions Ref: http://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html --- src/conf/spotbugs-exclude-filter.xml | 10 + .../imaging/formats/png/ChunkType.java | 205 +++++++++++++++--- .../imaging/formats/png/Extension.java | 34 +++ .../imaging/formats/png/PngImageMetadata.java | 98 +++++++++ .../imaging/formats/png/PngImageParser.java | 77 ++++--- .../imaging/formats/png/package-info.java | 9 +- .../imaging/formats/png/PngReadTest.java | 31 +++ .../png/IMAGING-340/image-with-exif.png | Bin 0 -> 177 bytes 8 files changed, 401 insertions(+), 63 deletions(-) create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/Extension.java create mode 100644 src/main/java/org/apache/commons/imaging/formats/png/PngImageMetadata.java create mode 100644 src/test/resources/images/png/IMAGING-340/image-with-exif.png diff --git a/src/conf/spotbugs-exclude-filter.xml b/src/conf/spotbugs-exclude-filter.xml index ebefe41fb1..03c58cad5b 100644 --- a/src/conf/spotbugs-exclude-filter.xml +++ b/src/conf/spotbugs-exclude-filter.xml @@ -173,6 +173,16 @@ + + + + + + + + + + diff --git a/src/main/java/org/apache/commons/imaging/formats/png/ChunkType.java b/src/main/java/org/apache/commons/imaging/formats/png/ChunkType.java index 1bd3c6ac58..922e992a0d 100644 --- a/src/main/java/org/apache/commons/imaging/formats/png/ChunkType.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/ChunkType.java @@ -16,80 +16,217 @@ */ package org.apache.commons.imaging.formats.png; +import java.io.IOException; import java.nio.charset.StandardCharsets; import org.apache.commons.imaging.common.BinaryFunctions; +import org.apache.commons.imaging.formats.png.chunks.PngChunk; +import org.apache.commons.imaging.formats.png.chunks.PngChunkGama; +import org.apache.commons.imaging.formats.png.chunks.PngChunkIccp; +import org.apache.commons.imaging.formats.png.chunks.PngChunkIdat; +import org.apache.commons.imaging.formats.png.chunks.PngChunkIhdr; +import org.apache.commons.imaging.formats.png.chunks.PngChunkItxt; +import org.apache.commons.imaging.formats.png.chunks.PngChunkPhys; +import org.apache.commons.imaging.formats.png.chunks.PngChunkPlte; +import org.apache.commons.imaging.formats.png.chunks.PngChunkScal; +import org.apache.commons.imaging.formats.png.chunks.PngChunkText; +import org.apache.commons.imaging.formats.png.chunks.PngChunkZtxt; /** - * Type of a PNG chunk. + * Type of PNG chunk. * * @see Portable Network Graphics Specification - Chunk specifications */ public enum ChunkType { - /** Image header */ - IHDR, + /** + * Image header + */ + IHDR(PngChunkIhdr::new), - /** Palette */ - PLTE, + /** + * Palette + */ + PLTE(PngChunkPlte::new), - /** Image data */ - IDAT, + /** + * Image data + */ + IDAT(PngChunkIdat::new), - /** Image trailer */ + /** + * Image trailer + */ IEND, - /** Transparency */ + /** + * Transparency + */ tRNS, - /** Primary chromaticities and white point */ + /** + * Primary chromaticities and white point + */ cHRM, - /** Image gamma */ - gAMA, + /** + * Image gamma + */ + gAMA(PngChunkGama::new), - /** Embedded ICC profile */ - iCCP, + /** + * Embedded ICC profile + */ + iCCP(PngChunkIccp::new), - /** Significant bits */ + /** + * Significant bits + */ sBIT, - /** Standard RGB color space */ + /** + * Standard RGB color space + */ sRGB, - /** Textual data */ - tEXt, + /** + * Textual data + */ + tEXt(PngChunkText::new), - /** Compressed textual data */ - zTXt, + /** + * Compressed textual data + */ + zTXt(PngChunkZtxt::new), - /** International textual data */ - iTXt, + /** + * International textual data + */ + iTXt(PngChunkItxt::new), - /** Background color */ + /** + * Background colour + */ bKGD, - /** Image histogram */ + /** + * Image histogram + */ hIST, - /** Physical pixel dimensions */ - pHYs, + /** + * Physical pixel dimensions + */ + pHYs(PngChunkPhys::new), - /** Physical scale */ - sCAL, - - /** Suggested palette */ + /** + * Suggested palette + */ sPLT, - /** Image last-modification time */ - tIME; + /** + * Image last-modification time + */ + tIME, + + /* + * PNGEXT + */ + + /** + * Image offset + * + * @since 1.0-alpha6 + */ + oFFs(Extension.PNGEXT), + + /** + * Calibration of pixel values + * + * @since 1.0-alpha6 + */ + pCAL(Extension.PNGEXT), + + /** + * Physical scale + */ + sCAL(Extension.PNGEXT, PngChunkScal::new), + + /** + * GIF Graphic Control Extension + * + * @since 1.0-alpha6 + */ + gIFg(Extension.PNGEXT), + + /** + * GIF Application Extension + * + * @since 1.0-alpha6 + */ + gIFx(Extension.PNGEXT), + + /** + * Indicator of Stereo Image + * + * @since 1.0-alpha6 + */ + sTER(Extension.PNGEXT), + + /** + * Exchangeable Image File (Exif) Profile + * + * @since 1.0-alpha6 + */ + eXIf(Extension.PNGEXT), + + ; + + @FunctionalInterface + private interface ChunkConstructor { + PngChunk make(int length, int chunkType, int crc, byte[] bytes) throws IOException; + } + + private static final ChunkType[] types = ChunkType.values(); + + static ChunkType findType(int chunkType) { + for (ChunkType type : types) { + if (type.value == chunkType) { + return type; + } + } + return null; + } + + static PngChunk makeChunk(int length, int chunkType, int crc, byte[] bytes) throws IOException { + ChunkType type = findType(chunkType); + return type != null && type.constructor != null + ? type.constructor.make(length, chunkType, crc, bytes) + : new PngChunk(length, chunkType, crc, bytes); + } final byte[] array; final int value; + final Extension extension; + final ChunkConstructor constructor; ChunkType() { + this(null, null); + } + + ChunkType(Extension extension) { + this(extension, null); + } + + ChunkType(ChunkConstructor constructor) { + this(null, constructor); + } + + ChunkType(Extension extension, ChunkConstructor constructor) { final char[] chars = name().toCharArray(); - array = name().getBytes(StandardCharsets.UTF_8); - value = BinaryFunctions.charsToQuad(chars[0], chars[1], chars[2], chars[3]); + this.array = name().getBytes(StandardCharsets.UTF_8); + this.value = BinaryFunctions.charsToQuad(chars[0], chars[1], chars[2], chars[3]); + this.extension = extension; + this.constructor = constructor; } } diff --git a/src/main/java/org/apache/commons/imaging/formats/png/Extension.java b/src/main/java/org/apache/commons/imaging/formats/png/Extension.java new file mode 100644 index 0000000000..a6f1dcc02d --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/Extension.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +/** + * PNG extension types. + * + * @since 1.0-alpha6 + */ +enum Extension { + /** + * @see Extensions to the PNG 1.2 Specification, Version 1.5.0 + */ + PNGEXT, + + /** + * @see APNG Specification + */ + APNG, +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/PngImageMetadata.java b/src/main/java/org/apache/commons/imaging/formats/png/PngImageMetadata.java new file mode 100644 index 0000000000..defa4232ee --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/formats/png/PngImageMetadata.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.imaging.formats.png; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.imaging.common.ImageMetadata; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.apache.commons.imaging.internal.Debug; + +/** + * @since 1.0-alpha6 + */ +public class PngImageMetadata implements ImageMetadata { + + private static final String NEWLINE = System.lineSeparator(); + + private final ImageMetadata textualInformation; + private final TiffImageMetadata exif; + + public PngImageMetadata(ImageMetadata textualInformation) { + this(textualInformation, null); + } + + public PngImageMetadata(ImageMetadata textualInformation, TiffImageMetadata exif) { + this.textualInformation = Objects.requireNonNull(textualInformation); + this.exif = exif; + } + + public ImageMetadata getTextualInformation() { + return textualInformation; + } + + public TiffImageMetadata getExif() { + return exif; + } + + @Override + public String toString() { + return toString(null); + } + + @Override + public String toString(String prefix) { + if (prefix == null) { + prefix = ""; + } + + final StringBuilder result = new StringBuilder(); + + result.append(prefix); + result.append("Textual information:"); + result.append(NEWLINE); + result.append(textualInformation.toString("\t")); + + if (exif != null) { + result.append(NEWLINE); + result.append(prefix); + result.append("Exif metadata:"); + result.append(NEWLINE); + result.append(exif.toString("\t")); + } + + return result.toString(); + } + + @Override + public List getItems() { + if (exif == null) { + return textualInformation.getItems(); + } + + ArrayList result = new ArrayList<>(); + result.addAll(textualInformation.getItems()); + result.addAll(exif.getItems()); + return result; + } + + public void dump() { + Debug.debug(this.toString()); + } +} diff --git a/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java b/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java index 24c11f1496..cb927968a7 100644 --- a/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java @@ -63,6 +63,9 @@ import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterGrayscale; import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterIndexedColor; import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterTrueColor; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.apache.commons.imaging.formats.tiff.TiffImageParser; +import org.apache.commons.imaging.formats.tiff.TiffImagingParameters; import org.apache.commons.imaging.icc.IccProfileParser; public class PngImageParser extends AbstractImageParser implements XmpEmbeddable { @@ -507,21 +510,61 @@ public Dimension getImageSize(final ByteSource byteSource, final PngImagingParam @Override public ImageMetadata getMetadata(final ByteSource byteSource, final PngImagingParameters params) throws ImagingException, IOException { - final List chunks = readChunks(byteSource, new ChunkType[] { ChunkType.tEXt, ChunkType.zTXt, ChunkType.iTXt }, false); + final ChunkType[] chunkTypes = { ChunkType.tEXt, ChunkType.zTXt, ChunkType.iTXt, ChunkType.eXIf }; + final List chunks = readChunks(byteSource, chunkTypes, false); if (chunks.isEmpty()) { return null; } - final GenericImageMetadata result = new GenericImageMetadata(); + final GenericImageMetadata textual = new GenericImageMetadata(); + TiffImageMetadata exif = null; for (final PngChunk chunk : chunks) { - final AbstractPngTextChunk textChunk = (AbstractPngTextChunk) chunk; + if (chunk instanceof AbstractPngTextChunk) { + final AbstractPngTextChunk textChunk = (AbstractPngTextChunk) chunk; + textual.add(textChunk.getKeyword(), textChunk.getText()); + } else if (chunk.getChunkType() == ChunkType.eXIf.value) { + if (exif != null) { + throw new ImagingException("Duplicate eXIf chunk"); + } + exif = (TiffImageMetadata) new TiffImageParser().getMetadata(chunk.getBytes()); + } else { + throw new ImagingException("Unexpected chunk type: " + chunk.getChunkType()); + } + } + + return new PngImageMetadata(textual, exif); + } - result.add(textChunk.getKeyword(), textChunk.getText()); + /** + * @since 1.0-alpha6 + */ + public TiffImageMetadata getExifMetadata(final ByteSource byteSource, TiffImagingParameters params) + throws ImagingException, IOException { + final byte[] bytes = getExifRawData(byteSource); + if (null == bytes) { + return null; } - return result; + if (params == null) { + params = new TiffImagingParameters(); + } + + return (TiffImageMetadata) new TiffImageParser().getMetadata(bytes, params); + } + + /** + * @since 1.0-alpha6 + */ + public byte[] getExifRawData(final ByteSource byteSource) throws ImagingException, IOException { + final List chunks = readChunks(byteSource, new ChunkType[] { ChunkType.eXIf }, true); + + if (chunks.isEmpty()) { + return null; + } + + return chunks.get(0).getBytes(); } @Override @@ -638,29 +681,7 @@ private List readChunks(final InputStream is, final ChunkType[] chunkT final int crc = BinaryFunctions.read4Bytes("CRC", is, "Not a Valid PNG File", getByteOrder()); if (keep) { - if (chunkType == ChunkType.iCCP.value) { - result.add(new PngChunkIccp(length, chunkType, crc, bytes)); - } else if (chunkType == ChunkType.tEXt.value) { - result.add(new PngChunkText(length, chunkType, crc, bytes)); - } else if (chunkType == ChunkType.zTXt.value) { - result.add(new PngChunkZtxt(length, chunkType, crc, bytes)); - } else if (chunkType == ChunkType.IHDR.value) { - result.add(new PngChunkIhdr(length, chunkType, crc, bytes)); - } else if (chunkType == ChunkType.PLTE.value) { - result.add(new PngChunkPlte(length, chunkType, crc, bytes)); - } else if (chunkType == ChunkType.pHYs.value) { - result.add(new PngChunkPhys(length, chunkType, crc, bytes)); - } else if (chunkType == ChunkType.sCAL.value) { - result.add(new PngChunkScal(length, chunkType, crc, bytes)); - } else if (chunkType == ChunkType.IDAT.value) { - result.add(new PngChunkIdat(length, chunkType, crc, bytes)); - } else if (chunkType == ChunkType.gAMA.value) { - result.add(new PngChunkGama(length, chunkType, crc, bytes)); - } else if (chunkType == ChunkType.iTXt.value) { - result.add(new PngChunkItxt(length, chunkType, crc, bytes)); - } else { - result.add(new PngChunk(length, chunkType, crc, bytes)); - } + result.add(ChunkType.makeChunk(length, chunkType, crc, bytes)); if (returnAfterFirst) { return result; diff --git a/src/main/java/org/apache/commons/imaging/formats/png/package-info.java b/src/main/java/org/apache/commons/imaging/formats/png/package-info.java index 72c7528686..fbdfa5c6c8 100644 --- a/src/main/java/org/apache/commons/imaging/formats/png/package-info.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/package-info.java @@ -16,6 +16,13 @@ */ /** - * The PNG image format. + * The PNG (Portable Network Graphics) image format. + *

+ * The implementation is based on the + * PNG specification version 1.2, + * and supports the following extensions: + *

*/ package org.apache.commons.imaging.formats.png; diff --git a/src/test/java/org/apache/commons/imaging/formats/png/PngReadTest.java b/src/test/java/org/apache/commons/imaging/formats/png/PngReadTest.java index 404deb267e..0fa4de0c96 100644 --- a/src/test/java/org/apache/commons/imaging/formats/png/PngReadTest.java +++ b/src/test/java/org/apache/commons/imaging/formats/png/PngReadTest.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.awt.image.BufferedImage; import java.io.File; @@ -33,6 +34,9 @@ import org.apache.commons.imaging.bytesource.ByteSource; import org.apache.commons.imaging.common.GenericImageMetadata; import org.apache.commons.imaging.common.ImageMetadata; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; import org.apache.commons.imaging.internal.Debug; import org.apache.commons.imaging.test.TestResources; import org.junit.jupiter.api.Test; @@ -121,4 +125,31 @@ public void testUncaughtExceptionOssFuzz37607() throws IOException { final PngImageParser parser = new PngImageParser(); assertThrows(ImagingException.class, () -> parser.getBufferedImage(ByteSource.file(file), new PngImagingParameters())); } + + /** + * Test reading EXIF from the 'eXIf' chunk in PNG file. + * + * @throws IOException if it fails to read the test image + * @throws ImagingException if it fails to read the test image + */ + @Test + public void testReadExif() throws IOException, ImagingException { + final String input = "/images/png/IMAGING-340/image-with-exif.png"; + final String file = PngReadTest.class.getResource(input).getFile(); + final PngImageParser parser = new PngImageParser(); + + final PngImageMetadata pngMetadata = (PngImageMetadata) parser.getMetadata(new File(file)); + + TiffImageMetadata exifMetadata = pngMetadata.getExif(); + assertEquals("Glavo", + exifMetadata.findDirectory(TiffDirectoryConstants.DIRECTORY_TYPE_ROOT) + .getFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_DESCRIPTION)); + + PngImageMetadata metadata = (PngImageMetadata) parser.getMetadata(new File(file)); + assertTrue(metadata.getTextualInformation().getItems().isEmpty()); + assertEquals("Glavo", + metadata.getExif() + .findDirectory(TiffDirectoryConstants.DIRECTORY_TYPE_ROOT) + .getFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_DESCRIPTION)); + } } diff --git a/src/test/resources/images/png/IMAGING-340/image-with-exif.png b/src/test/resources/images/png/IMAGING-340/image-with-exif.png new file mode 100644 index 0000000000000000000000000000000000000000..33224bc795361c6aaf9927a53463cff1bcff6b7d GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYkagwzPnG+$o^EwCgjBOe1uk`0Kx z7^N6kfixo!2QW&**%6Ey49sA0ph_lTC>x{-2;6fL%kn`C4 Date: Wed, 25 Dec 2024 22:34:05 +0100 Subject: [PATCH 2/2] [IMAGING-340] Add changelog --- src/changes/changes.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7b7d406852..aa91a71e4d 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -59,6 +59,7 @@ The type attribute can be add,update,fix,remove. Add an Imaging-specific security page #439. Add Maven property commons.taglist.version for debugging. Add support of GPSHPositioningError in GpsTagConstants #451. + Support Extensions from PNG 1.2 Specification, Version 1.5.0 #269. Bump org.apache.commons:commons-parent from 69 to 78 #400, #406, #428, #430, #436, #442, #445, #545. Bump org.apache.commons:commons-lang3 from 3.14.0 to 3.17.0 #416, #423, #431.