PICT metadata + PNTG support

This commit is contained in:
Harald Kuhr 2021-03-27 14:39:59 +01:00
parent bb650e5280
commit 967f8e6984
23 changed files with 812 additions and 126 deletions

View File

@ -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 | | | JPEG Lossless | | ✔ | - | Native & Standard |
| [PCX](https://github.com/haraldk/TwelveMonkeys/wiki/PCX-Plugin) | PCX | ZSoft Paintbrush Format | ✔ | - | Standard | | [PCX](https://github.com/haraldk/TwelveMonkeys/wiki/PCX-Plugin) | PCX | ZSoft Paintbrush Format | ✔ | - | Standard |
| | DCX | Multi-page PCX fax document | ✔ | - | 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 | | [PNM](https://github.com/haraldk/TwelveMonkeys/wiki/PNM-Plugin) | PAM | NetPBM Portable Any Map | ✔ | ✔ | Standard |
| | PBM | NetPBM Portable Bit Map | ✔ | - | Standard | | | PBM | NetPBM Portable Bit Map | ✔ | - | Standard |
| | PGM | NetPBM Portable Grey Map | ✔ | - | Standard | | | PGM | NetPBM Portable Grey Map | ✔ | - | Standard |

View File

@ -265,8 +265,9 @@ public abstract class ImageReaderBase extends ImageReader {
// - transferType is ok // - transferType is ok
// - bands are ok // - bands are ok
// TODO: Test if color model is ok? // TODO: Test if color model is ok?
if (specifier.getSampleModel().getTransferType() == dest.getSampleModel().getTransferType() && if (specifier.getSampleModel().getTransferType() == dest.getSampleModel().getTransferType()
specifier.getNumBands() <= dest.getSampleModel().getNumBands()) { && Arrays.equals(specifier.getSampleModel().getSampleSize(), dest.getSampleModel().getSampleSize())
&& specifier.getNumBands() <= dest.getSampleModel().getNumBands()) {
found = true; found = true;
break; break;
} }

View File

@ -68,6 +68,7 @@ import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder; import com.twelvemonkeys.io.enc.PackBitsDecoder;
import javax.imageio.*; import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
@ -76,11 +77,8 @@ import java.awt.geom.AffineTransform;
import java.awt.geom.Area; import java.awt.geom.Area;
import java.awt.image.*; import java.awt.image.*;
import java.io.*; 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.List;
import java.util.*;
/** /**
* Reader for Apple Mac Paint Picture (PICT) format. * Reader for Apple Mac Paint Picture (PICT) format.
@ -123,10 +121,11 @@ public final class PICTImageReader extends ImageReaderBase {
private double screenImageYRatio; private double screenImageYRatio;
// List of images created during image import // List of images created during image import
private List<BufferedImage> images = new ArrayList<>(); private final List<BufferedImage> images = new ArrayList<>();
private long imageStartStreamPos; private long imageStartStreamPos;
protected int picSize; protected int picSize;
@Deprecated
public PICTImageReader() { public PICTImageReader() {
this(null); this(null);
} }
@ -168,14 +167,14 @@ public final class PICTImageReader extends ImageReaderBase {
* @throws IOException if an I/O error occurs while reading the image. * @throws IOException if an I/O error occurs while reading the image.
*/ */
private void readPICTHeader(final ImageInputStream pStream) throws IOException { private void readPICTHeader(final ImageInputStream pStream) throws IOException {
pStream.seek(0l); pStream.seek(0L);
try { try {
readPICTHeader0(pStream); readPICTHeader0(pStream);
} }
catch (IIOException e) { catch (IIOException e) {
// Rest and try again // Rest and try again
pStream.seek(0l); pStream.seek(0L);
// Skip first 512 bytes // Skip first 512 bytes
PICTImageReaderSpi.skipNullHeader(pStream); PICTImageReaderSpi.skipNullHeader(pStream);
@ -207,7 +206,7 @@ public final class PICTImageReader extends ImageReaderBase {
System.out.println("frame: " + frame); 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; screenImageXRatio = 1.0;
screenImageYRatio = 1.0; screenImageYRatio = 1.0;
@ -215,7 +214,7 @@ public final class PICTImageReader extends ImageReaderBase {
boolean isExtendedV2 = false; boolean isExtendedV2 = false;
int version = pStream.readShort(); int version = pStream.readShort();
if (DEBUG) { 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) { if (version == (PICT.OP_VERSION << 8) + 0x01) {
@ -231,24 +230,20 @@ public final class PICTImageReader extends ImageReaderBase {
int headerVersion = pStream.readInt(); int headerVersion = pStream.readInt();
if (DEBUG) { 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...? // 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... // - 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) { if ((headerVersion & 0xffff0000) != PICT.HEADER_V2_EXT) {
// TODO: Test this.. Looks dodgy to me.. // TODO: Test this.. Looks dodgy to me..
// Get the image resolution and calculate the ratio between // Get the image resolution and calculate the ratio between
// the default Mac screen resolution and the image resolution // 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); double y2 = PICTUtil.readFixedPoint(pStream);
// int x (fixed point)
double x2 = PICTUtil.readFixedPoint(pStream); double x2 = PICTUtil.readFixedPoint(pStream);
// int w (fixed point)
double w2 = PICTUtil.readFixedPoint(pStream); // ?! double w2 = PICTUtil.readFixedPoint(pStream); // ?!
// int h (fixed point)
double h2 = PICTUtil.readFixedPoint(pStream); double h2 = PICTUtil.readFixedPoint(pStream);
screenImageXRatio = (w - x) / (w2 - x2); screenImageXRatio = (w - x) / (w2 - x2);
@ -264,7 +259,7 @@ public final class PICTImageReader extends ImageReaderBase {
// int reserved // int reserved
pStream.skipBytes(4); pStream.skipBytes(4);
} }
else /*if ((headerVersion & 0xffff0000) == PICT.HEADER_V2_EXT)*/ { else {
isExtendedV2 = true; isExtendedV2 = true;
// Get the image resolution // Get the image resolution
// Not sure if they are useful for anything... // 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 // Get the image resolution and calculate the ratio between
// the default Mac screen resolution and the image resolution // the default Mac screen resolution and the image resolution
// short y // short y, x, h, w
short y2 = pStream.readShort(); short y2 = pStream.readShort();
// short x
short x2 = pStream.readShort(); short x2 = pStream.readShort();
// short h
short h2 = pStream.readShort(); short h2 = pStream.readShort();
// short w
short w2 = pStream.readShort(); short w2 = pStream.readShort();
screenImageXRatio = (w - x) / (double) (w2 - x2); screenImageXRatio = (w - x) / (double) (w2 - x2);
@ -400,7 +392,7 @@ public final class PICTImageReader extends ImageReaderBase {
} }
break; 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 // Read the region
if ((region = readRegion(pStream, bounds)) == null) { if ((region = readRegion(pStream, bounds)) == null) {
throw new IIOException("Could not read region"); throw new IIOException("Could not read region");
@ -735,12 +727,13 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x25: case 0x25:
case 0x26: case 0x26:
case 0x27: case 0x27:
case 0x2F:
// Apple reserved // Apple reserved
dataLength = pStream.readUnsignedShort(); dataLength = pStream.readUnsignedShort();
pStream.readFully(new byte[dataLength], 0, dataLength); pStream.readFully(new byte[dataLength], 0, dataLength);
if (DEBUG) { 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; break;
@ -829,14 +822,6 @@ public final class PICTImageReader extends ImageReaderBase {
} }
break; 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 // Rect treatments
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
@ -920,7 +905,7 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x003e: case 0x003e:
case 0x003f: case 0x003f:
if (DEBUG) { 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; break;
@ -1092,7 +1077,7 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x57: case 0x57:
pStream.readFully(new byte[8], 0, 8); pStream.readFully(new byte[8], 0, 8);
if (DEBUG) { 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; break;
@ -1187,7 +1172,7 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x67: case 0x67:
pStream.readFully(new byte[12], 0, 12); pStream.readFully(new byte[12], 0, 12);
if (DEBUG) { 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; break;
case 0x6d: case 0x6d:
@ -1195,7 +1180,7 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x6f: case 0x6f:
pStream.readFully(new byte[4], 0, 4); pStream.readFully(new byte[4], 0, 4);
if (DEBUG) { 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; break;
@ -1283,7 +1268,7 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x7e: case 0x7e:
case 0x7f: case 0x7f:
if (DEBUG) { 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; break;
@ -1293,7 +1278,7 @@ public final class PICTImageReader extends ImageReaderBase {
// Read the polygon // Read the polygon
polygon = readPoly(pStream, bounds); polygon = readPoly(pStream, bounds);
if (DEBUG) { 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; break;
@ -1384,7 +1369,7 @@ public final class PICTImageReader extends ImageReaderBase {
// Read the region // Read the region
region = readRegion(pStream, bounds); region = readRegion(pStream, bounds);
if (DEBUG) { 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; break;
@ -1414,7 +1399,7 @@ public final class PICTImageReader extends ImageReaderBase {
dataLength = pStream.readUnsignedShort(); dataLength = pStream.readUnsignedShort();
pStream.readFully(new byte[dataLength], 0, dataLength); pStream.readFully(new byte[dataLength], 0, dataLength);
if (DEBUG) { 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; break;
@ -1442,7 +1427,7 @@ public final class PICTImageReader extends ImageReaderBase {
dataLength = pStream.readUnsignedShort(); dataLength = pStream.readUnsignedShort();
pStream.readFully(new byte[dataLength], 0, dataLength); pStream.readFully(new byte[dataLength], 0, dataLength);
if (DEBUG) { 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; break;
@ -1478,7 +1463,7 @@ public final class PICTImageReader extends ImageReaderBase {
// TODO: Read this as well, need test data // TODO: Read this as well, need test data
dataLength = pStream.readInt(); dataLength = pStream.readInt();
if (DEBUG) { 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); pStream.readFully(new byte[dataLength], 0, dataLength);
break; break;
@ -1515,7 +1500,7 @@ public final class PICTImageReader extends ImageReaderBase {
} }
if (DEBUG) { 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) { if (dataLength != 0) {
@ -1577,7 +1562,7 @@ public final class PICTImageReader extends ImageReaderBase {
matrix[i] = pStream.readInt(); matrix[i] = pStream.readInt();
} }
if (DEBUG) { if (DEBUG) {
System.out.println(String.format("matrix: %s", Arrays.toString(matrix))); System.out.printf("matrix: %s%n", Arrays.toString(matrix));
} }
// Matte // Matte
@ -1833,7 +1818,7 @@ public final class PICTImageReader extends ImageReaderBase {
//////////////////////////////////////////////////// ////////////////////////////////////////////////////
// TODO: This works for single image PICTs only... // TODO: This works for single image PICTs only...
// However, this is the most common case. Ok for now // However, this is the most common case. Ok for now
processImageProgress(scanline * 100 / srcRect.height); processImageProgress(scanline * 100 / (float) srcRect.height);
if (abortRequested()) { if (abortRequested()) {
processReadAborted(); processReadAborted();
@ -2134,7 +2119,7 @@ public final class PICTImageReader extends ImageReaderBase {
//////////////////////////////////////////////////// ////////////////////////////////////////////////////
// TODO: This works for single image PICTs only... // TODO: This works for single image PICTs only...
// However, this is the most common case. Ok for now // However, this is the most common case. Ok for now
processImageProgress(scanline * 100 / srcRect.height); processImageProgress(scanline * 100 / (float) srcRect.height);
if (abortRequested()) { if (abortRequested()) {
processReadAborted(); processReadAborted();
@ -2626,7 +2611,7 @@ public final class PICTImageReader extends ImageReaderBase {
return getYPtCoord(getPICTFrame().height); return getYPtCoord(getPICTFrame().height);
} }
public Iterator<ImageTypeSpecifier> getImageTypes(int pIndex) throws IOException { public Iterator<ImageTypeSpecifier> getImageTypes(int pIndex) {
// TODO: The images look slightly different in Preview.. Could indicate the color space is wrong... // TODO: The images look slightly different in Preview.. Could indicate the color space is wrong...
return Collections.singletonList( return Collections.singletonList(
ImageTypeSpecifiers.createPacked( ImageTypeSpecifiers.createPacked(
@ -2636,11 +2621,19 @@ public final class PICTImageReader extends ImageReaderBase {
).iterator(); ).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) { protected static void showIt(final BufferedImage pImage, final String pTitle) {
ImageReaderBase.showIt(pImage, 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()); ImageReader reader = new PICTImageReader(new PICTImageReaderSpi());
for (String arg : pArgs) { for (String arg : pArgs) {

View File

@ -71,7 +71,7 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase {
try { try {
if (isPICT(stream)) { if (isPICT(stream)) {
// If PICT Clipping format, return true immediately // If PICT clipboard format, return true immediately
return true; return true;
} }
else { else {
@ -154,8 +154,8 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase {
pStream.skipBytes(PICT.PICT_NULL_HEADER_SIZE); 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 { 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... // Size may be 0, so we can't use this for validation...
pStream.readUnsignedShort(); pStream.readUnsignedShort();
@ -169,8 +169,8 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase {
return false; return false;
} }
// Validate magic
int magic = pStream.readInt(); int magic = pStream.readInt();
return (magic & 0xffff0000) == PICT.MAGIC_V1 || magic == PICT.MAGIC_V2; 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) { public String getDescription(final Locale pLocale) {
return "Apple Mac Paint Picture (PICT) image reader"; return "Apple MacPaint/QuickDraw Picture (PICT) image reader";
} }
} }

View File

@ -0,0 +1,92 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.metadata.IIOMetadataNode;
/**
* PICTMetadata.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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;
}
}

View File

@ -37,6 +37,7 @@ import java.awt.image.IndexColorModel;
import java.io.DataInput; import java.io.DataInput;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException; import java.nio.charset.UnsupportedCharsetException;
/** /**
@ -76,7 +77,8 @@ final class PICTUtil {
static String readIdString(final DataInput pStream) throws IOException { static String readIdString(final DataInput pStream) throws IOException {
byte[] bytes = new byte[4]; byte[] bytes = new byte[4];
pStream.readFully(bytes); pStream.readFully(bytes);
return new String(bytes, "ASCII");
return new String(bytes, StandardCharsets.US_ASCII);
} }
/** /**

View File

@ -30,12 +30,16 @@
package com.twelvemonkeys.imageio.plugins.pict; package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
import com.twelvemonkeys.io.FastByteArrayOutputStream; import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.LittleEndianDataOutputStream; import com.twelvemonkeys.io.LittleEndianDataOutputStream;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; 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 * QTBMPDecompressor
@ -45,28 +49,24 @@ import java.io.*;
* @version $Id: QTBMPDecompressor.java,v 1.0 Feb 16, 2009 9:18:28 PM haraldk Exp$ * @version $Id: QTBMPDecompressor.java,v 1.0 Feb 16, 2009 9:18:28 PM haraldk Exp$
*/ */
final class QTBMPDecompressor extends QTDecompressor { final class QTBMPDecompressor extends QTDecompressor {
public boolean canDecompress(final QuickTime.ImageDesc pDescription) { public boolean canDecompress(final ImageDesc description) {
return QuickTime.VENDOR_APPLE.equals(pDescription.compressorVendor) && "WRLE".equals(pDescription.compressorIdentifer) return QuickTime.VENDOR_APPLE.equals(description.compressorVendor)
&& "bmp ".equals(idString(pDescription.extraDesc, 4)); && "WRLE".equals(description.compressorIdentifer)
&& "bmp ".equals(idString(description.extraDesc, 4));
} }
private static String idString(final byte[] pData, final int pOffset) { private static String idString(final byte[] data, final int offset) {
try { return new String(data, offset, 4, StandardCharsets.US_ASCII);
return new String(pData, pOffset, 4, "ASCII");
}
catch (UnsupportedEncodingException e) {
throw new Error("ASCII charset must always be supported", e);
}
} }
public BufferedImage decompress(final QuickTime.ImageDesc pDescription, final InputStream pStream) throws IOException { public BufferedImage decompress(final ImageDesc description, final InputStream stream) throws IOException {
return ImageIO.read(new SequenceInputStream(fakeBMPHeader(pDescription), pStream)); 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 bmpHeaderSize = 14;
int dibHeaderSize = 12; // 12: OS/2 V1 int dibHeaderSize = 12; // 12: OS/2 V1
ByteArrayOutputStream out = new FastByteArrayOutputStream(bmpHeaderSize + dibHeaderSize); FastByteArrayOutputStream out = new FastByteArrayOutputStream(bmpHeaderSize + dibHeaderSize);
LittleEndianDataOutputStream stream = new LittleEndianDataOutputStream(out); LittleEndianDataOutputStream stream = new LittleEndianDataOutputStream(out);
@ -74,7 +74,7 @@ final class QTBMPDecompressor extends QTDecompressor {
stream.writeByte('B'); stream.writeByte('B');
stream.writeByte('M'); 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
stream.writeShort(0x0); // Reserved stream.writeShort(0x0); // Reserved
@ -84,12 +84,12 @@ final class QTBMPDecompressor extends QTDecompressor {
// DIB header // DIB header
stream.writeInt(dibHeaderSize); // DIB header size stream.writeInt(dibHeaderSize); // DIB header size
stream.writeShort(pDescription.width); stream.writeShort(description.width);
stream.writeShort(pDescription.height); stream.writeShort(description.height);
stream.writeShort(1); // Planes, only legal value: 1 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();
} }
} }

View File

@ -30,6 +30,8 @@
package com.twelvemonkeys.imageio.plugins.pict; package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -46,20 +48,20 @@ abstract class QTDecompressor {
* Returns whether this decompressor is capable of decompressing the image * Returns whether this decompressor is capable of decompressing the image
* data described by the given image description. * 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 * @return {@code true} if this decompressor is capable of decompressing
* he data in the given image description, otherwise {@code false}. * 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. * Decompresses an image.
* *
* @param pDescription the image description ({@code 'idsc'} Atom). * @param description the image description ({@code 'idsc'} Atom).
* @param pStream the image data stream * @param stream the image data stream
* @return the decompressed image * @return the decompressed image
* *
* @throws java.io.IOException if an I/O exception occurs during reading. * @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;
} }

View File

@ -31,9 +31,14 @@
package com.twelvemonkeys.imageio.plugins.pict; package com.twelvemonkeys.imageio.plugins.pict;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Iterator;
import static com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
/** /**
* QTGenericDecompressor * 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$ * @version $Id: QTGenericDecompressor.java,v 1.0 Feb 16, 2009 9:26:13 PM haraldk Exp$
*/ */
final class QTGenericDecompressor extends QTDecompressor { 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; return true;
} }
public BufferedImage decompress(final QuickTime.ImageDesc pDescription, final InputStream pStream) throws IOException { public BufferedImage decompress(final ImageDesc description, final InputStream stream) throws IOException {
return ImageIO.read(pStream); 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<ImageReader> 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;
} }
} }

View File

@ -38,6 +38,9 @@ import java.io.DataInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import static com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
import static com.twelvemonkeys.imageio.plugins.pict.QuickTime.VENDOR_APPLE;
/** /**
* QTRAWDecompressor * QTRAWDecompressor
* *
@ -51,21 +54,17 @@ final class QTRAWDecompressor extends QTDecompressor {
// - Have a look at com.sun.media.imageio.stream.RawImageInputStream... // - Have a look at com.sun.media.imageio.stream.RawImageInputStream...
// TODO: Support different bit depths // TODO: Support different bit depths
public boolean canDecompress(final QuickTime.ImageDesc pDescription) { public boolean canDecompress(final ImageDesc description) {
return QuickTime.VENDOR_APPLE.equals(pDescription.compressorVendor) return VENDOR_APPLE.equals(description.compressorVendor)
&& "raw ".equals(pDescription.compressorIdentifer) && "raw ".equals(description.compressorIdentifer)
&& (pDescription.depth == 24 || pDescription.depth == 32); && (description.depth == 24 || description.depth == 32 || description.depth == 40);
} }
public BufferedImage decompress(final QuickTime.ImageDesc pDescription, final InputStream pStream) throws IOException { public BufferedImage decompress(final ImageDesc description, final InputStream stream) throws IOException {
byte[] data = new byte[pDescription.dataSize]; byte[] data = new byte[description.dataSize];
DataInputStream stream = new DataInputStream(pStream); try (DataInputStream dataStream = new DataInputStream(stream)) {
try { dataStream.readFully(data, 0, description.dataSize);
stream.readFully(data, 0, pDescription.dataSize);
}
finally {
stream.close();
} }
DataBuffer buffer = new DataBufferByte(data, data.length); DataBuffer buffer = new DataBufferByte(data, data.length);
@ -73,12 +72,12 @@ final class QTRAWDecompressor extends QTDecompressor {
WritableRaster raster; WritableRaster raster;
// TODO: Depth parameter can be 1-32 (color) or 33-40 (gray scale) // 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) case 40: // 8 bit gray (untested)
raster = Raster.createInterleavedRaster( raster = Raster.createInterleavedRaster(
buffer, buffer,
pDescription.width, pDescription.height, description.width, description.height,
pDescription.width, 1, description.width, 1,
new int[] {0}, new int[] {0},
null null
); );
@ -86,8 +85,8 @@ final class QTRAWDecompressor extends QTDecompressor {
case 24: // 24 bit RGB case 24: // 24 bit RGB
raster = Raster.createInterleavedRaster( raster = Raster.createInterleavedRaster(
buffer, buffer,
pDescription.width, pDescription.height, description.width, description.height,
pDescription.width * 3, 3, description.width * 3, 3,
new int[] {0, 1, 2}, new int[] {0, 1, 2},
null 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 // 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 // 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... // 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 y = 0; y < description.height; y++) {
for (int x = 0; x < pDescription.width; x++) { for (int x = 0; x < description.width; x++) {
int offset = 4 * y * pDescription.width + x * 4; int offset = 4 * y * description.width + x * 4;
byte temp = data[offset + 1]; byte temp = data[offset + 1];
data[offset + 1] = data[offset + 3]; data[offset + 1] = data[offset + 3];
data[offset + 3] = temp; data[offset + 3] = temp;
@ -107,21 +106,21 @@ final class QTRAWDecompressor extends QTDecompressor {
raster = Raster.createInterleavedRaster( raster = Raster.createInterleavedRaster(
buffer, buffer,
pDescription.width, pDescription.height, description.width, description.height,
pDescription.width * 4, 4, description.width * 4, 4,
new int[] {3, 2, 1, 0}, // B & R mixed up. {1, 2, 3, 0} is correct new int[] {3, 2, 1, 0}, // B & R mixed up. {1, 2, 3, 0} is correct
null null
); );
break; break;
default: default:
throw new IIOException("Unsupported RAW depth: " + pDescription.depth); throw new IIOException("Unsupported QuickTime RAW depth: " + description.depth);
} }
ColorModel cm = new ComponentColorModel( ColorModel cm = new ComponentColorModel(
pDescription.depth <= 32 ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpace.getInstance(ColorSpace.CS_GRAY), description.depth <= 32 ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpace.getInstance(ColorSpace.CS_GRAY),
pDescription.depth == 32, description.depth == 32,
false, false,
pDescription.depth == 32 ? Transparency.TRANSLUCENT : Transparency.OPAQUE, description.depth == 32 ? Transparency.TRANSLUCENT : Transparency.OPAQUE,
DataBuffer.TYPE_BYTE DataBuffer.TYPE_BYTE
); );

View File

@ -56,7 +56,7 @@ final class QuickTime {
private static final List<QTDecompressor> sDecompressors = Arrays.asList( private static final List<QTDecompressor> sDecompressors = Arrays.asList(
new QTBMPDecompressor(), new QTBMPDecompressor(),
new QTRAWDecompressor(), 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() new QTGenericDecompressor()
); );
@ -87,7 +87,7 @@ final class QuickTime {
kH263CodecType ='h263' kH263CodecType ='h263'
kIndeo4CodecType ='IV41' kIndeo4CodecType ='IV41'
kJPEGCodecType ='jpeg' -> JPEG, SUPPORTED 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' kMicrosoftVideo1CodecType ='msvc'
kMotionJPEGACodecType ='mjpa' kMotionJPEGACodecType ='mjpa'
kMotionJPEGBCodecType ='mjpb' kMotionJPEGBCodecType ='mjpb'
@ -99,12 +99,12 @@ final class QuickTime {
kQuickDrawCodecType ='qdrw' -> QD? kQuickDrawCodecType ='qdrw' -> QD?
kQuickDrawGXCodecType ='qdgx' -> QD? kQuickDrawGXCodecType ='qdgx' -> QD?
kRawCodecType ='raw ' -> Raw (A)RGB pixel data 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? k16GrayCodecType ='b16g' -> Raw 16 bit gray data?
k64ARGBCodecType ='b64a' -> Raw 64 bit (16 bpp) color data? k64ARGBCodecType ='b64a' -> Raw 64 bit (16 bpp) color data?
kSorensonCodecType ='SVQ1' kSorensonCodecType ='SVQ1'
kSorensonYUV9CodecType ='syv9' 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? k32AlphaGrayCodecType ='b32a' -> 16 bit gray + 16 bit alpha raw data?
kTIFFCodecType ='tiff' -> TIFF, SUPPORTED kTIFFCodecType ='tiff' -> TIFF, SUPPORTED
kVectorCodecType ='path' kVectorCodecType ='path'
@ -117,13 +117,13 @@ final class QuickTime {
/** /**
* Gets a decompressor that can decompress the described data. * 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}, * @return a decompressor that can decompress data decribed by the given {@link ImageDesc description},
* or {@code null} if no decompressor is found * 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) { for (QTDecompressor decompressor : sDecompressors) {
if (decompressor.canDecompress(pDescription)) { if (decompressor.canDecompress(description)) {
return decompressor; return decompressor;
} }
} }
@ -134,13 +134,13 @@ final class QuickTime {
/** /**
* Decompresses the QuickTime image data from the given stream. * 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 * @return a {@link BufferedImage} containing the image data, or {@code null} if no decompressor is capable of
* decompressing the image. * decompressing the image.
* @throws IOException if an I/O exception occurs during read * @throws IOException if an I/O exception occurs during read
*/ */
public static BufferedImage decompress(final ImageInputStream pStream) throws IOException { public static BufferedImage decompress(final ImageInputStream stream) throws IOException {
ImageDesc description = ImageDesc.read(pStream); ImageDesc description = ImageDesc.read(stream);
if (PICTImageReader.DEBUG) { if (PICTImageReader.DEBUG) {
System.out.println(description); System.out.println(description);
@ -152,12 +152,8 @@ final class QuickTime {
return null; return null;
} }
InputStream streamAdapter = IIOUtil.createStreamAdapter(pStream, description.dataSize); try (InputStream streamAdapter = IIOUtil.createStreamAdapter(stream, description.dataSize)) {
try { return decompressor.decompress(description, streamAdapter);
return decompressor.decompress(description, streamAdapter);
}
finally {
streamAdapter.close();
} }
} }
@ -195,7 +191,7 @@ final class QuickTime {
byte[] extraDesc; byte[] extraDesc;
private ImageDesc() {} ImageDesc() {}
public static ImageDesc read(final DataInput pStream) throws IOException { public static ImageDesc read(final DataInput pStream) throws IOException {
// The following looks like the 'idsc' Atom (as described in the QuickTime File Format) // The following looks like the 'idsc' Atom (as described in the QuickTime File Format)

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<ImageTypeSpecifier> 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<ImageTypeSpecifier> 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");
}
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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";
}
}

View File

@ -0,0 +1,86 @@
package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.metadata.IIOMetadataNode;
/**
* PNTGMetadata.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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();
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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
);
}
}

View File

@ -1 +1,2 @@
com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi com.twelvemonkeys.imageio.plugins.pntg.PNTGImageReaderSpi
com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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));
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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)));
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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)));
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PNTGImageReaderTest.java,v 1.0 23/03/2021 haraldk Exp$
*/
public class PNTGImageReaderTest extends ImageReaderAbstractTest<PNTGImageReader> {
@Override
protected ImageReaderSpi createProvider() {
return new PNTGImageReaderSpi();
}
@Override
protected List<TestData> 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<String> getFormatNames() {
return Arrays.asList("PNTG", "pntg");
}
@Override
protected List<String> getSuffixes() {
return Arrays.asList("mac", "pntg");
}
@Override
protected List<String> 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> 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));
}
}
}

View File

@ -0,0 +1,17 @@
package com.twelvemonkeys.imageio.plugins.pntg;
import org.junit.Test;
/**
* PNTGMetadataTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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();
}
}