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:
+ *
+ * - Pixel indices are packed starting from the Most Significant Bit (MSB, bit 7) of each byte (PDF mandatory rule)
+ * - Each row of pixel data is byte-aligned (padded with zeros to match calculated row stride)
+ * - Supports standard indexed color bit depths: 1, 2, 4, 8 bits per pixel
+ * - Normalizes palette indices to unsigned 0-255 range to prevent invalid negative values
+ *
+ *
+ * @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();
+ }
+
}