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 65816834a..5e676f639 100644 --- a/openpdf-core/src/main/java/org/openpdf/text/Image.java +++ b/openpdf-core/src/main/java/org/openpdf/text/Image.java @@ -872,8 +872,10 @@ public static Image getInstance(java.awt.Image image, java.awt.Color color, 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); + // Create the image directly as raw data. Image.getInstance(width, height, 1, 1, data) must + // not be used here: it CCITT-G4 encodes 1-bit data as black/white, which discards the + // palette and inverts images whose palette is {black, white} (see issue #1534). + Image img = new ImgRaw(width, height, 1, bitsPerPixel, pixelData); // Set up indexed colorspace: [/Indexed /DeviceRGB maxIndex palette] PdfArray indexed = new PdfArray(); 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 f711fa997..adeffd010 100644 --- a/openpdf-core/src/test/java/org/openpdf/text/ImageTest.java +++ b/openpdf-core/src/test/java/org/openpdf/text/ImageTest.java @@ -4,12 +4,18 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import java.awt.Color; +import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.awt.image.IndexColorModel; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import javax.imageio.ImageIO; import org.junit.jupiter.api.Test; +import org.openpdf.text.pdf.PdfArray; import org.openpdf.text.pdf.PdfName; +import org.openpdf.text.pdf.PdfString; class ImageTest { @@ -212,4 +218,74 @@ void shouldFallbackToRgbWhenIndexedColorHasTransparency() throws Exception { assertThat(image.isSmask()).isTrue(); } + @Test + void shouldPreservePaletteFor1BitIndexedImage() throws Exception { + // Palette with index 0 = white, index 1 = black, as produced when reading a + // bilevel TIFF with PhotometricInterpretation "WhiteIsZero" (issue #1534) + byte[] whiteBlack = {(byte) 255, 0}; + IndexColorModel colorModel = new IndexColorModel(1, 2, whiteBlack, whiteBlack, whiteBlack); + BufferedImage bufferedImage = new BufferedImage(16, 16, BufferedImage.TYPE_BYTE_BINARY, colorModel); + Graphics2D g = bufferedImage.createGraphics(); + g.setColor(Color.WHITE); + g.fillRect(0, 0, 16, 16); + g.setColor(Color.BLACK); + g.fillRect(0, 0, 8, 16); + g.dispose(); + + Image image = Image.getInstance(bufferedImage, null); + + // Must stay a raw 1-bit indexed image; CCITT encoding would drop the palette and invert the colors + assertThat(image.getBpc()).isEqualTo(1); + assertThat(image.getColorspace()).isEqualTo(1); + assertImageMatches(bufferedImage, image); + } + + @Test + void shouldRenderBlackAndWhitePngWithCorrectColors() throws Exception { + // Black text on white background, encoded to PNG bytes as in issue #1534 + BufferedImage bufferedImage = new BufferedImage(16, 16, BufferedImage.TYPE_BYTE_BINARY); + Graphics2D g = bufferedImage.createGraphics(); + g.setColor(Color.WHITE); + g.fillRect(0, 0, 16, 16); + g.setColor(Color.BLACK); + g.fillRect(0, 0, 8, 16); + g.dispose(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(bufferedImage, "png", baos); + + Image image = Image.getInstance(baos.toByteArray()); + + assertThat(image.getBpc()).isEqualTo(1); + assertImageMatches(bufferedImage, image); + } + + /** + * Resolves each pixel of a raw 1-bit indexed {@link Image} through its palette and compares it to the + * corresponding pixel of the original {@link BufferedImage}. + */ + private static void assertImageMatches(BufferedImage expected, Image image) { + PdfArray colorspace = (PdfArray) image.getAdditional().get(PdfName.COLORSPACE); + assertThat(colorspace).isNotNull(); + assertThat(colorspace.getPdfObject(0)).isEqualTo(PdfName.INDEXED); + byte[] palette = ((PdfString) colorspace.getPdfObject(3)).getBytes(); + + byte[] data = image.getRawData(); + int width = (int) image.getWidth(); + int height = (int) image.getHeight(); + int rowStride = (width + 7) / 8; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int index = (data[y * rowStride + x / 8] >> (7 - x % 8)) & 1; + int rgb = 0xff000000 + | ((palette[index * 3] & 0xff) << 16) + | ((palette[index * 3 + 1] & 0xff) << 8) + | (palette[index * 3 + 2] & 0xff); + assertThat(rgb) + .as("pixel at (%d, %d)", x, y) + .isEqualTo(expected.getRGB(x, y)); + } + } + } + }