|
61 | 61 | import org.openpdf.text.pdf.PdfObject; |
62 | 62 | import org.openpdf.text.pdf.PdfReader; |
63 | 63 | import org.openpdf.text.pdf.PdfStream; |
| 64 | +import org.openpdf.text.pdf.PdfString; |
64 | 65 | import org.openpdf.text.pdf.PdfTemplate; |
65 | 66 | import org.openpdf.text.pdf.PdfWriter; |
66 | 67 | import org.openpdf.text.pdf.codec.CCITTG4Encoder; |
67 | 68 | import java.awt.Graphics2D; |
68 | 69 | import java.awt.color.ICC_Profile; |
69 | 70 | import java.awt.image.BufferedImage; |
| 71 | +import java.awt.image.IndexColorModel; |
| 72 | +import java.awt.image.WritableRaster; |
70 | 73 | import java.io.IOException; |
71 | 74 | import java.io.InputStream; |
72 | 75 | import java.lang.reflect.Constructor; |
@@ -824,6 +827,63 @@ public static Image getInstance(java.awt.Image image, java.awt.Color color, |
824 | 827 | if (bi.getType() == BufferedImage.TYPE_BYTE_BINARY && bi.getColorModel().getNumColorComponents() <= 2) { |
825 | 828 | forceBW = true; |
826 | 829 | } |
| 830 | + |
| 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 | + } |
| 850 | + |
| 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 | + } |
| 866 | + |
| 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; |
| 886 | + } |
827 | 887 | } |
828 | 888 |
|
829 | 889 | 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, |
987 | 1047 | } |
988 | 1048 | } |
989 | 1049 |
|
| 1050 | + /** |
| 1051 | + * Generates PDF-compliant pixel data for indexed color images (IndexColorModel). |
| 1052 | + * <p> |
| 1053 | + * This method packs palette indices from a WritableRaster into a byte array that strictly adheres to |
| 1054 | + * PDF specification requirements for indexed color image storage: |
| 1055 | + * <ul> |
| 1056 | + * <li>Pixel indices are packed starting from the Most Significant Bit (MSB, bit 7) of each byte (PDF mandatory rule)</li> |
| 1057 | + * <li>Each row of pixel data is byte-aligned (padded with zeros to match calculated row stride)</li> |
| 1058 | + * <li>Supports standard indexed color bit depths: 1, 2, 4, 8 bits per pixel</li> |
| 1059 | + * <li>Normalizes palette indices to unsigned 0-255 range to prevent invalid negative values</li> |
| 1060 | + * </ul> |
| 1061 | + * |
| 1062 | + * @param width Width of the indexed color image (in pixels) |
| 1063 | + * @param bitsPerPixel Number of bits per pixel (must be 1, 2, 4, or 8 for valid indexed color) |
| 1064 | + * @param height Height of the indexed color image (in pixels) |
| 1065 | + * @param raster WritableRaster containing the indexed color pixel indices (from IndexColorModel BufferedImage) |
| 1066 | + * @return Byte array of pixel data packed according to PDF indexed color specifications, with row-wise byte alignment |
| 1067 | + * @see WritableRaster |
| 1068 | + * @see IndexColorModel |
| 1069 | + */ |
| 1070 | + private static byte[] generateIndexedColorPixelData(int width, int bitsPerPixel, int height, WritableRaster raster) { |
| 1071 | + int rowStride = (width * bitsPerPixel + 7) / 8; |
| 1072 | + byte[] pixelData = new byte[rowStride * height]; |
| 1073 | + |
| 1074 | + int bytePos = 0; |
| 1075 | + int bitOffset; |
| 1076 | + |
| 1077 | + for (int y = 0; y < height; y++) { |
| 1078 | + bitOffset = 7; |
| 1079 | + for (int x = 0; x < width; x++) { |
| 1080 | + int pixelIndex = raster.getSample(x, y, 0); |
| 1081 | + if (pixelIndex < 0) { |
| 1082 | + pixelIndex = 0; |
| 1083 | + } |
| 1084 | + pixelIndex = pixelIndex & 0xFF; |
| 1085 | + |
| 1086 | + bitOffset = packPixelByBitDepth(bitsPerPixel, pixelIndex, pixelData, bytePos, bitOffset); |
| 1087 | + |
| 1088 | + if (bitOffset < 0) { |
| 1089 | + bytePos++; |
| 1090 | + bitOffset = 7; |
| 1091 | + } |
| 1092 | + } |
| 1093 | + int usedBytesInRow = bytePos - (y * rowStride); |
| 1094 | + if (usedBytesInRow < rowStride) { |
| 1095 | + int padBytes = rowStride - usedBytesInRow; |
| 1096 | + bytePos += padBytes; |
| 1097 | + } |
| 1098 | + } |
| 1099 | + return pixelData; |
| 1100 | + } |
| 1101 | + |
| 1102 | + /** |
| 1103 | + * Packs a single pixel index into the target byte array based on specified bit depth (PDF MSB-first rule). |
| 1104 | + */ |
| 1105 | + private static int packPixelByBitDepth(int bitsPerPixel, int pixelIndex, byte[] pixelData, int bytePos, int bitOffset) { |
| 1106 | + int currentBitOffset = bitOffset; |
| 1107 | + |
| 1108 | + switch (bitsPerPixel) { |
| 1109 | + case 1: |
| 1110 | + if ((pixelIndex & 0x01) == 1) { |
| 1111 | + pixelData[bytePos] |= (byte) (1 << currentBitOffset); |
| 1112 | + } |
| 1113 | + currentBitOffset--; |
| 1114 | + break; |
| 1115 | + case 2: |
| 1116 | + pixelData[bytePos] |= (byte) ((pixelIndex & 0x03) << (currentBitOffset - 1)); |
| 1117 | + currentBitOffset -= 2; |
| 1118 | + break; |
| 1119 | + case 4: |
| 1120 | + pixelData[bytePos] |= (byte) ((pixelIndex & 0x0F) << (currentBitOffset - 3)); |
| 1121 | + currentBitOffset -= 4; |
| 1122 | + break; |
| 1123 | + case 8: |
| 1124 | + default: |
| 1125 | + pixelData[bytePos] = (byte) pixelIndex; |
| 1126 | + currentBitOffset = -1; |
| 1127 | + break; |
| 1128 | + } |
| 1129 | + return currentBitOffset; |
| 1130 | + } |
| 1131 | + |
990 | 1132 | /** |
991 | 1133 | * Gets an instance of an Image from a java.awt.Image. |
992 | 1134 | * |
|
0 commit comments