diff --git a/imageio/imageio-xwd/license.txt b/imageio/imageio-xwd/license.txt new file mode 100755 index 00000000..a7ef43a0 --- /dev/null +++ b/imageio/imageio-xwd/license.txt @@ -0,0 +1,27 @@ +Copyright (c) 2020, 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. diff --git a/imageio/imageio-xwd/pom.xml b/imageio/imageio-xwd/pom.xml new file mode 100755 index 00000000..6c903b1b --- /dev/null +++ b/imageio/imageio-xwd/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + com.twelvemonkeys.imageio + imageio + 3.7-SNAPSHOT + + imageio-xwd + TwelveMonkeys :: ImageIO :: XWD plugin + + ImageIO plugin for X11 Window Dump Format (XWD) + + + + + com.twelvemonkeys.imageio + imageio-core + + + com.twelvemonkeys.imageio + imageio-core + test-jar + test + + + diff --git a/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/X11.java b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/X11.java new file mode 100644 index 00000000..93645547 --- /dev/null +++ b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/X11.java @@ -0,0 +1,20 @@ +package com.twelvemonkeys.imageio.plugins.xwd; + +interface X11 { +// int X10_HEADER_SIZE = 40; +// int X10_HEADER_VERSION = 0x06; + + int X11_HEADER_SIZE = 100; + int X11_HEADER_VERSION = 0x07; + + int PIXMAP_FORMAT_XYBITMAP = 0; // 1 bit format + int PIXMAP_FORMAT_XYPIXMAP = 1; // single plane bitmap + int PIXMAP_FORMAT_ZPIXMAP = 2; // multiple bitplanes + + int VISUAL_CLASS_STATIC_GRAY = 0; + int VISUAL_CLASS_GRAY_SCALE = 1; + int VISUAL_CLASS_STATIC_COLOR = 2; + int VISUAL_CLASS_PSEUDO_COLOR = 3; + int VISUAL_CLASS_TRUE_COLOR = 4; + int VISUAL_CLASS_DIRECT_COLOR = 5; +} diff --git a/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageMetadata.java b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageMetadata.java new file mode 100644 index 00000000..020ad941 --- /dev/null +++ b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageMetadata.java @@ -0,0 +1,139 @@ +package com.twelvemonkeys.imageio.plugins.xwd; + +import com.twelvemonkeys.imageio.AbstractMetadata; + +import javax.imageio.metadata.IIOMetadataNode; +import java.nio.ByteOrder; + +import static com.twelvemonkeys.lang.Validate.notNull; + +final class XWDImageMetadata extends AbstractMetadata { + private final XWDX11Header header; + + XWDImageMetadata(XWDX11Header header) { + super(true, null, null, null, null); + this.header = notNull(header, "header"); + } + + @Override + protected IIOMetadataNode getStandardChromaNode() { + IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); + IIOMetadataNode colorSpaceType = new IIOMetadataNode("ColorSpaceType"); + + switch (header.visualClass) { + case X11.VISUAL_CLASS_STATIC_GRAY: + case X11.VISUAL_CLASS_GRAY_SCALE: + colorSpaceType.setAttribute("name", "GRAY"); + break; + default: + colorSpaceType.setAttribute("name", "RGB"); + } + + chroma.appendChild(colorSpaceType); + + // TODO: Depending on visual class OR the presence of color mop!? + switch (header.visualClass) { + case X11.VISUAL_CLASS_STATIC_COLOR: + case X11.VISUAL_CLASS_PSEUDO_COLOR: + IIOMetadataNode palette = new IIOMetadataNode("Palette"); + chroma.appendChild(palette); + + for (int i = 0; i < header.colorMap.getMapSize(); i++) { + IIOMetadataNode paletteEntry = new IIOMetadataNode("PaletteEntry"); + paletteEntry.setAttribute("index", Integer.toString(i)); + + paletteEntry.setAttribute("red", Integer.toString(header.colorMap.getRed(i))); + paletteEntry.setAttribute("green", Integer.toString(header.colorMap.getGreen(i))); + paletteEntry.setAttribute("blue", Integer.toString(header.colorMap.getBlue(i))); + + palette.appendChild(paletteEntry); + } + break; + + default: + // No palette + } + + + return chroma; + } + + @Override + protected IIOMetadataNode getStandardDataNode() { + IIOMetadataNode node = new IIOMetadataNode("Data"); + + IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration"); + planarConfiguration.setAttribute("value", "PixelInterleaved"); + node.appendChild(planarConfiguration); + + IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat"); + node.appendChild(sampleFormat); + + switch (header.visualClass) { + case X11.VISUAL_CLASS_STATIC_COLOR: + case X11.VISUAL_CLASS_PSEUDO_COLOR: + sampleFormat.setAttribute("value", "Index"); + break; + default: + sampleFormat.setAttribute("value", "UnsignedIntegral"); + break; + } + + IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample"); + node.appendChild(bitsPerSample); + + int numComponents = header.numComponents(); + bitsPerSample.setAttribute("value", createListValue(numComponents, Integer.toString(header.bitsPerPixel / numComponents))); + + // SampleMSB + if (header.bitsPerRGB < 8 && header.bitFillOrder == ByteOrder.LITTLE_ENDIAN) { + IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB"); + node.appendChild(sampleMSB); + sampleMSB.setAttribute("value", createListValue(header.numComponents(), "0")); + } + + return node; + } + + + @Override + protected IIOMetadataNode getStandardDocumentNode() { + IIOMetadataNode document = new IIOMetadataNode("Document"); + + // The only format we support is the X11 format, and it's version is 7. + IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion"); + document.appendChild(formatVersion); + formatVersion.setAttribute("value", "7"); + + return document; + } + + @Override + protected IIOMetadataNode getStandardTextNode() { + IIOMetadataNode text = new IIOMetadataNode("Text"); + + if (header.windowName != null) { + IIOMetadataNode node = new IIOMetadataNode("TextEntry"); + text.appendChild(node); + node.setAttribute("keyword", "DocumentName"); // For TIFF interop. :-) + node.setAttribute("value", header.windowName); + } + + return text.hasChildNodes() ? text : null; + } + + // TODO: Candidate superclass method! + private String createListValue(final int itemCount, final String... values) { + StringBuilder buffer = new StringBuilder(); + + for (int i = 0; i < itemCount; i++) { + if (buffer.length() > 0) { + buffer.append(' '); + } + + buffer.append(values[i % values.length]); + } + + return buffer.toString(); + } +} diff --git a/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageReader.java b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageReader.java new file mode 100644 index 00000000..4826eb0e --- /dev/null +++ b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageReader.java @@ -0,0 +1,225 @@ +package com.twelvemonkeys.imageio.plugins.xwd; + +import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; + +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.color.ColorSpace; +import java.awt.image.*; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.Collections; +import java.util.Iterator; + +import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow; + +final class XWDImageReader extends ImageReaderBase { + // TODO: This table is also in c.t.i.p.tiff.ReverseInputStream, consider moving to common util? + // http://graphics.stanford.edu/~seander/bithacks.html + static final byte[] BIT_REVERSE_TABLE = { + 0x00, (byte) 0x80, 0x40, (byte) 0xC0, 0x20, (byte) 0xA0, 0x60, (byte) 0xE0, 0x10, (byte) 0x90, 0x50, (byte) 0xD0, 0x30, (byte) 0xB0, 0x70, (byte) 0xF0, + 0x08, (byte) 0x88, 0x48, (byte) 0xC8, 0x28, (byte) 0xA8, 0x68, (byte) 0xE8, 0x18, (byte) 0x98, 0x58, (byte) 0xD8, 0x38, (byte) 0xB8, 0x78, (byte) 0xF8, + 0x04, (byte) 0x84, 0x44, (byte) 0xC4, 0x24, (byte) 0xA4, 0x64, (byte) 0xE4, 0x14, (byte) 0x94, 0x54, (byte) 0xD4, 0x34, (byte) 0xB4, 0x74, (byte) 0xF4, + 0x0C, (byte) 0x8C, 0x4C, (byte) 0xCC, 0x2C, (byte) 0xAC, 0x6C, (byte) 0xEC, 0x1C, (byte) 0x9C, 0x5C, (byte) 0xDC, 0x3C, (byte) 0xBC, 0x7C, (byte) 0xFC, + 0x02, (byte) 0x82, 0x42, (byte) 0xC2, 0x22, (byte) 0xA2, 0x62, (byte) 0xE2, 0x12, (byte) 0x92, 0x52, (byte) 0xD2, 0x32, (byte) 0xB2, 0x72, (byte) 0xF2, + 0x0A, (byte) 0x8A, 0x4A, (byte) 0xCA, 0x2A, (byte) 0xAA, 0x6A, (byte) 0xEA, 0x1A, (byte) 0x9A, 0x5A, (byte) 0xDA, 0x3A, (byte) 0xBA, 0x7A, (byte) 0xFA, + 0x06, (byte) 0x86, 0x46, (byte) 0xC6, 0x26, (byte) 0xA6, 0x66, (byte) 0xE6, 0x16, (byte) 0x96, 0x56, (byte) 0xD6, 0x36, (byte) 0xB6, 0x76, (byte) 0xF6, + 0x0E, (byte) 0x8E, 0x4E, (byte) 0xCE, 0x2E, (byte) 0xAE, 0x6E, (byte) 0xEE, 0x1E, (byte) 0x9E, 0x5E, (byte) 0xDE, 0x3E, (byte) 0xBE, 0x7E, (byte) 0xFE, + 0x01, (byte) 0x81, 0x41, (byte) 0xC1, 0x21, (byte) 0xA1, 0x61, (byte) 0xE1, 0x11, (byte) 0x91, 0x51, (byte) 0xD1, 0x31, (byte) 0xB1, 0x71, (byte) 0xF1, + 0x09, (byte) 0x89, 0x49, (byte) 0xC9, 0x29, (byte) 0xA9, 0x69, (byte) 0xE9, 0x19, (byte) 0x99, 0x59, (byte) 0xD9, 0x39, (byte) 0xB9, 0x79, (byte) 0xF9, + 0x05, (byte) 0x85, 0x45, (byte) 0xC5, 0x25, (byte) 0xA5, 0x65, (byte) 0xE5, 0x15, (byte) 0x95, 0x55, (byte) 0xD5, 0x35, (byte) 0xB5, 0x75, (byte) 0xF5, + 0x0D, (byte) 0x8D, 0x4D, (byte) 0xCD, 0x2D, (byte) 0xAD, 0x6D, (byte) 0xED, 0x1D, (byte) 0x9D, 0x5D, (byte) 0xDD, 0x3D, (byte) 0xBD, 0x7D, (byte) 0xFD, + 0x03, (byte) 0x83, 0x43, (byte) 0xC3, 0x23, (byte) 0xA3, 0x63, (byte) 0xE3, 0x13, (byte) 0x93, 0x53, (byte) 0xD3, 0x33, (byte) 0xB3, 0x73, (byte) 0xF3, + 0x0B, (byte) 0x8B, 0x4B, (byte) 0xCB, 0x2B, (byte) 0xAB, 0x6B, (byte) 0xEB, 0x1B, (byte) 0x9B, 0x5B, (byte) 0xDB, 0x3B, (byte) 0xBB, 0x7B, (byte) 0xFB, + 0x07, (byte) 0x87, 0x47, (byte) 0xC7, 0x27, (byte) 0xA7, 0x67, (byte) 0xE7, 0x17, (byte) 0x97, 0x57, (byte) 0xD7, 0x37, (byte) 0xB7, 0x77, (byte) 0xF7, + 0x0F, (byte) 0x8F, 0x4F, (byte) 0xCF, 0x2F, (byte) 0xAF, 0x6F, (byte) 0xEF, 0x1F, (byte) 0x9F, 0x5F, (byte) 0xDF, 0x3F, (byte) 0xBF, 0x7F, (byte) 0xFF + }; + + private XWDX11Header header; + + XWDImageReader(ImageReaderSpi provider) { + super(provider); + } + + @Override + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return new XWDImageMetadata(header); + } + + @Override + public int getWidth(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return header.width; + } + + @Override + public int getHeight(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return header.height; + } + + private void readHeader() throws IOException { + assertInput(); + + if (header == null) { + header = XWDX11Header.read(imageInput); + } + } + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + ImageTypeSpecifier rawImageType = getRawImageType(imageIndex); + +// ArrayList specs = new ArrayList<>(); + // TODO: Add TYPE_3BYTE_BGR & TYPE_4BYTE_ABGR + TYPE_INT_ types? +// if (rawImageType ) + + return Collections.singletonList(rawImageType).iterator(); + } + + @Override + public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + switch (header.visualClass) { + case X11.VISUAL_CLASS_STATIC_GRAY: + case X11.VISUAL_CLASS_GRAY_SCALE: + return ImageTypeSpecifiers.createGrayscale(header.bitsPerPixel, DataBuffer.TYPE_BYTE, false); + case X11.VISUAL_CLASS_STATIC_COLOR: + case X11.VISUAL_CLASS_PSEUDO_COLOR: + return ImageTypeSpecifiers.createFromIndexColorModel(header.colorMap); + case X11.VISUAL_CLASS_TRUE_COLOR: + case X11.VISUAL_CLASS_DIRECT_COLOR: + int numComponents = header.numComponents(); + return ImageTypeSpecifiers.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), createBandArray(numComponents, header), DataBuffer.TYPE_BYTE, numComponents > 3, false); + default: + throw new IIOException(String.format("Unknown visual class: %d", header.visualClass)); + } + } + + private Raster createRowRaster(byte[] row) { + DataBuffer databuffer = new DataBufferByte(row, row.length); + + switch (header.visualClass) { + case X11.VISUAL_CLASS_TRUE_COLOR: + case X11.VISUAL_CLASS_DIRECT_COLOR: + return Raster.createInterleavedRaster(databuffer, header.width, 1, header.bytesPerLine, header.bitsPerPixel / 8, createBandArray(header.numComponents(), header), null); + default: + return Raster.createPackedRaster(databuffer, header.width, 1, header.bitsPerPixel, null); + } + } + + @Override + public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { + checkBounds(imageIndex); + readHeader(); + + BufferedImage destination = getDestination(param, getImageTypes(imageIndex), header.width, header.height); + WritableRaster raster = destination.getRaster(); + checkReadParamBandSettings(param, header.numComponents(), raster.getNumBands()); + + Rectangle srcRegion = new Rectangle(); + Rectangle destRegion = new Rectangle(); + computeRegions(param, header.width, header.height, destination, srcRegion, destRegion); + + byte[] row = new byte[header.bytesPerLine]; + Raster rowRaster = createRowRaster(row).createChild(srcRegion.x, 0, destRegion.width, 1, 0, 0, null); + + final boolean reverseBits = header.bitsPerPixel < 8 && header.bitFillOrder == ByteOrder.LITTLE_ENDIAN; + final boolean clearAlpha = destination.getColorModel().hasAlpha(); + final int xSub = param == null ? 1 : param.getSourceXSubsampling(); + final int ySub = param == null ? 1 : param.getSourceYSubsampling(); + + imageInput.seek(header.pixelOffset); + + processImageStarted(imageIndex); + + for (int y = 0; y < srcRegion.y + srcRegion.height; y++) { + if (y < srcRegion.y || y % ySub != 0) { + // Skip row + imageInput.skipBytes(row.length); + continue; + } + + imageInput.readFully(row); + + if (reverseBits) { + for (int i = 0; i < row.length; i++) { + // TODO: Why do we have to inverse (XOR) as well? + row[i] = (byte) ~BIT_REVERSE_TABLE[row[i] & 0xff]; + } + } + + if (clearAlpha) { + // If we have alpha, inverse the alpha sample (or set it to opaque?) + // TODO: Where's the alpha! First or last depending on byte order? + for (int i = 0; i < row.length; i += rowRaster.getNumBands()) { + row[i] = (byte) ~row[i]; + } + } + + if (xSub != 1) { + // Horizontal subsampling + int samplesPerPixel = header.numComponents(); + subsampleRow(row, srcRegion.x * samplesPerPixel, srcRegion.width, row, srcRegion.x * samplesPerPixel, samplesPerPixel, header.bitsPerRGB, xSub); + } + + raster.setDataElements(0, (y - srcRegion.y) / ySub, rowRaster); + + if (abortRequested()) { + processReadAborted(); + break; + } + + processImageProgress((y - srcRegion.y) * 100f / srcRegion.height); + } + + processImageComplete(); + + return destination; + } + + private int bytePos(int bitMask, int numBands) { + switch (bitMask) { + case 0xff: + return numBands - 1; + case 0xff00: + return numBands - 2; + case 0xff0000: + return numBands - 3; + case 0xff000000: + return numBands - 4; + default: + // We only support full byte masks for now + throw new IllegalArgumentException(String.format("Unsupported bitmask: 0x%08x", bitMask)); + } + } + + private int[] createBandArray(int numBands, final XWDX11Header header) { + int[] offsets = new int[numBands]; + + for (int i = 0; i < numBands; i++) { + offsets[i] = bytePos(header.masks[i], numBands); + } + + return offsets; + } + + @Override + protected void resetMembers() { + header = null; + } +} diff --git a/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageReaderSpi.java b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageReaderSpi.java new file mode 100644 index 00000000..9fc7e59e --- /dev/null +++ b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageReaderSpi.java @@ -0,0 +1,40 @@ +package com.twelvemonkeys.imageio.plugins.xwd; + +import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; + +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.util.Locale; + +public final class XWDImageReaderSpi extends ImageReaderSpiBase { + public XWDImageReaderSpi() { + super(new XWDProviderInfo()); + } + + @Override + public boolean canDecodeInput(Object source) throws IOException { + if (!(source instanceof ImageInputStream)) { + return false; + } + + ImageInputStream stream = (ImageInputStream) source; + + stream.mark(); + + try { + return XWDX11Header.isX11(stream); + } finally { + stream.reset(); + } + } + + @Override + public XWDImageReader createReaderInstance(Object extension) { + return new XWDImageReader(this); + } + + @Override + public String getDescription(Locale locale) { + return "X11 Window Dump Format (XWD) reader"; + } +} diff --git a/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDProviderInfo.java b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDProviderInfo.java new file mode 100644 index 00000000..58bbf132 --- /dev/null +++ b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDProviderInfo.java @@ -0,0 +1,28 @@ +package com.twelvemonkeys.imageio.plugins.xwd; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; + +final class XWDProviderInfo extends ReaderWriterProviderInfo { + + final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.xwd.debug")); + + protected XWDProviderInfo() { + super( + XWDProviderInfo.class, + new String[] { + "XWD", "xwd" + }, + new String[] {"xwd",}, + new String[] { + // No official IANA record exists + "image/xwd", "image/x-xwd" + }, + "com.twelvemonkeys.imageio.plugins.xwd.XWDImageReader", + new String[]{"com.twelvemonkeys.imageio.plugins.xwd.XWDImageReaderSpi"}, + null, + null, + false, null, null, null, null, + true, null, null, null, null + ); + } +} diff --git a/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDX11Header.java b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDX11Header.java new file mode 100644 index 00000000..4dd844df --- /dev/null +++ b/imageio/imageio-xwd/src/main/java/com/twelvemonkeys/imageio/plugins/xwd/XWDX11Header.java @@ -0,0 +1,164 @@ +package com.twelvemonkeys.imageio.plugins.xwd; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.DataBuffer; +import java.awt.image.IndexColorModel; +import java.io.DataInput; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +final class XWDX11Header { + + final int width; + final int height; + final ByteOrder byteOrder; + final ByteOrder bitFillOrder; + final int bitsPerPixel; + final int bytesPerLine; + + final int visualClass; + final int[] masks; + final int bitsPerRGB; + + final IndexColorModel colorMap; + final String windowName; + + final long pixelOffset; + + private XWDX11Header(int width, int height, ByteOrder byteOrder, ByteOrder bitFillOrder, + int bitsPerPixel, int bytesPerLine, int visualClass, + int readMask, int greenMask, int blueMask, int bitsPerRGB, + final IndexColorModel colorMap, final String windowName, long pixelOffset) { + this.width = width; + this.height = height; + this.byteOrder = byteOrder; + this.bitFillOrder = bitFillOrder; + this.bitsPerPixel = bitsPerPixel; + this.bytesPerLine = bytesPerLine; + this.visualClass = visualClass; + this.masks = new int[] {readMask, greenMask, blueMask, ~(readMask | greenMask | blueMask)}; + this.bitsPerRGB = bitsPerRGB; + this.colorMap = colorMap; + this.windowName = windowName; + this.pixelOffset = pixelOffset; + } + + static boolean isX11(final DataInput input) throws IOException { + return input.readInt() >= X11.X11_HEADER_SIZE + && input.readInt() == X11.X11_HEADER_VERSION; + } + + static XWDX11Header read(final ImageInputStream input) throws IOException { + input.mark(); + if (!isX11(input)) { + throw new IIOException("Not a valid X11 Window Dump"); + } + input.reset(); + + long pos = input.getStreamPosition(); + int length = input.readInt(); + input.readInt(); + + int format = input.readInt(); // Pixel Format: 0 = 1 bit XYBitmap, 1 = single plane XYPixmap (gray?), 2 = two or more planes ZPixmap + int depth = input.readInt(); // Pixel Bit Depth: 1-32 (never seen above 24...) + + int width = input.readInt(); + int height = input.readInt(); + int xOffset = input.readInt(); // Rarely used... + + int byteOrder = input.readInt(); + int unit = input.readInt(); + int bitOrder = input.readInt(); // Same as TIFF FillOrder... + int pad = input.readInt(); // Can be inferred from bytePerLine? + int bitsPerPixel = input.readInt(); + int bytePerLine = input.readInt(); // Scan line + int visualClass = input.readInt(); + + // TODO: Probably need these masks... Use them for creating color model? + int redMask = input.readInt(); + int greenMask = input.readInt(); + int blueMask = input.readInt(); + + // Size of each color mask in bits", basically bitPerSample + // NOTE: This field can't be trusted... :-/ + int bitsPerRGB = input.readInt(); + if (bitsPerPixel == 24 && bitsPerRGB == 24) { + bitsPerRGB = 8; + } + + // Hmmm.. Unclear which of these that matters... + // - Can the map be larger than colors used? + // - Can numColors be > 0 for non-colormapped types? + int numColors = input.readInt(); + int colorMapEntries = input.readInt(); + + // Not useful? Metadata? + int windowWidth = input.readInt(); + int windowHeight = input.readInt(); + int windowX = input.readInt(); + int windowY = input.readInt(); + int windowBorderWidth = input.readInt(); + + byte[] windowNameData = new byte[length - X11.X11_HEADER_SIZE]; + input.readFully(windowNameData); + String windowName = windowNameData.length <= 1 ? null : new String(windowNameData, 0, windowNameData.length - 1, StandardCharsets.UTF_8); + + if (XWDProviderInfo.DEBUG) { + System.out.println("format = " + format); + System.out.println("depth = " + depth); + System.out.println("byteOrder = " + byteOrder); + System.out.println("unit = " + unit); + System.out.println("bitOrder = " + bitOrder); + System.out.println("pad = " + pad); + System.out.println("bitsPerPixel = " + bitsPerPixel); + System.out.println("bytePerLine = " + bytePerLine); + System.out.println("visualClass = " + visualClass); + + System.out.printf("redMask = 0x%08x%n", redMask); + System.out.printf("greenMask = 0x%08x%n", greenMask); + System.out.printf("blueMask = 0x%08x%n", blueMask); + + System.out.println("bitsPerRGB = " + bitsPerRGB); + + System.out.println("numColors = " + numColors); + System.out.println("colorMapEntries = " + colorMapEntries); + System.out.println("windowName = " + windowName); + } + + byte[] colorMap = new byte[12 * colorMapEntries]; + input.readFully(colorMap); + + return new XWDX11Header(width, height, + byteOrder == 0 ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN, + bitOrder == 0 ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN, + bitsPerPixel, bytePerLine, visualClass, redMask, greenMask, blueMask, bitsPerRGB, + createColorMap(bitsPerRGB, colorMap), + windowName, pos + length + colorMap.length); + } + + private static IndexColorModel createColorMap(int bitDepth, byte[] colorMap) { + if (colorMap.length == 0) { + return null; + } + + int size = colorMap.length / 12; + int[] rgb = new int[size]; + + for (int i = 0; i < size; i++) { +// int index = (colorMap[i * 12] & 0xff) << 24 | (colorMap[i * 12 + 1] & 0xff) << 16 | (colorMap[i * 12 + 2] & 0xff) << 8 | (colorMap[i * 12 + 3] & 0xff); +// System.out.println("index = " + index); + + // We'll just use the high 8 bits, as Java don't really have good support for 16 bit index color model + rgb[i] = (colorMap[i * 12 + 4] & 0xff) << 16 | (colorMap[i * 12 + 6] & 0xff) << 8 | (colorMap[i * 12 + 8] & 0xff); + } + + return new IndexColorModel(bitDepth, size, rgb, 0, false, -1, DataBuffer.TYPE_BYTE); + } + + + int numComponents() { + return bitsPerPixel / bitsPerRGB; + } +} diff --git a/imageio/imageio-xwd/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-xwd/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi new file mode 100755 index 00000000..d9d939e7 --- /dev/null +++ b/imageio/imageio-xwd/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -0,0 +1 @@ +com.twelvemonkeys.imageio.plugins.xwd.XWDImageReaderSpi diff --git a/imageio/imageio-xwd/src/test/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageReaderTest.java b/imageio/imageio-xwd/src/test/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageReaderTest.java new file mode 100644 index 00000000..0168ef9f --- /dev/null +++ b/imageio/imageio-xwd/src/test/java/com/twelvemonkeys/imageio/plugins/xwd/XWDImageReaderTest.java @@ -0,0 +1,53 @@ +package com.twelvemonkeys.imageio.plugins.xwd; + +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; + +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class XWDImageReaderTest extends ImageReaderAbstractTest { + + private final XWDImageReaderSpi provider = new XWDImageReaderSpi(); + + @Override + protected List getTestData() { + return Arrays.asList( + new TestData(getClassLoaderResource("/xwd/input.xwd"), new Dimension(70, 46)), + new TestData(getClassLoaderResource("/xwd/brain.xwd"), new Dimension(520, 510)), + new TestData(getClassLoaderResource("/xwd/sample_640x426.xwd"), new Dimension(640, 426)) + ); + } + + @Override + protected ImageReaderSpi createProvider() { + return provider; + } + + @Override + protected Class getReaderClass() { + return XWDImageReader.class; + } + + @Override + protected XWDImageReader createReader() { + return provider.createReaderInstance(null); + } + + @Override + protected List getFormatNames() { + return Arrays.asList("xwd", "XWD"); + } + + @Override + protected List getSuffixes() { + return Collections.singletonList("xwd"); + } + + @Override + protected List getMIMETypes() { + return Arrays.asList("image/xwd", "image/x-xwd"); + } +} \ No newline at end of file diff --git a/imageio/imageio-xwd/src/test/resources/xwd/brain.xwd b/imageio/imageio-xwd/src/test/resources/xwd/brain.xwd new file mode 100644 index 00000000..a082a8bb Binary files /dev/null and b/imageio/imageio-xwd/src/test/resources/xwd/brain.xwd differ diff --git a/imageio/imageio-xwd/src/test/resources/xwd/input.xwd b/imageio/imageio-xwd/src/test/resources/xwd/input.xwd new file mode 100644 index 00000000..13a67c23 Binary files /dev/null and b/imageio/imageio-xwd/src/test/resources/xwd/input.xwd differ diff --git a/imageio/imageio-xwd/src/test/resources/xwd/sample_640x426.xwd b/imageio/imageio-xwd/src/test/resources/xwd/sample_640x426.xwd new file mode 100644 index 00000000..da24e768 Binary files /dev/null and b/imageio/imageio-xwd/src/test/resources/xwd/sample_640x426.xwd differ diff --git a/imageio/pom.xml b/imageio/pom.xml index 5fd8bd6d..42254be5 100644 --- a/imageio/pom.xml +++ b/imageio/pom.xml @@ -44,6 +44,7 @@ imageio-tga imageio-thumbsdb imageio-tiff + imageio-xwd imageio-batik