Skip to content
Open
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
130 changes: 77 additions & 53 deletions openpdf-core/src/main/java/org/openpdf/text/Image.java
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ public abstract class Image extends Rectangle {

// member variables
public static final int[] PNGID = {137, 80, 78, 71, 13, 10, 26, 10};
private static final int INDEXED_NO_TRANSPARENCY = -1;
private static final int INDEXED_UNSUPPORTED_TRANSPARENCY = -2;
/**
* a static that is used for attributing a unique id to each image.
*/
Expand Down Expand Up @@ -828,61 +830,68 @@ public static Image getInstance(java.awt.Image image, java.awt.Color color,
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;
}
// Handle indexed color images when transparency can be represented directly.
// More complex alpha (for example semi-transparency) must fallback to the generic RGB+SMask path below
if (bi.getColorModel() instanceof IndexColorModel icm && !forceBW && color == null) {
int transparentIndex = getIndexedTransparentPaletteIndex(icm);
if (transparentIndex != INDEXED_UNSUPPORTED_TRANSPARENCY) {
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 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);
if (transparentIndex >= 0) {
img.setTransparency(new int[]{transparentIndex, transparentIndex});
}

// 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;
return img;
}
// Unsupported indexed transparency falls through to the generic RGB+SMask path below.
}
}

Expand Down Expand Up @@ -1099,6 +1108,21 @@ private static byte[] generateIndexedColorPixelData(int width, int bitsPerPixel,
return pixelData;
}

private static int getIndexedTransparentPaletteIndex(IndexColorModel colorModel) {
int transparentIndex = INDEXED_NO_TRANSPARENCY;
for (int index = 0; index < colorModel.getMapSize(); index++) {
int alpha = colorModel.getAlpha(index);
if (alpha == 0xFF) {
continue;
}
if (alpha != 0x00 || transparentIndex != INDEXED_NO_TRANSPARENCY) {
return INDEXED_UNSUPPORTED_TRANSPARENCY;
}
transparentIndex = index;
}
return transparentIndex;
}

/**
* Packs a single pixel index into the target byte array based on specified bit depth (PDF MSB-first rule).
*/
Expand Down
35 changes: 35 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 @@ -177,4 +177,39 @@ void shouldDetectIndexedColorFromBufferedImage() throws Exception {
assertThat(image.getAdditional().get(PdfName.COLORSPACE)).isNotNull();
}

@Test
void shouldFallbackToRgbWhenIndexedColorHasTransparency() throws Exception {
int width = 10;
int height = 10;

byte[] reds = {(byte) 255, 0, 0, 0};
byte[] greens = {0, (byte) 255, 0, 0};
byte[] blues = {0, 0, (byte) 255, 0};
byte[] alphas = {0, (byte) 128, (byte) 255, (byte) 255};

IndexColorModel colorModel = new IndexColorModel(
2,
4,
reds, greens, blues, alphas
);

BufferedImage bufferedImage = new BufferedImage(
width, height, BufferedImage.TYPE_BYTE_INDEXED, colorModel
);

for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
bufferedImage.getRaster().setSample(x, y, 0, (x + y) % 4);
}
}

Image image = Image.getInstance(bufferedImage, null);

assertNotNull(image);
assertThat(image.getColorspace()).isEqualTo(3);
assertThat(image.getImageMask()).isNotNull();
assertThat(image.getImageMask().isMask()).isTrue();
assertThat(image.isSmask()).isTrue();
}

}
Loading