Skip to content

Commit 297a81b

Browse files
author
obo
committed
fix: preserve indexed PNG transparency to prevent black box regression when stamping PDF (#1524)
1 parent efb0457 commit 297a81b

2 files changed

Lines changed: 112 additions & 53 deletions

File tree

openpdf-core/src/main/java/org/openpdf/text/Image.java

Lines changed: 77 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,8 @@ public abstract class Image extends Rectangle {
260260

261261
// member variables
262262
public static final int[] PNGID = {137, 80, 78, 71, 13, 10, 26, 10};
263+
private static final int INDEXED_NO_TRANSPARENCY = -1;
264+
private static final int INDEXED_UNSUPPORTED_TRANSPARENCY = -2;
263265
/**
264266
* a static that is used for attributing a unique id to each image.
265267
*/
@@ -828,61 +830,68 @@ public static Image getInstance(java.awt.Image image, java.awt.Color color,
828830
forceBW = true;
829831
}
830832

831-
// Handle indexed color images
832-
if (bi.getColorModel() instanceof IndexColorModel && !forceBW) {
833-
IndexColorModel icm = (IndexColorModel) bi.getColorModel();
834-
int mapSize = icm.getMapSize();
835-
int bitsPerPixel = icm.getPixelSize();
836-
837-
// Ensure bits per pixel is valid (1, 2, 4, or 8)
838-
// For PDF indexed images, bpc should be the bits needed to index the palette
839-
if (bitsPerPixel > 8 || bitsPerPixel == 0) {
840-
bitsPerPixel = 8;
841-
} else if (bitsPerPixel > 4) {
842-
bitsPerPixel = 8;
843-
} else if (bitsPerPixel > 2) {
844-
bitsPerPixel = 4;
845-
} else if (bitsPerPixel > 1) {
846-
bitsPerPixel = 2;
847-
} else {
848-
bitsPerPixel = 1;
849-
}
833+
// Handle indexed color images when transparency can be represented directly.
834+
// More complex alpha (for example semi-transparency) must fallback to the generic RGB+SMask path below
835+
if (bi.getColorModel() instanceof IndexColorModel icm && !forceBW && color == null) {
836+
int transparentIndex = getIndexedTransparentPaletteIndex(icm);
837+
if (transparentIndex != INDEXED_UNSUPPORTED_TRANSPARENCY) {
838+
int mapSize = icm.getMapSize();
839+
int bitsPerPixel = icm.getPixelSize();
840+
841+
// Ensure bits per pixel is valid (1, 2, 4, or 8)
842+
// For PDF indexed images, bpc should be the bits needed to index the palette
843+
if (bitsPerPixel > 8 || bitsPerPixel == 0) {
844+
bitsPerPixel = 8;
845+
} else if (bitsPerPixel > 4) {
846+
bitsPerPixel = 8;
847+
} else if (bitsPerPixel > 2) {
848+
bitsPerPixel = 4;
849+
} else if (bitsPerPixel > 1) {
850+
bitsPerPixel = 2;
851+
} else {
852+
bitsPerPixel = 1;
853+
}
850854

851-
// Extract palette data
852-
byte[] reds = new byte[mapSize];
853-
byte[] greens = new byte[mapSize];
854-
byte[] blues = new byte[mapSize];
855-
icm.getReds(reds);
856-
icm.getGreens(greens);
857-
icm.getBlues(blues);
858-
859-
// Build palette as RGB byte array
860-
byte[] palette = new byte[mapSize * 3];
861-
for (int i = 0; i < mapSize; i++) {
862-
palette[i * 3] = reds[i];
863-
palette[i * 3 + 1] = greens[i];
864-
palette[i * 3 + 2] = blues[i];
865-
}
855+
// Extract palette data
856+
byte[] reds = new byte[mapSize];
857+
byte[] greens = new byte[mapSize];
858+
byte[] blues = new byte[mapSize];
859+
icm.getReds(reds);
860+
icm.getGreens(greens);
861+
icm.getBlues(blues);
862+
863+
// Build palette as RGB byte array
864+
byte[] palette = new byte[mapSize * 3];
865+
for (int i = 0; i < mapSize; i++) {
866+
palette[i * 3] = reds[i];
867+
palette[i * 3 + 1] = greens[i];
868+
palette[i * 3 + 2] = blues[i];
869+
}
870+
871+
// Extract pixel indices
872+
int width = bi.getWidth();
873+
int height = bi.getHeight();
874+
byte[] pixelData = generateIndexedColorPixelData(width, bitsPerPixel, height, bi.getRaster());
875+
// Create indexed image with palette
876+
Image img = Image.getInstance(width, height, 1, bitsPerPixel, pixelData);
877+
878+
// Set up indexed colorspace: [/Indexed /DeviceRGB maxIndex palette]
879+
PdfArray indexed = new PdfArray();
880+
indexed.add(PdfName.INDEXED);
881+
indexed.add(PdfName.DEVICERGB);
882+
indexed.add(new PdfNumber(mapSize - 1));
883+
indexed.add(new PdfString(palette));
884+
885+
PdfDictionary additional = new PdfDictionary();
886+
additional.put(PdfName.COLORSPACE, indexed);
887+
img.setAdditional(additional);
888+
if (transparentIndex >= 0) {
889+
img.setTransparency(new int[]{transparentIndex, transparentIndex});
890+
}
866891

867-
// Extract pixel indices
868-
int width = bi.getWidth();
869-
int height = bi.getHeight();
870-
byte[] pixelData = generateIndexedColorPixelData(width, bitsPerPixel, height, bi.getRaster());
871-
// Create indexed image with palette
872-
Image img = Image.getInstance(width, height, 1, bitsPerPixel, pixelData);
873-
874-
// Set up indexed colorspace: [/Indexed /DeviceRGB maxIndex palette]
875-
PdfArray indexed = new PdfArray();
876-
indexed.add(PdfName.INDEXED);
877-
indexed.add(PdfName.DEVICERGB);
878-
indexed.add(new PdfNumber(mapSize - 1));
879-
indexed.add(new PdfString(palette));
880-
881-
PdfDictionary additional = new PdfDictionary();
882-
additional.put(PdfName.COLORSPACE, indexed);
883-
img.setAdditional(additional);
884-
885-
return img;
892+
return img;
893+
}
894+
// Unsupported indexed transparency falls through to the generic RGB+SMask path below.
886895
}
887896
}
888897

@@ -1099,6 +1108,21 @@ private static byte[] generateIndexedColorPixelData(int width, int bitsPerPixel,
10991108
return pixelData;
11001109
}
11011110

1111+
private static int getIndexedTransparentPaletteIndex(IndexColorModel colorModel) {
1112+
int transparentIndex = INDEXED_NO_TRANSPARENCY;
1113+
for (int index = 0; index < colorModel.getMapSize(); index++) {
1114+
int alpha = colorModel.getAlpha(index);
1115+
if (alpha == 0xFF) {
1116+
continue;
1117+
}
1118+
if (alpha != 0x00 || transparentIndex != INDEXED_NO_TRANSPARENCY) {
1119+
return INDEXED_UNSUPPORTED_TRANSPARENCY;
1120+
}
1121+
transparentIndex = index;
1122+
}
1123+
return transparentIndex;
1124+
}
1125+
11021126
/**
11031127
* Packs a single pixel index into the target byte array based on specified bit depth (PDF MSB-first rule).
11041128
*/

openpdf-core/src/test/java/org/openpdf/text/ImageTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,39 @@ void shouldDetectIndexedColorFromBufferedImage() throws Exception {
177177
assertThat(image.getAdditional().get(PdfName.COLORSPACE)).isNotNull();
178178
}
179179

180+
@Test
181+
void shouldFallbackToRgbWhenIndexedColorHasTransparency() throws Exception {
182+
int width = 10;
183+
int height = 10;
184+
185+
byte[] reds = {(byte) 255, 0, 0, 0};
186+
byte[] greens = {0, (byte) 255, 0, 0};
187+
byte[] blues = {0, 0, (byte) 255, 0};
188+
byte[] alphas = {0, (byte) 128, (byte) 255, (byte) 255};
189+
190+
IndexColorModel colorModel = new IndexColorModel(
191+
2,
192+
4,
193+
reds, greens, blues, alphas
194+
);
195+
196+
BufferedImage bufferedImage = new BufferedImage(
197+
width, height, BufferedImage.TYPE_BYTE_INDEXED, colorModel
198+
);
199+
200+
for (int y = 0; y < height; y++) {
201+
for (int x = 0; x < width; x++) {
202+
bufferedImage.getRaster().setSample(x, y, 0, (x + y) % 4);
203+
}
204+
}
205+
206+
Image image = Image.getInstance(bufferedImage, null);
207+
208+
assertNotNull(image);
209+
assertThat(image.getColorspace()).isEqualTo(3);
210+
assertThat(image.getImageMask()).isNotNull();
211+
assertThat(image.getImageMask().isMask()).isTrue();
212+
assertThat(image.isSmask()).isTrue();
213+
}
214+
180215
}

0 commit comments

Comments
 (0)