From 967f8e698428d04f06ebb4a4002ec36ca3b799d3 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sat, 27 Mar 2021 14:39:59 +0100 Subject: [PATCH] PICT metadata + PNTG support --- README.md | 3 +- .../imageio/ImageReaderBase.java | 5 +- .../imageio/plugins/pict/PICTImageReader.java | 85 +++++----- .../plugins/pict/PICTImageReaderSpi.java | 8 +- .../imageio/plugins/pict/PICTMetadata.java | 92 +++++++++++ .../imageio/plugins/pict/PICTUtil.java | 4 +- .../plugins/pict/QTBMPDecompressor.java | 40 ++--- .../imageio/plugins/pict/QTDecompressor.java | 12 +- .../plugins/pict/QTGenericDecompressor.java | 36 ++++- .../plugins/pict/QTRAWDecompressor.java | 51 +++--- .../imageio/plugins/pict/QuickTime.java | 30 ++-- .../imageio/plugins/pntg/PNTGImageReader.java | 146 ++++++++++++++++++ .../plugins/pntg/PNTGImageReaderSpi.java | 71 +++++++++ .../imageio/plugins/pntg/PNTGMetadata.java | 86 +++++++++++ .../plugins/pntg/PNTGProviderInfo.java | 56 +++++++ .../services/javax.imageio.spi.ImageReaderSpi | 3 +- .../plugins/pict/QTBMPDecompressorTest.java | 30 ++++ .../pict/QTGenericDecompressorTest.java | 52 +++++++ .../plugins/pict/QTRAWDecompressorTest.java | 46 ++++++ .../plugins/pntg/PNTGImageReaderTest.java | 65 ++++++++ .../plugins/pntg/PNTGMetadataTest.java | 17 ++ .../src/test/resources/mac/MARBLES.MAC | Bin 0 -> 29912 bytes .../src/test/resources/mac/porsches.mac | Bin 0 -> 16512 bytes 23 files changed, 812 insertions(+), 126 deletions(-) create mode 100644 imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTMetadata.java create mode 100644 imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReader.java create mode 100644 imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReaderSpi.java create mode 100644 imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGMetadata.java create mode 100644 imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGProviderInfo.java create mode 100644 imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressorTest.java create mode 100644 imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTGenericDecompressorTest.java create mode 100644 imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTRAWDecompressorTest.java create mode 100644 imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReaderTest.java create mode 100644 imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGMetadataTest.java create mode 100644 imageio/imageio-pict/src/test/resources/mac/MARBLES.MAC create mode 100644 imageio/imageio-pict/src/test/resources/mac/porsches.mac diff --git a/README.md b/README.md index 6b8e80ea..c885241c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ The goal is to create a set of efficient and robust ImageIO plug-ins, that can b | | JPEG Lossless | | ✔ | - | Native & Standard | | [PCX](https://github.com/haraldk/TwelveMonkeys/wiki/PCX-Plugin) | PCX | ZSoft Paintbrush Format | ✔ | - | Standard | | | DCX | Multi-page PCX fax document | ✔ | - | Standard | -| [PICT](https://github.com/haraldk/TwelveMonkeys/wiki/PICT-Plugin) | PICT | Apple Mac Paint Picture Format | ✔ | ✔ | - | +| [PICT](https://github.com/haraldk/TwelveMonkeys/wiki/PICT-Plugin) | PICT | Apple QuickTime Picture Format | ✔ | ✔ | Standard | +| | PNTG | Apple MacPaint Picture Format | ✔ | | Standard | | [PNM](https://github.com/haraldk/TwelveMonkeys/wiki/PNM-Plugin) | PAM | NetPBM Portable Any Map | ✔ | ✔ | Standard | | | PBM | NetPBM Portable Bit Map | ✔ | - | Standard | | | PGM | NetPBM Portable Grey Map | ✔ | - | Standard | diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java index 5055bc64..9ae42c28 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java @@ -265,8 +265,9 @@ public abstract class ImageReaderBase extends ImageReader { // - transferType is ok // - bands are ok // TODO: Test if color model is ok? - if (specifier.getSampleModel().getTransferType() == dest.getSampleModel().getTransferType() && - specifier.getNumBands() <= dest.getSampleModel().getNumBands()) { + if (specifier.getSampleModel().getTransferType() == dest.getSampleModel().getTransferType() + && Arrays.equals(specifier.getSampleModel().getSampleSize(), dest.getSampleModel().getSampleSize()) + && specifier.getNumBands() <= dest.getSampleModel().getNumBands()) { found = true; break; } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java index b1d2b681..e2dda333 100644 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReader.java @@ -68,6 +68,7 @@ import com.twelvemonkeys.io.enc.DecoderStream; import com.twelvemonkeys.io.enc.PackBitsDecoder; import javax.imageio.*; +import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import java.awt.*; @@ -76,11 +77,8 @@ import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.image.*; import java.io.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; import java.util.List; +import java.util.*; /** * Reader for Apple Mac Paint Picture (PICT) format. @@ -123,10 +121,11 @@ public final class PICTImageReader extends ImageReaderBase { private double screenImageYRatio; // List of images created during image import - private List images = new ArrayList<>(); + private final List images = new ArrayList<>(); private long imageStartStreamPos; protected int picSize; + @Deprecated public PICTImageReader() { this(null); } @@ -168,14 +167,14 @@ public final class PICTImageReader extends ImageReaderBase { * @throws IOException if an I/O error occurs while reading the image. */ private void readPICTHeader(final ImageInputStream pStream) throws IOException { - pStream.seek(0l); + pStream.seek(0L); try { readPICTHeader0(pStream); } catch (IIOException e) { // Rest and try again - pStream.seek(0l); + pStream.seek(0L); // Skip first 512 bytes PICTImageReaderSpi.skipNullHeader(pStream); @@ -207,7 +206,7 @@ public final class PICTImageReader extends ImageReaderBase { System.out.println("frame: " + frame); } - // Set default display ratios. 72 dpi is the standard Macintosh resolution. + // Set default display ratios. 72 dpi is the standard Mac resolution. screenImageXRatio = 1.0; screenImageYRatio = 1.0; @@ -215,7 +214,7 @@ public final class PICTImageReader extends ImageReaderBase { boolean isExtendedV2 = false; int version = pStream.readShort(); if (DEBUG) { - System.out.println(String.format("PICT version: 0x%04x", version)); + System.out.printf("PICT version: 0x%04x%n", version); } if (version == (PICT.OP_VERSION << 8) + 0x01) { @@ -231,24 +230,20 @@ public final class PICTImageReader extends ImageReaderBase { int headerVersion = pStream.readInt(); if (DEBUG) { - System.out.println(String.format("headerVersion: 0x%04x", headerVersion)); + System.out.printf("headerVersion: 0x%04x%n", headerVersion); } // TODO: This (headerVersion) should be picture size (bytes) for non-V2-EXT...? // - but.. We should take care to make sure we don't mis-interpret non-PICT data... - //if (headerVersion == PICT.HEADER_V2) { if ((headerVersion & 0xffff0000) != PICT.HEADER_V2_EXT) { // TODO: Test this.. Looks dodgy to me.. // Get the image resolution and calculate the ratio between // the default Mac screen resolution and the image resolution - // int y (fixed point) + // int y, x, w(?), h (fixed point) double y2 = PICTUtil.readFixedPoint(pStream); - // int x (fixed point) double x2 = PICTUtil.readFixedPoint(pStream); - // int w (fixed point) double w2 = PICTUtil.readFixedPoint(pStream); // ?! - // int h (fixed point) double h2 = PICTUtil.readFixedPoint(pStream); screenImageXRatio = (w - x) / (w2 - x2); @@ -264,7 +259,7 @@ public final class PICTImageReader extends ImageReaderBase { // int reserved pStream.skipBytes(4); } - else /*if ((headerVersion & 0xffff0000) == PICT.HEADER_V2_EXT)*/ { + else { isExtendedV2 = true; // Get the image resolution // Not sure if they are useful for anything... @@ -281,13 +276,10 @@ public final class PICTImageReader extends ImageReaderBase { // Get the image resolution and calculate the ratio between // the default Mac screen resolution and the image resolution - // short y + // short y, x, h, w short y2 = pStream.readShort(); - // short x short x2 = pStream.readShort(); - // short h short h2 = pStream.readShort(); - // short w short w2 = pStream.readShort(); screenImageXRatio = (w - x) / (double) (w2 - x2); @@ -400,7 +392,7 @@ public final class PICTImageReader extends ImageReaderBase { } break; - case PICT.OP_CLIP_RGN:// OK for RECTS, not for regions yet + case PICT.OP_CLIP_RGN:// OK for RECTs, not for regions yet // Read the region if ((region = readRegion(pStream, bounds)) == null) { throw new IIOException("Could not read region"); @@ -735,12 +727,13 @@ public final class PICTImageReader extends ImageReaderBase { case 0x25: case 0x26: case 0x27: + case 0x2F: // Apple reserved dataLength = pStream.readUnsignedShort(); pStream.readFully(new byte[dataLength], 0, dataLength); if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode); } break; @@ -829,14 +822,6 @@ public final class PICTImageReader extends ImageReaderBase { } break; - case 0x2F: - dataLength = pStream.readUnsignedShort(); - pStream.readFully(new byte[dataLength], 0, dataLength); - if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); - } - break; - //-------------------------------------------------------------------------------- // Rect treatments //-------------------------------------------------------------------------------- @@ -920,7 +905,7 @@ public final class PICTImageReader extends ImageReaderBase { case 0x003e: case 0x003f: if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode); } break; @@ -1092,7 +1077,7 @@ public final class PICTImageReader extends ImageReaderBase { case 0x57: pStream.readFully(new byte[8], 0, 8); if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode); } break; @@ -1187,7 +1172,7 @@ public final class PICTImageReader extends ImageReaderBase { case 0x67: pStream.readFully(new byte[12], 0, 12); if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode); } break; case 0x6d: @@ -1195,7 +1180,7 @@ public final class PICTImageReader extends ImageReaderBase { case 0x6f: pStream.readFully(new byte[4], 0, 4); if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode); } break; @@ -1283,7 +1268,7 @@ public final class PICTImageReader extends ImageReaderBase { case 0x7e: case 0x7f: if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode); } break; @@ -1293,7 +1278,7 @@ public final class PICTImageReader extends ImageReaderBase { // Read the polygon polygon = readPoly(pStream, bounds); if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode); } break; @@ -1384,7 +1369,7 @@ public final class PICTImageReader extends ImageReaderBase { // Read the region region = readRegion(pStream, bounds); if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode); } break; @@ -1414,7 +1399,7 @@ public final class PICTImageReader extends ImageReaderBase { dataLength = pStream.readUnsignedShort(); pStream.readFully(new byte[dataLength], 0, dataLength); if (DEBUG) { - System.out.println(String.format("%s: 0x%04x - length: %d", PICT.APPLE_USE_RESERVED_FIELD, opCode, dataLength)); + System.out.printf("%s: 0x%04x - length: %d%n", PICT.APPLE_USE_RESERVED_FIELD, opCode, dataLength); } break; @@ -1442,7 +1427,7 @@ public final class PICTImageReader extends ImageReaderBase { dataLength = pStream.readUnsignedShort(); pStream.readFully(new byte[dataLength], 0, dataLength); if (DEBUG) { - System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode)); + System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode); } break; @@ -1478,7 +1463,7 @@ public final class PICTImageReader extends ImageReaderBase { // TODO: Read this as well, need test data dataLength = pStream.readInt(); if (DEBUG) { - System.out.println(String.format("unCompressedQuickTime, length %d", dataLength)); + System.out.printf("unCompressedQuickTime, length %d%n", dataLength); } pStream.readFully(new byte[dataLength], 0, dataLength); break; @@ -1515,7 +1500,7 @@ public final class PICTImageReader extends ImageReaderBase { } if (DEBUG) { - System.out.println(String.format("%s: 0x%04x - length: %s", PICT.APPLE_USE_RESERVED_FIELD, opCode, dataLength)); + System.out.printf("%s: 0x%04x - length: %s%n", PICT.APPLE_USE_RESERVED_FIELD, opCode, dataLength); } if (dataLength != 0) { @@ -1577,7 +1562,7 @@ public final class PICTImageReader extends ImageReaderBase { matrix[i] = pStream.readInt(); } if (DEBUG) { - System.out.println(String.format("matrix: %s", Arrays.toString(matrix))); + System.out.printf("matrix: %s%n", Arrays.toString(matrix)); } // Matte @@ -1833,7 +1818,7 @@ public final class PICTImageReader extends ImageReaderBase { //////////////////////////////////////////////////// // TODO: This works for single image PICTs only... // However, this is the most common case. Ok for now - processImageProgress(scanline * 100 / srcRect.height); + processImageProgress(scanline * 100 / (float) srcRect.height); if (abortRequested()) { processReadAborted(); @@ -2134,7 +2119,7 @@ public final class PICTImageReader extends ImageReaderBase { //////////////////////////////////////////////////// // TODO: This works for single image PICTs only... // However, this is the most common case. Ok for now - processImageProgress(scanline * 100 / srcRect.height); + processImageProgress(scanline * 100 / (float) srcRect.height); if (abortRequested()) { processReadAborted(); @@ -2626,7 +2611,7 @@ public final class PICTImageReader extends ImageReaderBase { return getYPtCoord(getPICTFrame().height); } - public Iterator getImageTypes(int pIndex) throws IOException { + public Iterator getImageTypes(int pIndex) { // TODO: The images look slightly different in Preview.. Could indicate the color space is wrong... return Collections.singletonList( ImageTypeSpecifiers.createPacked( @@ -2636,11 +2621,19 @@ public final class PICTImageReader extends ImageReaderBase { ).iterator(); } + @Override + public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { + checkBounds(imageIndex); + getPICTFrame(); // TODO: Would probably be better to use readPictHeader here, but it isn't cached + + return new PICTMetadata(version, screenImageXRatio, screenImageYRatio); + } + protected static void showIt(final BufferedImage pImage, final String pTitle) { ImageReaderBase.showIt(pImage, pTitle); } - public static void main(final String[] pArgs) throws IOException { + public static void main(final String[] pArgs) { ImageReader reader = new PICTImageReader(new PICTImageReaderSpi()); for (String arg : pArgs) { diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java index 27399d00..66d232db 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageReaderSpi.java @@ -71,7 +71,7 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase { try { if (isPICT(stream)) { - // If PICT Clipping format, return true immediately + // If PICT clipboard format, return true immediately return true; } else { @@ -154,8 +154,8 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase { pStream.skipBytes(PICT.PICT_NULL_HEADER_SIZE); } + // NOTE: As the PICT format has a very weak identifier, a true return value is not necessarily a PICT... private boolean isPICT(final ImageInputStream pStream) throws IOException { - // TODO: Need to validate better... // Size may be 0, so we can't use this for validation... pStream.readUnsignedShort(); @@ -169,8 +169,8 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase { return false; } + // Validate magic int magic = pStream.readInt(); - return (magic & 0xffff0000) == PICT.MAGIC_V1 || magic == PICT.MAGIC_V2; } @@ -179,6 +179,6 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase { } public String getDescription(final Locale pLocale) { - return "Apple Mac Paint Picture (PICT) image reader"; + return "Apple MacPaint/QuickDraw Picture (PICT) image reader"; } } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTMetadata.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTMetadata.java new file mode 100644 index 00000000..8a882a48 --- /dev/null +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTMetadata.java @@ -0,0 +1,92 @@ +package com.twelvemonkeys.imageio.plugins.pict; + +import com.twelvemonkeys.imageio.AbstractMetadata; + +import javax.imageio.metadata.IIOMetadataNode; + +/** + * PICTMetadata. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PICTMetadata.java,v 1.0 23/03/2021 haraldk Exp$ + */ +public class PICTMetadata extends AbstractMetadata { + + private final int version; + private final double screenImageXRatio; + private final double screenImageYRatio; + + PICTMetadata(final int version, final double screenImageXRatio, final double screenImageYRatio) { + this.version = version; + this.screenImageXRatio = screenImageXRatio; + this.screenImageYRatio = screenImageYRatio; + } + + @Override + protected IIOMetadataNode getStandardChromaNode() { + IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); + + IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); + chroma.appendChild(csType); + csType.setAttribute("name", "RGB"); + + // NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data) + IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels"); + chroma.appendChild(numChannels); + numChannels.setAttribute("value", "3"); + + IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero"); + chroma.appendChild(blackIsZero); + blackIsZero.setAttribute("value", "TRUE"); + + return chroma; + } + + @Override + protected IIOMetadataNode getStandardDimensionNode() { + if (screenImageXRatio > 0.0d && screenImageYRatio > 0.0d) { + IIOMetadataNode node = new IIOMetadataNode("Dimension"); + double ratio = screenImageXRatio / screenImageYRatio; + IIOMetadataNode subNode = new IIOMetadataNode("PixelAspectRatio"); + subNode.setAttribute("value", "" + ratio); + node.appendChild(subNode); + + return node; + } + return null; + } + + @Override + protected IIOMetadataNode getStandardDataNode() { + IIOMetadataNode data = new IIOMetadataNode("Data"); + + // As this is a vector-ish format, with possibly multiple regions of pixel data, this makes no sense... :-P + // This is, however, consistent with the getRawImageTyp/getImageTypes + + IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration"); + planarConfiguration.setAttribute("value", "PixelInterleaved"); + data.appendChild(planarConfiguration); + + IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat"); + sampleFormat.setAttribute("value", "UnsignedIntegral"); + data.appendChild(sampleFormat); + + IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample"); + bitsPerSample.setAttribute("value", "32"); + data.appendChild(bitsPerSample); + + return data; + } + + @Override + protected IIOMetadataNode getStandardDocumentNode() { + IIOMetadataNode document = new IIOMetadataNode("Document"); + + IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion"); + document.appendChild(formatVersion); + formatVersion.setAttribute("value", Integer.toString(version)); + + return document; + } +} diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTUtil.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTUtil.java index cb80be0b..1c574d3d 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTUtil.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTUtil.java @@ -37,6 +37,7 @@ import java.awt.image.IndexColorModel; import java.io.DataInput; import java.io.IOException; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; /** @@ -76,7 +77,8 @@ final class PICTUtil { static String readIdString(final DataInput pStream) throws IOException { byte[] bytes = new byte[4]; pStream.readFully(bytes); - return new String(bytes, "ASCII"); + + return new String(bytes, StandardCharsets.US_ASCII); } /** diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressor.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressor.java index 7d39b2ea..3ddd7bf2 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressor.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressor.java @@ -30,12 +30,16 @@ package com.twelvemonkeys.imageio.plugins.pict; +import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc; import com.twelvemonkeys.io.FastByteArrayOutputStream; import com.twelvemonkeys.io.LittleEndianDataOutputStream; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.io.*; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.nio.charset.StandardCharsets; /** * QTBMPDecompressor @@ -45,28 +49,24 @@ import java.io.*; * @version $Id: QTBMPDecompressor.java,v 1.0 Feb 16, 2009 9:18:28 PM haraldk Exp$ */ final class QTBMPDecompressor extends QTDecompressor { - public boolean canDecompress(final QuickTime.ImageDesc pDescription) { - return QuickTime.VENDOR_APPLE.equals(pDescription.compressorVendor) && "WRLE".equals(pDescription.compressorIdentifer) - && "bmp ".equals(idString(pDescription.extraDesc, 4)); + public boolean canDecompress(final ImageDesc description) { + return QuickTime.VENDOR_APPLE.equals(description.compressorVendor) + && "WRLE".equals(description.compressorIdentifer) + && "bmp ".equals(idString(description.extraDesc, 4)); } - private static String idString(final byte[] pData, final int pOffset) { - try { - return new String(pData, pOffset, 4, "ASCII"); - } - catch (UnsupportedEncodingException e) { - throw new Error("ASCII charset must always be supported", e); - } + private static String idString(final byte[] data, final int offset) { + return new String(data, offset, 4, StandardCharsets.US_ASCII); } - public BufferedImage decompress(final QuickTime.ImageDesc pDescription, final InputStream pStream) throws IOException { - return ImageIO.read(new SequenceInputStream(fakeBMPHeader(pDescription), pStream)); + public BufferedImage decompress(final ImageDesc description, final InputStream stream) throws IOException { + return ImageIO.read(new SequenceInputStream(fakeBMPHeader(description), stream)); } - private InputStream fakeBMPHeader(final QuickTime.ImageDesc pDescription) throws IOException { + private InputStream fakeBMPHeader(final ImageDesc description) throws IOException { int bmpHeaderSize = 14; int dibHeaderSize = 12; // 12: OS/2 V1 - ByteArrayOutputStream out = new FastByteArrayOutputStream(bmpHeaderSize + dibHeaderSize); + FastByteArrayOutputStream out = new FastByteArrayOutputStream(bmpHeaderSize + dibHeaderSize); LittleEndianDataOutputStream stream = new LittleEndianDataOutputStream(out); @@ -74,7 +74,7 @@ final class QTBMPDecompressor extends QTDecompressor { stream.writeByte('B'); stream.writeByte('M'); - stream.writeInt(pDescription.dataSize + bmpHeaderSize + dibHeaderSize); // Data size + BMP header + DIB header + stream.writeInt(description.dataSize + bmpHeaderSize + dibHeaderSize); // Data size + BMP header + DIB header stream.writeShort(0x0); // Reserved stream.writeShort(0x0); // Reserved @@ -84,12 +84,12 @@ final class QTBMPDecompressor extends QTDecompressor { // DIB header stream.writeInt(dibHeaderSize); // DIB header size - stream.writeShort(pDescription.width); - stream.writeShort(pDescription.height); + stream.writeShort(description.width); + stream.writeShort(description.height); stream.writeShort(1); // Planes, only legal value: 1 - stream.writeShort(pDescription.depth); // Bit depth + stream.writeShort(description.depth); // Bit depth - return new ByteArrayInputStream(out.toByteArray()); + return out.createInputStream(); } } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTDecompressor.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTDecompressor.java index 28b58aca..adf42a51 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTDecompressor.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTDecompressor.java @@ -30,6 +30,8 @@ package com.twelvemonkeys.imageio.plugins.pict; +import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc; + import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; @@ -46,20 +48,20 @@ abstract class QTDecompressor { * Returns whether this decompressor is capable of decompressing the image * data described by the given image description. * - * @param pDescription the image description ({@code 'idsc'} Atom). + * @param description the image description ({@code 'idsc'} Atom). * @return {@code true} if this decompressor is capable of decompressing * he data in the given image description, otherwise {@code false}. */ - public abstract boolean canDecompress(QuickTime.ImageDesc pDescription); + public abstract boolean canDecompress(ImageDesc description); /** * Decompresses an image. * - * @param pDescription the image description ({@code 'idsc'} Atom). - * @param pStream the image data stream + * @param description the image description ({@code 'idsc'} Atom). + * @param stream the image data stream * @return the decompressed image * * @throws java.io.IOException if an I/O exception occurs during reading. */ - public abstract BufferedImage decompress(QuickTime.ImageDesc pDescription, InputStream pStream) throws IOException; + public abstract BufferedImage decompress(ImageDesc description, InputStream stream) throws IOException; } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTGenericDecompressor.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTGenericDecompressor.java index 4e58c91c..9a9b3ee8 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTGenericDecompressor.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTGenericDecompressor.java @@ -31,9 +31,14 @@ package com.twelvemonkeys.imageio.plugins.pict; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; +import java.util.Iterator; + +import static com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc; /** * QTGenericDecompressor @@ -43,11 +48,36 @@ import java.io.InputStream; * @version $Id: QTGenericDecompressor.java,v 1.0 Feb 16, 2009 9:26:13 PM haraldk Exp$ */ final class QTGenericDecompressor extends QTDecompressor { - public boolean canDecompress(final QuickTime.ImageDesc pDescription) { + public boolean canDecompress(final ImageDesc description) { + // Instead of testing, we just allow everything, and might eventually fail on decompress later... return true; } - public BufferedImage decompress(final QuickTime.ImageDesc pDescription, final InputStream pStream) throws IOException { - return ImageIO.read(pStream); + public BufferedImage decompress(final ImageDesc description, final InputStream stream) throws IOException { + BufferedImage image = ImageIO.read(stream); + + if (image == null) { + return readUsingFormatName(description.compressorIdentifer.trim(), stream); + } + + return image; + } + + private BufferedImage readUsingFormatName(final String formatName, final InputStream stream) throws IOException { + Iterator readers = ImageIO.getImageReadersByFormatName(formatName); + + if (readers.hasNext()) { + ImageReader reader = readers.next(); + + try (ImageInputStream input = ImageIO.createImageInputStream(stream)) { + reader.setInput(input); + return reader.read(0); + } + finally { + reader.dispose(); + } + } + + return null; } } diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTRAWDecompressor.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTRAWDecompressor.java index c585e12d..53f00058 100644 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTRAWDecompressor.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QTRAWDecompressor.java @@ -38,6 +38,9 @@ import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; +import static com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc; +import static com.twelvemonkeys.imageio.plugins.pict.QuickTime.VENDOR_APPLE; + /** * QTRAWDecompressor * @@ -51,21 +54,17 @@ final class QTRAWDecompressor extends QTDecompressor { // - Have a look at com.sun.media.imageio.stream.RawImageInputStream... // TODO: Support different bit depths - public boolean canDecompress(final QuickTime.ImageDesc pDescription) { - return QuickTime.VENDOR_APPLE.equals(pDescription.compressorVendor) - && "raw ".equals(pDescription.compressorIdentifer) - && (pDescription.depth == 24 || pDescription.depth == 32); + public boolean canDecompress(final ImageDesc description) { + return VENDOR_APPLE.equals(description.compressorVendor) + && "raw ".equals(description.compressorIdentifer) + && (description.depth == 24 || description.depth == 32 || description.depth == 40); } - public BufferedImage decompress(final QuickTime.ImageDesc pDescription, final InputStream pStream) throws IOException { - byte[] data = new byte[pDescription.dataSize]; + public BufferedImage decompress(final ImageDesc description, final InputStream stream) throws IOException { + byte[] data = new byte[description.dataSize]; - DataInputStream stream = new DataInputStream(pStream); - try { - stream.readFully(data, 0, pDescription.dataSize); - } - finally { - stream.close(); + try (DataInputStream dataStream = new DataInputStream(stream)) { + dataStream.readFully(data, 0, description.dataSize); } DataBuffer buffer = new DataBufferByte(data, data.length); @@ -73,12 +72,12 @@ final class QTRAWDecompressor extends QTDecompressor { WritableRaster raster; // TODO: Depth parameter can be 1-32 (color) or 33-40 (gray scale) - switch (pDescription.depth) { + switch (description.depth) { case 40: // 8 bit gray (untested) raster = Raster.createInterleavedRaster( buffer, - pDescription.width, pDescription.height, - pDescription.width, 1, + description.width, description.height, + description.width, 1, new int[] {0}, null ); @@ -86,8 +85,8 @@ final class QTRAWDecompressor extends QTDecompressor { case 24: // 24 bit RGB raster = Raster.createInterleavedRaster( buffer, - pDescription.width, pDescription.height, - pDescription.width * 3, 3, + description.width, description.height, + description.width * 3, 3, new int[] {0, 1, 2}, null ); @@ -96,9 +95,9 @@ final class QTRAWDecompressor extends QTDecompressor { // WORKAROUND: There is a bug in the way Java 2D interprets the band offsets in // Raster.createInterleavedRaster (see below) before Java 6. So, instead of // passing a correct offset array below, we swap channel 1 & 3 to make it ABGR... - for (int y = 0; y < pDescription.height; y++) { - for (int x = 0; x < pDescription.width; x++) { - int offset = 4 * y * pDescription.width + x * 4; + for (int y = 0; y < description.height; y++) { + for (int x = 0; x < description.width; x++) { + int offset = 4 * y * description.width + x * 4; byte temp = data[offset + 1]; data[offset + 1] = data[offset + 3]; data[offset + 3] = temp; @@ -107,21 +106,21 @@ final class QTRAWDecompressor extends QTDecompressor { raster = Raster.createInterleavedRaster( buffer, - pDescription.width, pDescription.height, - pDescription.width * 4, 4, + description.width, description.height, + description.width * 4, 4, new int[] {3, 2, 1, 0}, // B & R mixed up. {1, 2, 3, 0} is correct null ); break; default: - throw new IIOException("Unsupported RAW depth: " + pDescription.depth); + throw new IIOException("Unsupported QuickTime RAW depth: " + description.depth); } ColorModel cm = new ComponentColorModel( - pDescription.depth <= 32 ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpace.getInstance(ColorSpace.CS_GRAY), - pDescription.depth == 32, + description.depth <= 32 ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpace.getInstance(ColorSpace.CS_GRAY), + description.depth == 32, false, - pDescription.depth == 32 ? Transparency.TRANSLUCENT : Transparency.OPAQUE, + description.depth == 32 ? Transparency.TRANSLUCENT : Transparency.OPAQUE, DataBuffer.TYPE_BYTE ); diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickTime.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickTime.java index 826f1ccb..50fca198 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickTime.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/QuickTime.java @@ -56,7 +56,7 @@ final class QuickTime { private static final List sDecompressors = Arrays.asList( new QTBMPDecompressor(), new QTRAWDecompressor(), - // The GenericDecompressor must be the last in the list + // The GenericDecompressor MUST be the last in the list, as it claims to read everything... new QTGenericDecompressor() ); @@ -87,7 +87,7 @@ final class QuickTime { kH263CodecType ='h263' kIndeo4CodecType ='IV41' kJPEGCodecType ='jpeg' -> JPEG, SUPPORTED - kMacPaintCodecType ='PNTG' -> Isn't this the PICT format itself? Does that make sense?! ;-) + kMacPaintCodecType ='PNTG' -> PNTG, should work, but lacks test data kMicrosoftVideo1CodecType ='msvc' kMotionJPEGACodecType ='mjpa' kMotionJPEGBCodecType ='mjpb' @@ -99,12 +99,12 @@ final class QuickTime { kQuickDrawCodecType ='qdrw' -> QD? kQuickDrawGXCodecType ='qdgx' -> QD? kRawCodecType ='raw ' -> Raw (A)RGB pixel data - kSGICodecType ='.SGI' + kSGICodecType ='.SGI' -> SGI, should work, but lacks test data k16GrayCodecType ='b16g' -> Raw 16 bit gray data? k64ARGBCodecType ='b64a' -> Raw 64 bit (16 bpp) color data? kSorensonCodecType ='SVQ1' kSorensonYUV9CodecType ='syv9' - kTargaCodecType ='tga ' -> TGA, maybe create a plugin for that + kTargaCodecType ='tga ' -> TGA, should work, but lacks test data k32AlphaGrayCodecType ='b32a' -> 16 bit gray + 16 bit alpha raw data? kTIFFCodecType ='tiff' -> TIFF, SUPPORTED kVectorCodecType ='path' @@ -117,13 +117,13 @@ final class QuickTime { /** * Gets a decompressor that can decompress the described data. * - * @param pDescription the image description ({@code 'idsc'} Atom). + * @param description the image description ({@code 'idsc'} Atom). * @return a decompressor that can decompress data decribed by the given {@link ImageDesc description}, * or {@code null} if no decompressor is found */ - private static QTDecompressor getDecompressor(final ImageDesc pDescription) { + private static QTDecompressor getDecompressor(final ImageDesc description) { for (QTDecompressor decompressor : sDecompressors) { - if (decompressor.canDecompress(pDescription)) { + if (decompressor.canDecompress(description)) { return decompressor; } } @@ -134,13 +134,13 @@ final class QuickTime { /** * Decompresses the QuickTime image data from the given stream. * - * @param pStream the image input stream + * @param stream the image input stream * @return a {@link BufferedImage} containing the image data, or {@code null} if no decompressor is capable of * decompressing the image. * @throws IOException if an I/O exception occurs during read */ - public static BufferedImage decompress(final ImageInputStream pStream) throws IOException { - ImageDesc description = ImageDesc.read(pStream); + public static BufferedImage decompress(final ImageInputStream stream) throws IOException { + ImageDesc description = ImageDesc.read(stream); if (PICTImageReader.DEBUG) { System.out.println(description); @@ -152,12 +152,8 @@ final class QuickTime { return null; } - InputStream streamAdapter = IIOUtil.createStreamAdapter(pStream, description.dataSize); - try { - return decompressor.decompress(description, streamAdapter); - } - finally { - streamAdapter.close(); + try (InputStream streamAdapter = IIOUtil.createStreamAdapter(stream, description.dataSize)) { + return decompressor.decompress(description, streamAdapter); } } @@ -195,7 +191,7 @@ final class QuickTime { byte[] extraDesc; - private ImageDesc() {} + ImageDesc() {} public static ImageDesc read(final DataInput pStream) throws IOException { // The following looks like the 'idsc' Atom (as described in the QuickTime File Format) diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReader.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReader.java new file mode 100644 index 00000000..1c697054 --- /dev/null +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReader.java @@ -0,0 +1,146 @@ +package com.twelvemonkeys.imageio.plugins.pntg; + +import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; +import com.twelvemonkeys.io.enc.DecoderStream; +import com.twelvemonkeys.io.enc.PackBitsDecoder; + +import javax.imageio.IIOException; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.awt.image.*; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; + +import static com.twelvemonkeys.imageio.plugins.pntg.PNTGImageReaderSpi.isMacBinaryPNTG; +import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow; + +/** + * PNTGImageReader. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PNTGImageReader.java,v 1.0 23/03/2021 haraldk Exp$ + */ +public final class PNTGImageReader extends ImageReaderBase { + + private static final Set IMAGE_TYPES = + Collections.singleton(ImageTypeSpecifiers.createIndexed(new int[] {-1, 0}, false, -1, 1, DataBuffer.TYPE_BYTE)); + + protected PNTGImageReader(final ImageReaderSpi provider) { + super(provider); + } + + @Override + protected void resetMembers() { + } + + @Override + public int getWidth(final int imageIndex) throws IOException { + checkBounds(imageIndex); + + return 576; + } + + @Override + public int getHeight(final int imageIndex) throws IOException { + checkBounds(imageIndex); + + return 720; + } + + @Override + public Iterator getImageTypes(final int imageIndex) throws IOException { + checkBounds(imageIndex); + + return IMAGE_TYPES.iterator(); + } + + @Override + public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { + checkBounds(imageIndex); + readHeader(); + + int width = getWidth(imageIndex); + int height = getHeight(imageIndex); + + BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height); + int[] destBands = param != null ? param.getDestinationBands() : null; + + Rectangle srcRegion = new Rectangle(); + Rectangle destRegion = new Rectangle(); + computeRegions(param, width, height, destination, srcRegion, destRegion); + + int xSub = param != null ? param.getSourceXSubsampling() : 1; + int ySub = param != null ? param.getSourceYSubsampling() : 1; + + WritableRaster destRaster = destination.getRaster() + .createWritableChild(destRegion.x, destRegion.y, destRegion.width, destRegion.height, 0, 0, destBands); + + Raster rowRaster = Raster.createPackedRaster(DataBuffer.TYPE_BYTE, width, 1, 1, 1, null) + .createChild(srcRegion.x, 0, destRegion.width, 1, 0, 0, destBands); + + processImageStarted(imageIndex); + + readData(srcRegion, destRegion, xSub, ySub, destRaster, rowRaster); + + processImageComplete(); + + return destination; + } + + private void readData(Rectangle srcRegion, Rectangle destRegion, int xSub, int ySub, WritableRaster destRaster, Raster rowRaster) throws IOException { + byte[] rowData = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); + + try (DataInputStream decoderStream = new DataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(imageInput), new PackBitsDecoder()))) { + int srcMaxY = srcRegion.y + srcRegion.height; + for (int y = 0; y < srcMaxY; y++) { + decoderStream.readFully(rowData); + + if (y >= srcRegion.y && y % ySub == 0) { + subsampleRow(rowData, srcRegion.x, srcRegion.width, rowData, destRegion.x, 1, 1, xSub); + + int destY = (y - srcRegion.y) / ySub; + destRaster.setDataElements(0, destY, rowRaster); + + processImageProgress(y / (float) srcMaxY); + } + + if (abortRequested()) { + processReadAborted(); + break; + } + } + } + } + + @Override + public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { + checkBounds(imageIndex); + + return new PNTGMetadata(); + } + + private void readHeader() throws IOException { + if (isMacBinaryPNTG(imageInput)) { + // Seek to end of MacBinary header + // TODO: Could actually get the file name, creation date etc metadata from this data + imageInput.seek(128); + } + else { + imageInput.seek(0); + } + + // Skip pattern data section (usually all 0s) + if (imageInput.skipBytes(512) != 512) { + throw new IIOException("Could not skip pattern data"); + } + } +} diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReaderSpi.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReaderSpi.java new file mode 100644 index 00000000..27f852ec --- /dev/null +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReaderSpi.java @@ -0,0 +1,71 @@ +package com.twelvemonkeys.imageio.plugins.pntg; + +import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; + +import javax.imageio.stream.ImageInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.util.Locale; + +/** + * PNTGImageReaderSpi. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PNTGImageReaderSpi.java,v 1.0 23/03/2021 haraldk Exp$ + */ +public final class PNTGImageReaderSpi extends ImageReaderSpiBase { + public PNTGImageReaderSpi() { + super(new PNTGProviderInfo()); + } + + @Override + public boolean canDecodeInput(final Object source) throws IOException { + if (!(source instanceof ImageInputStream)) { + return false; + } + + ImageInputStream stream = (ImageInputStream) source; + stream.mark(); + + try { + // TODO: Figure out how to read the files without the MacBinary header... + // Probably not possible, as it's just 512 bytes of nulls OR pattern information + return isMacBinaryPNTG(stream); + } + catch (EOFException ignore) { + return false; + } + finally { + stream.reset(); + } + } + + static boolean isMacBinaryPNTG(final ImageInputStream stream) throws IOException { + stream.seek(0); + + if (stream.readByte() != 0) { + return false; + } + + byte nameLen = stream.readByte(); + if (nameLen < 0 || nameLen > 63) { + return false; + } + + stream.skipBytes(63); + + // Validate that type is PNTG and that next 4 bytes are all within the ASCII range, typically 'MPNT' + return stream.readInt() == ('P' << 24 | 'N' << 16 | 'T' << 8 | 'G') && (stream.readInt() & 0x80808080) == 0; + } + + @Override + public PNTGImageReader createReaderInstance(final Object extension) { + return new PNTGImageReader(this); + } + + @Override + public String getDescription(final Locale locale) { + return "Apple MacPaint Painting (PNTG) image reader"; + } +} diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGMetadata.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGMetadata.java new file mode 100644 index 00000000..90f22b95 --- /dev/null +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGMetadata.java @@ -0,0 +1,86 @@ +package com.twelvemonkeys.imageio.plugins.pntg; + +import com.twelvemonkeys.imageio.AbstractMetadata; + +import javax.imageio.metadata.IIOMetadataNode; + +/** + * PNTGMetadata. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PNTGMetadata.java,v 1.0 23/03/2021 haraldk Exp$ + */ +public class PNTGMetadata extends AbstractMetadata { + @Override + protected IIOMetadataNode getStandardChromaNode() { + IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); + + IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); + chroma.appendChild(csType); + csType.setAttribute("name", "GRAY"); + + // NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data) + IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels"); + chroma.appendChild(numChannels); + numChannels.setAttribute("value", "1"); + + IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero"); + chroma.appendChild(blackIsZero); + blackIsZero.setAttribute("value", "FALSE"); + + return chroma; + } + + @Override + protected IIOMetadataNode getStandardCompressionNode() { + IIOMetadataNode compressionNode = new IIOMetadataNode("Compression"); + + IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName"); + compressionTypeName.setAttribute("value", "PackBits"); // RLE? + compressionNode.appendChild(compressionTypeName); + compressionNode.appendChild(new IIOMetadataNode("Lossless")); + // "value" defaults to TRUE + + return compressionNode; + } + + @Override + protected IIOMetadataNode getStandardDataNode() { + IIOMetadataNode data = new IIOMetadataNode("Data"); + + // PlanarConfiguration + IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration"); + planarConfiguration.setAttribute("value", "PixelInterleaved"); + data.appendChild(planarConfiguration); + + IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat"); + sampleFormat.setAttribute("value", "UnsignedIntegral"); + data.appendChild(sampleFormat); + + IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample"); + bitsPerSample.setAttribute("value", "1"); + data.appendChild(bitsPerSample); + + return data; + } + + @Override + protected IIOMetadataNode getStandardDocumentNode() { + IIOMetadataNode document = new IIOMetadataNode("Document"); + + IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion"); + document.appendChild(formatVersion); + formatVersion.setAttribute("value", "1.0"); + + // TODO: We could get the file creation time from MacBinary header here... + + return document; + } + + @Override + protected IIOMetadataNode getStandardTextNode() { + // TODO: We could get the file name from MacBinary header here... + return super.getStandardTextNode(); + } +} diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGProviderInfo.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGProviderInfo.java new file mode 100644 index 00000000..a30edcae --- /dev/null +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGProviderInfo.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2015, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.pntg; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; + +/** + * PNTGProviderInfo. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: PNTGProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$ + */ +final class PNTGProviderInfo extends ReaderWriterProviderInfo { + protected PNTGProviderInfo() { + super( + PNTGProviderInfo.class, + new String[] {"pntg", "PNTG"}, + new String[] {"mac", "pic", "pntg"}, + new String[] {"image/x-pntg"}, + "com.twelvemonkeys.imageio.plugins.mac.MACImageReader", + new String[] {"com.twelvemonkeys.imageio.plugins.mac.MACImageReaderSpi"}, + null, null, + false, null, null, null, null, + true, null, null, null, null + ); + } +} diff --git a/imageio/imageio-pict/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-pict/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi index 1f4a92e9..d004807f 100755 --- a/imageio/imageio-pict/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi +++ b/imageio/imageio-pict/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -1 +1,2 @@ -com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi \ No newline at end of file +com.twelvemonkeys.imageio.plugins.pntg.PNTGImageReaderSpi +com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi diff --git a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressorTest.java b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressorTest.java new file mode 100644 index 00000000..860a0f35 --- /dev/null +++ b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTBMPDecompressorTest.java @@ -0,0 +1,30 @@ +package com.twelvemonkeys.imageio.plugins.pict; + +import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc; + +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertTrue; + +/** + * QTBMPDecompressorTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: QTBMPDecompressorTest.java,v 1.0 24/03/2021 haraldk Exp$ + */ +public class QTBMPDecompressorTest { + @Test + public void canDecompress() { + QTDecompressor decompressor = new QTBMPDecompressor(); + + ImageDesc description = new ImageDesc(); + description.compressorVendor = QuickTime.VENDOR_APPLE; + description.compressorIdentifer = "WRLE"; + description.extraDesc = "....bmp ...something...".getBytes(StandardCharsets.UTF_8); + + assertTrue(decompressor.canDecompress(description)); + } +} \ No newline at end of file diff --git a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTGenericDecompressorTest.java b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTGenericDecompressorTest.java new file mode 100644 index 00000000..cbb0707b --- /dev/null +++ b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTGenericDecompressorTest.java @@ -0,0 +1,52 @@ +package com.twelvemonkeys.imageio.plugins.pict; + +import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * QTBMPDecompressorTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: QTBMPDecompressorTest.java,v 1.0 24/03/2021 haraldk Exp$ + */ +public class QTGenericDecompressorTest { + private ImageDesc createDescription(final String identifer, final String name, final int depth) { + ImageDesc description = new ImageDesc(); + description.compressorVendor = QuickTime.VENDOR_APPLE; + description.compressorIdentifer = identifer; + description.compressorName = name; + description.depth = (short) depth; + + return description; + } + + @Test + public void canDecompressJPEG() { + QTDecompressor decompressor = new QTGenericDecompressor(); + + assertTrue(decompressor.canDecompress(createDescription("jpeg", "Photo - JPEG", 8))); + assertTrue(decompressor.canDecompress(createDescription("jpeg", "Photo - JPEG", 24))); + } + + @Test + public void canDecompressPNG() { + QTDecompressor decompressor = new QTGenericDecompressor(); + + assertTrue(decompressor.canDecompress(createDescription("png ", "PNG", 8))); + assertTrue(decompressor.canDecompress(createDescription("png ", "PNG", 24))); + assertTrue(decompressor.canDecompress(createDescription("png ", "PNG", 32))); + } + + @Test + public void canDecompressTIFF() { + QTDecompressor decompressor = new QTGenericDecompressor(); + + assertTrue(decompressor.canDecompress(createDescription("tiff", "TIFF", 8))); + assertTrue(decompressor.canDecompress(createDescription("tiff", "TIFF", 24))); + assertTrue(decompressor.canDecompress(createDescription("tiff", "TIFF", 32))); + } +} \ No newline at end of file diff --git a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTRAWDecompressorTest.java b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTRAWDecompressorTest.java new file mode 100644 index 00000000..a8188432 --- /dev/null +++ b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pict/QTRAWDecompressorTest.java @@ -0,0 +1,46 @@ +package com.twelvemonkeys.imageio.plugins.pict; + +import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * QTBMPDecompressorTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: QTBMPDecompressorTest.java,v 1.0 24/03/2021 haraldk Exp$ + */ +public class QTRAWDecompressorTest { + private ImageDesc createDescription(int bitDepth) { + ImageDesc description = new ImageDesc(); + description.compressorVendor = QuickTime.VENDOR_APPLE; + description.compressorIdentifer = "raw "; + description.depth = (short) bitDepth; + + return description; + } + + @Test + public void canDecompressRGB() { + QTDecompressor decompressor = new QTRAWDecompressor(); + + assertTrue(decompressor.canDecompress(createDescription(24))); + } + + @Test + public void canDecompressRGBA() { + QTDecompressor decompressor = new QTRAWDecompressor(); + + assertTrue(decompressor.canDecompress(createDescription(32))); + } + + @Test + public void canDecompressGray() { + QTDecompressor decompressor = new QTRAWDecompressor(); + + assertTrue(decompressor.canDecompress(createDescription(40))); + } +} \ No newline at end of file diff --git a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReaderTest.java b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReaderTest.java new file mode 100644 index 00000000..8b75cce9 --- /dev/null +++ b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGImageReaderTest.java @@ -0,0 +1,65 @@ +package com.twelvemonkeys.imageio.plugins.pntg; + +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; + +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +import java.awt.*; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * PNTGImageReaderTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PNTGImageReaderTest.java,v 1.0 23/03/2021 haraldk Exp$ + */ +public class PNTGImageReaderTest extends ImageReaderAbstractTest { + + @Override + protected ImageReaderSpi createProvider() { + return new PNTGImageReaderSpi(); + } + + @Override + protected List getTestData() { + return Arrays.asList(new TestData(getClassLoaderResource("/mac/porsches.mac"), new Dimension(576, 720)), + new TestData(getClassLoaderResource("/mac/MARBLES.MAC"), new Dimension(576, 720))); + } + + @Override + protected List getFormatNames() { + return Arrays.asList("PNTG", "pntg"); + } + + @Override + protected List getSuffixes() { + return Arrays.asList("mac", "pntg"); + } + + @Override + protected List getMIMETypes() { + return Collections.singletonList("image/x-pntg"); + } + + @Override + public void testProviderCanRead() throws IOException { + // TODO: This a kind of hack... + // Currently, the provider don't claim to read the MARBLES.MAC image, + // as it lacks the MacBinary header and thus no way to identify format. + // We can still read it, so we'll include it in the other tests. + List testData = getTestData().subList(0, 1); + + for (TestData data : testData) { + ImageInputStream stream = data.getInputStream(); + assertNotNull(stream); + assertTrue("Provider is expected to be able to decode data: " + data, provider.canDecodeInput(stream)); + } + } +} \ No newline at end of file diff --git a/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGMetadataTest.java b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGMetadataTest.java new file mode 100644 index 00000000..27cf8f9a --- /dev/null +++ b/imageio/imageio-pict/src/test/java/com/twelvemonkeys/imageio/plugins/pntg/PNTGMetadataTest.java @@ -0,0 +1,17 @@ +package com.twelvemonkeys.imageio.plugins.pntg; + +import org.junit.Test; + +/** + * PNTGMetadataTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PNTGMetadataTest.java,v 1.0 23/03/2021 haraldk Exp$ + */ +public class PNTGMetadataTest { + @Test + public void testCreate() { + new PNTGMetadata(); + } +} \ No newline at end of file diff --git a/imageio/imageio-pict/src/test/resources/mac/MARBLES.MAC b/imageio/imageio-pict/src/test/resources/mac/MARBLES.MAC new file mode 100644 index 0000000000000000000000000000000000000000..668d06833f95212aeea21d7bbd50fad1f74b9201 GIT binary patch literal 29912 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 aP^mI)QS$$NS@0_DzkmMQ0{?$4@P7cVMq6V5 literal 0 HcmV?d00001 diff --git a/imageio/imageio-pict/src/test/resources/mac/porsches.mac b/imageio/imageio-pict/src/test/resources/mac/porsches.mac new file mode 100644 index 0000000000000000000000000000000000000000..e50096acefc4b55a7b7250e572230a2e1bb02434 GIT binary patch literal 16512 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 literal 0 HcmV?d00001