Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions openpdf-core/src/main/java/org/openpdf/text/Image.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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).
* <p>
* This method packs palette indices from a WritableRaster into a byte array that strictly adheres to
* PDF specification requirements for indexed color image storage:
* <ul>
* <li>Pixel indices are packed starting from the Most Significant Bit (MSB, bit 7) of each byte (PDF mandatory rule)</li>
* <li>Each row of pixel data is byte-aligned (padded with zeros to match calculated row stride)</li>
* <li>Supports standard indexed color bit depths: 1, 2, 4, 8 bits per pixel</li>
* <li>Normalizes palette indices to unsigned 0-255 range to prevent invalid negative values</li>
* </ul>
*
* @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.
*
Expand Down
58 changes: 58 additions & 0 deletions openpdf-core/src/test/java/org/openpdf/text/ImageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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();
}

}
Loading