diff --git a/openpdf-core/src/main/java/org/openpdf/text/Image.java b/openpdf-core/src/main/java/org/openpdf/text/Image.java index f574ed630..b151e7622 100644 --- a/openpdf-core/src/main/java/org/openpdf/text/Image.java +++ b/openpdf-core/src/main/java/org/openpdf/text/Image.java @@ -61,12 +61,15 @@ import org.openpdf.text.pdf.PdfObject; import org.openpdf.text.pdf.PdfReader; import org.openpdf.text.pdf.PdfStream; +import org.openpdf.text.pdf.PdfString; import org.openpdf.text.pdf.PdfTemplate; import org.openpdf.text.pdf.PdfWriter; import org.openpdf.text.pdf.codec.CCITTG4Encoder; import java.awt.Graphics2D; import java.awt.color.ICC_Profile; import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.awt.image.WritableRaster; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; @@ -824,6 +827,63 @@ public static Image getInstance(java.awt.Image image, java.awt.Color color, if (bi.getType() == BufferedImage.TYPE_BYTE_BINARY && bi.getColorModel().getNumColorComponents() <= 2) { forceBW = true; } + + // Handle indexed color images + if (bi.getColorModel() instanceof IndexColorModel && !forceBW) { + IndexColorModel icm = (IndexColorModel) bi.getColorModel(); + int mapSize = icm.getMapSize(); + int bitsPerPixel = icm.getPixelSize(); + + // Ensure bits per pixel is valid (1, 2, 4, or 8) + // For PDF indexed images, bpc should be the bits needed to index the palette + if (bitsPerPixel > 8 || bitsPerPixel == 0) { + bitsPerPixel = 8; + } else if (bitsPerPixel > 4) { + bitsPerPixel = 8; + } else if (bitsPerPixel > 2) { + bitsPerPixel = 4; + } else if (bitsPerPixel > 1) { + bitsPerPixel = 2; + } else { + bitsPerPixel = 1; + } + + // Extract palette data + byte[] reds = new byte[mapSize]; + byte[] greens = new byte[mapSize]; + byte[] blues = new byte[mapSize]; + icm.getReds(reds); + icm.getGreens(greens); + icm.getBlues(blues); + + // Build palette as RGB byte array + byte[] palette = new byte[mapSize * 3]; + for (int i = 0; i < mapSize; i++) { + palette[i * 3] = reds[i]; + palette[i * 3 + 1] = greens[i]; + palette[i * 3 + 2] = blues[i]; + } + + // Extract pixel indices + int width = bi.getWidth(); + int height = bi.getHeight(); + byte[] pixelData = generateIndexedColorPixelData(width, bitsPerPixel, height, bi.getRaster()); + // Create indexed image with palette + Image img = Image.getInstance(width, height, 1, bitsPerPixel, pixelData); + + // Set up indexed colorspace: [/Indexed /DeviceRGB maxIndex palette] + PdfArray indexed = new PdfArray(); + indexed.add(PdfName.INDEXED); + indexed.add(PdfName.DEVICERGB); + indexed.add(new PdfNumber(mapSize - 1)); + indexed.add(new PdfString(palette)); + + PdfDictionary additional = new PdfDictionary(); + additional.put(PdfName.COLORSPACE, indexed); + img.setAdditional(additional); + + return img; + } } java.awt.image.PixelGrabber pg = new java.awt.image.PixelGrabber(image, @@ -987,6 +1047,88 @@ public static Image getInstance(java.awt.Image image, java.awt.Color color, } } + /** + * Generates PDF-compliant pixel data for indexed color images (IndexColorModel). + *

+ * This method packs palette indices from a WritableRaster into a byte array that strictly adheres to + * PDF specification requirements for indexed color image storage: + *

+ * + * @param width Width of the indexed color image (in pixels) + * @param bitsPerPixel Number of bits per pixel (must be 1, 2, 4, or 8 for valid indexed color) + * @param height Height of the indexed color image (in pixels) + * @param raster WritableRaster containing the indexed color pixel indices (from IndexColorModel BufferedImage) + * @return Byte array of pixel data packed according to PDF indexed color specifications, with row-wise byte alignment + * @see WritableRaster + * @see IndexColorModel + */ + private static byte[] generateIndexedColorPixelData(int width, int bitsPerPixel, int height, WritableRaster raster) { + int rowStride = (width * bitsPerPixel + 7) / 8; + byte[] pixelData = new byte[rowStride * height]; + + int bytePos = 0; + int bitOffset; + + for (int y = 0; y < height; y++) { + bitOffset = 7; + for (int x = 0; x < width; x++) { + int pixelIndex = raster.getSample(x, y, 0); + if (pixelIndex < 0) { + pixelIndex = 0; + } + pixelIndex = pixelIndex & 0xFF; + + bitOffset = packPixelByBitDepth(bitsPerPixel, pixelIndex, pixelData, bytePos, bitOffset); + + if (bitOffset < 0) { + bytePos++; + bitOffset = 7; + } + } + int usedBytesInRow = bytePos - (y * rowStride); + if (usedBytesInRow < rowStride) { + int padBytes = rowStride - usedBytesInRow; + bytePos += padBytes; + } + } + return pixelData; + } + + /** + * Packs a single pixel index into the target byte array based on specified bit depth (PDF MSB-first rule). + */ + private static int packPixelByBitDepth(int bitsPerPixel, int pixelIndex, byte[] pixelData, int bytePos, int bitOffset) { + int currentBitOffset = bitOffset; + + switch (bitsPerPixel) { + case 1: + if ((pixelIndex & 0x01) == 1) { + pixelData[bytePos] |= (byte) (1 << currentBitOffset); + } + currentBitOffset--; + break; + case 2: + pixelData[bytePos] |= (byte) ((pixelIndex & 0x03) << (currentBitOffset - 1)); + currentBitOffset -= 2; + break; + case 4: + pixelData[bytePos] |= (byte) ((pixelIndex & 0x0F) << (currentBitOffset - 3)); + currentBitOffset -= 4; + break; + case 8: + default: + pixelData[bytePos] = (byte) pixelIndex; + currentBitOffset = -1; + break; + } + return currentBitOffset; + } + /** * Gets an instance of an Image from a java.awt.Image. * diff --git a/openpdf-core/src/test/java/org/openpdf/text/ImageTest.java b/openpdf-core/src/test/java/org/openpdf/text/ImageTest.java index 0f8017641..1d9764894 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/ImageTest.java +++ b/openpdf-core/src/test/java/org/openpdf/text/ImageTest.java @@ -4,9 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; import java.io.IOException; import java.io.InputStream; import org.junit.jupiter.api.Test; +import org.openpdf.text.pdf.PdfName; class ImageTest { @@ -119,4 +122,59 @@ private byte[] readFileBytes() throws IOException { return bytes; } + @Test + void shouldDetectIndexedColorGif() throws Exception { + // Load H.gif which is an indexed color GIF + String fileName = "src/test/resources/H.gif"; + Image image = Image.getInstance(fileName); + + assertNotNull(image); + // colorspace should be 1 for indexed images (not 3 for RGB) + assertThat(image.getColorspace()).isEqualTo(1); + + // Verify that additional colorspace info is set for indexed images + assertThat(image.getAdditional()).isNotNull(); + assertThat(image.getAdditional().get(PdfName.COLORSPACE)).isNotNull(); + } + + @Test + void shouldDetectIndexedColorFromBufferedImage() throws Exception { + // Create an indexed color BufferedImage programmatically + int width = 10; + int height = 10; + + // Create a simple 4-color palette (red, green, blue, black) + byte[] reds = {(byte) 255, 0, 0, 0}; + byte[] greens = {0, (byte) 255, 0, 0}; + byte[] blues = {0, 0, (byte) 255, 0}; + + IndexColorModel colorModel = new IndexColorModel( + 2, // 2 bits per pixel (4 colors) + 4, // 4 colors in palette + reds, greens, blues + ); + + BufferedImage bufferedImage = new BufferedImage( + width, height, BufferedImage.TYPE_BYTE_INDEXED, colorModel + ); + + // Fill with some pattern + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + bufferedImage.getRaster().setSample(x, y, 0, (x + y) % 4); + } + } + + // Convert to Image + Image image = Image.getInstance(bufferedImage, null); + + assertNotNull(image); + // Should be indexed (colorspace = 1), not RGB (colorspace = 3) + assertThat(image.getColorspace()).isEqualTo(1); + + // Verify that additional colorspace info is set + assertThat(image.getAdditional()).isNotNull(); + assertThat(image.getAdditional().get(PdfName.COLORSPACE)).isNotNull(); + } + }