Skip to content

Commit 3803028

Browse files
committed
Merge pull request #3 from bstoots/add-jpeg-cmyk-ycck-support
Add jpeg cmyk ycck support
2 parents 1bb5705 + 83ff145 commit 3803028

File tree

3 files changed

+254
-17
lines changed

3 files changed

+254
-17
lines changed

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
</properties>
1515

1616
<dependencies>
17+
<dependency>
18+
<groupId>org.apache.sanselan</groupId>
19+
<artifactId>sanselan</artifactId>
20+
<version>0.97-incubator</version>
21+
</dependency>
1722
<dependency>
1823
<groupId>junit</groupId>
1924
<artifactId>junit</artifactId>

src/main/java/com/koadweb/javafpdf/FPDF.java

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.util.Map;
4242
import java.util.Set;
4343
import javax.imageio.ImageIO;
44+
import org.apache.sanselan.ImageReadException;
4445

4546
/**
4647
* Faithful Java port of <a href="http://www.fpdf.org">FPDF for PHP</a>.
@@ -591,35 +592,42 @@ protected void _out(final String s) {
591592
}
592593
}
593594
}
594-
595+
595596
protected Map<String, Object> _parsejpg(String fileName, byte[] data) {
596597
BufferedImage img = null;
597598
try {
598-
img = ImageIO.read(new ByteArrayInputStream(data));
599+
// Image quality isn't the best this way but it fully supports CMYK and YCCK
600+
JpegReader jpegReader = new JpegReader();
601+
img = jpegReader.readImage(data);
599602

600-
Map<String, Object> image = new HashMap<>();
601-
image.put("w", Integer.valueOf(img.getWidth()));
602-
image.put("h", Integer.valueOf(img.getHeight()));
603603
String colspace;
604+
// In some cases ColorSpaces get converted by jpegReader but not always
605+
// 9 - TYPE_CMYK
606+
// 5 - TYPE_RGB
607+
// 6 - TYPE_GRAY
604608
if (img.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_CMYK) {
605-
colspace = "DeviceCMYK";
606-
} else if (img.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB) {
607-
colspace = "DeviceRGB";
608-
} else if (img.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY) {
609-
colspace = "DeviceGray";
610-
} else {
611-
throw new IllegalArgumentException("Ungültiges Farbmodell " + img.getColorModel().getColorSpace().getType());
612-
}
609+
colspace = "DeviceCMYK";
610+
} else if (img.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB) {
611+
colspace = "DeviceRGB";
612+
} else if (img.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY) {
613+
colspace = "DeviceGray";
614+
} else {
615+
throw new IllegalArgumentException("Ungültiges Farbmodell " + img.getColorModel().getColorSpace().getType());
616+
}
617+
//
618+
ByteArrayOutputStream boas = new ByteArrayOutputStream();
619+
ImageIO.write(img, "jpg", boas);
620+
// Load image map with img metadata / raw image data
621+
Map<String, Object> image = new HashMap<>();
622+
image.put("w", Integer.valueOf(img.getWidth()));
623+
image.put("h", Integer.valueOf(img.getHeight()));
613624
image.put("cs", colspace);
614625
image.put("bpc", 8);
615626
image.put("f", "DCTDecode");
616627
image.put("i", Integer.valueOf(this.images.size() + 1));
617-
618-
ByteArrayOutputStream boas = new ByteArrayOutputStream();
619-
ImageIO.write(img, "jpg", boas);
620628
image.put("data", boas.toByteArray());
621629
return image;
622-
} catch (IOException e) {
630+
} catch (IOException | ImageReadException e) {
623631
throw new RuntimeException(e);
624632
}
625633
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
2+
package com.koadweb.javafpdf;
3+
4+
import java.awt.color.ColorSpace;
5+
import java.awt.color.ICC_ColorSpace;
6+
import java.awt.color.ICC_Profile;
7+
import java.awt.image.BufferedImage;
8+
import java.awt.image.ColorConvertOp;
9+
import java.awt.image.Raster;
10+
import java.awt.image.WritableRaster;
11+
import java.io.ByteArrayInputStream;
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.util.ArrayList;
15+
import java.util.Iterator;
16+
import javax.imageio.IIOException;
17+
import javax.imageio.ImageIO;
18+
import javax.imageio.ImageReader;
19+
import javax.imageio.stream.ImageInputStream;
20+
import org.apache.sanselan.ImageReadException;
21+
import org.apache.sanselan.Sanselan;
22+
import org.apache.sanselan.common.byteSources.ByteSource;
23+
import org.apache.sanselan.common.byteSources.ByteSourceArray;
24+
import org.apache.sanselan.common.byteSources.ByteSourceFile;
25+
import org.apache.sanselan.formats.jpeg.JpegImageParser;
26+
import org.apache.sanselan.formats.jpeg.segments.UnknownSegment;
27+
28+
/**
29+
* All thanks go to Codo @ Stack Overflow for this gem
30+
* http://stackoverflow.com/questions/3123574/how-to-convert-from-cmyk-to-rgb-in-java-correctly
31+
* Can't believe this isn't built in to the ImageIO class ...
32+
*/
33+
public class JpegReader {
34+
35+
public static final int COLOR_TYPE_RGB = 1;
36+
public static final int COLOR_TYPE_CMYK = 2;
37+
public static final int COLOR_TYPE_YCCK = 3;
38+
39+
private int colorType = COLOR_TYPE_RGB;
40+
private boolean hasAdobeMarker = false;
41+
42+
public BufferedImage readImage(byte[] bytes) throws IOException, ImageReadException {
43+
colorType = COLOR_TYPE_RGB;
44+
hasAdobeMarker = false;
45+
ImageInputStream stream = ImageIO.createImageInputStream(new ByteArrayInputStream(bytes));
46+
Iterator<ImageReader> iter = ImageIO.getImageReaders(stream);
47+
while (iter.hasNext()) {
48+
ImageReader reader = iter.next();
49+
reader.setInput(stream);
50+
51+
BufferedImage image;
52+
ICC_Profile profile = null;
53+
try {
54+
image = reader.read(0);
55+
} catch (IIOException e) {
56+
colorType = COLOR_TYPE_CMYK;
57+
checkAdobeMarker(bytes);
58+
profile = Sanselan.getICCProfile(bytes);
59+
WritableRaster raster = (WritableRaster) reader.readRaster(0, null);
60+
if (colorType == COLOR_TYPE_YCCK)
61+
convertYcckToCmyk(raster);
62+
if (hasAdobeMarker)
63+
convertInvertedColors(raster);
64+
image = convertCmykToRgb(raster, profile);
65+
}
66+
67+
return image;
68+
}
69+
70+
return null;
71+
}
72+
73+
public BufferedImage readImage(File file) throws IOException, ImageReadException {
74+
colorType = COLOR_TYPE_RGB;
75+
hasAdobeMarker = false;
76+
ImageInputStream stream = ImageIO.createImageInputStream(file);
77+
Iterator<ImageReader> iter = ImageIO.getImageReaders(stream);
78+
while (iter.hasNext()) {
79+
ImageReader reader = iter.next();
80+
reader.setInput(stream);
81+
82+
BufferedImage image;
83+
ICC_Profile profile = null;
84+
try {
85+
image = reader.read(0);
86+
} catch (IIOException e) {
87+
colorType = COLOR_TYPE_CMYK;
88+
checkAdobeMarker(file);
89+
profile = Sanselan.getICCProfile(file);
90+
WritableRaster raster = (WritableRaster) reader.readRaster(0, null);
91+
if (colorType == COLOR_TYPE_YCCK)
92+
convertYcckToCmyk(raster);
93+
if (hasAdobeMarker)
94+
convertInvertedColors(raster);
95+
image = convertCmykToRgb(raster, profile);
96+
}
97+
98+
return image;
99+
}
100+
101+
return null;
102+
}
103+
104+
/**
105+
* Check Adobe markers in File
106+
* @param file
107+
* @throws IOException
108+
* @throws ImageReadException
109+
*/
110+
public void checkAdobeMarker(File file) throws IOException, ImageReadException {
111+
JpegImageParser parser = new JpegImageParser();
112+
ByteSource byteSource = new ByteSourceFile(file);
113+
@SuppressWarnings("rawtypes")
114+
ArrayList segments = parser.readSegments(byteSource, new int[] { 0xffee }, true);
115+
if (segments != null && segments.size() >= 1) {
116+
UnknownSegment app14Segment = (UnknownSegment) segments.get(0);
117+
byte[] data = app14Segment.bytes;
118+
if (data.length >= 12 && data[0] == 'A' && data[1] == 'd' && data[2] == 'o' && data[3] == 'b' && data[4] == 'e')
119+
{
120+
hasAdobeMarker = true;
121+
int transform = app14Segment.bytes[11] & 0xff;
122+
if (transform == 2)
123+
colorType = COLOR_TYPE_YCCK;
124+
}
125+
}
126+
}
127+
128+
/**
129+
* Check Adobe markers in byte array
130+
* @param bytes
131+
* @throws IOException
132+
* @throws ImageReadException
133+
*/
134+
public void checkAdobeMarker(byte[] bytes) throws IOException, ImageReadException {
135+
JpegImageParser parser = new JpegImageParser();
136+
ByteSource byteSource = new ByteSourceArray(bytes);
137+
@SuppressWarnings("rawtypes")
138+
ArrayList segments = parser.readSegments(byteSource, new int[] { 0xffee }, true);
139+
if (segments != null && segments.size() >= 1) {
140+
UnknownSegment app14Segment = (UnknownSegment) segments.get(0);
141+
byte[] data = app14Segment.bytes;
142+
if (data.length >= 12 && data[0] == 'A' && data[1] == 'd' && data[2] == 'o' && data[3] == 'b' && data[4] == 'e')
143+
{
144+
hasAdobeMarker = true;
145+
int transform = app14Segment.bytes[11] & 0xff;
146+
if (transform == 2)
147+
colorType = COLOR_TYPE_YCCK;
148+
}
149+
}
150+
}
151+
152+
public static void convertYcckToCmyk(WritableRaster raster) {
153+
int height = raster.getHeight();
154+
int width = raster.getWidth();
155+
int stride = width * 4;
156+
int[] pixelRow = new int[stride];
157+
for (int h = 0; h < height; h++) {
158+
raster.getPixels(0, h, width, 1, pixelRow);
159+
160+
for (int x = 0; x < stride; x += 4) {
161+
int y = pixelRow[x];
162+
int cb = pixelRow[x + 1];
163+
int cr = pixelRow[x + 2];
164+
165+
int c = (int) (y + 1.402 * cr - 178.956);
166+
int m = (int) (y - 0.34414 * cb - 0.71414 * cr + 135.95984);
167+
y = (int) (y + 1.772 * cb - 226.316);
168+
169+
if (c < 0) c = 0; else if (c > 255) c = 255;
170+
if (m < 0) m = 0; else if (m > 255) m = 255;
171+
if (y < 0) y = 0; else if (y > 255) y = 255;
172+
173+
pixelRow[x] = 255 - c;
174+
pixelRow[x + 1] = 255 - m;
175+
pixelRow[x + 2] = 255 - y;
176+
}
177+
178+
raster.setPixels(0, h, width, 1, pixelRow);
179+
}
180+
}
181+
182+
public static void convertInvertedColors(WritableRaster raster) {
183+
int height = raster.getHeight();
184+
int width = raster.getWidth();
185+
int stride = width * 4;
186+
int[] pixelRow = new int[stride];
187+
for (int h = 0; h < height; h++) {
188+
raster.getPixels(0, h, width, 1, pixelRow);
189+
for (int x = 0; x < stride; x++)
190+
pixelRow[x] = 255 - pixelRow[x];
191+
raster.setPixels(0, h, width, 1, pixelRow);
192+
}
193+
}
194+
195+
public static BufferedImage convertCmykToRgb(Raster cmykRaster, ICC_Profile cmykProfile) throws IOException {
196+
if (cmykProfile == null)
197+
cmykProfile = ICC_Profile.getInstance(JpegReader.class.getResourceAsStream("/ISOcoated_v2_300_eci.icc"));
198+
199+
if (cmykProfile.getProfileClass() != ICC_Profile.CLASS_DISPLAY) {
200+
byte[] profileData = cmykProfile.getData();
201+
202+
if (profileData[ICC_Profile.icHdrRenderingIntent] == ICC_Profile.icPerceptual) {
203+
intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first
204+
205+
cmykProfile = ICC_Profile.getInstance(profileData);
206+
}
207+
}
208+
209+
ICC_ColorSpace cmykCS = new ICC_ColorSpace(cmykProfile);
210+
BufferedImage rgbImage = new BufferedImage(cmykRaster.getWidth(), cmykRaster.getHeight(), BufferedImage.TYPE_INT_RGB);
211+
WritableRaster rgbRaster = rgbImage.getRaster();
212+
ColorSpace rgbCS = rgbImage.getColorModel().getColorSpace();
213+
ColorConvertOp cmykToRgb = new ColorConvertOp(cmykCS, rgbCS, null);
214+
cmykToRgb.filter(cmykRaster, rgbRaster);
215+
return rgbImage;
216+
}
217+
218+
static void intToBigEndian(int value, byte[] array, int index) {
219+
array[index] = (byte) (value >> 24);
220+
array[index+1] = (byte) (value >> 16);
221+
array[index+2] = (byte) (value >> 8);
222+
array[index+3] = (byte) (value);
223+
}
224+
}

0 commit comments

Comments
 (0)