001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: /* $Id: JpegImage.java 523452 2007-03-28 20:28:23Z jeremias $ */
019:
020: package org.apache.fop.image;
021:
022: // Java
023: import java.awt.color.ColorSpace;
024: import java.awt.color.ICC_Profile;
025:
026: // FOP
027: import org.apache.commons.io.IOUtils;
028: import org.apache.commons.io.output.ByteArrayOutputStream;
029: import org.apache.fop.util.CMYKColorSpace;
030:
031: /**
032: * FopImage object for JPEG images, Using Java native classes.
033: * @author Eric Dalquist
034: * @see AbstractFopImage
035: * @see FopImage
036: */
037: public class JpegImage extends AbstractFopImage {
038: private ICC_Profile iccProfile = null;
039: private boolean foundICCProfile = false;
040: private boolean hasAPPEMarker = false;
041:
042: /**
043: * Create a jpeg image with the info.
044: *
045: * @param imgInfo the image info for this jpeg
046: */
047: public JpegImage(FopImage.ImageInfo imgInfo) {
048: super (imgInfo);
049: }
050:
051: /**
052: * Load the original jpeg data.
053: * This loads the original jpeg data and reads the color space,
054: * and icc profile if any.
055: *
056: * @return true if loaded false for any error
057: */
058: protected boolean loadOriginalData() {
059: ByteArrayOutputStream baos = new ByteArrayOutputStream();
060: ByteArrayOutputStream iccStream = null;
061: int index = 0;
062: boolean cont = true;
063:
064: try {
065: byte[] readBuf = new byte[4096];
066: int bytesRead;
067: while ((bytesRead = inputStream.read(readBuf)) != -1) {
068: baos.write(readBuf, 0, bytesRead);
069: }
070: } catch (java.io.IOException ex) {
071: log.error("Error while loading image (Jpeg): "
072: + ex.getMessage(), ex);
073: return false;
074: } finally {
075: IOUtils.closeQuietly(inputStream);
076: inputStream = null;
077: }
078:
079: this .raw = baos.toByteArray();
080: this .bitsPerPixel = 8;
081: this .isTransparent = false;
082:
083: //Check for SOI (Start of image) marker (FFD8)
084: if (this .raw.length > (index + 2)
085: && uByte(this .raw[index]) == 255 /*0xFF*/
086: && uByte(this .raw[index + 1]) == 216 /*0xD8*/) {
087: index += 2;
088:
089: while (index < this .raw.length && cont) {
090: //check to be sure this is the begining of a header
091: if (this .raw.length > (index + 2)
092: && uByte(this .raw[index]) == 255 /*0xFF*/) {
093:
094: //192 or 194 are the header bytes that contain
095: // the jpeg width height and color depth.
096: if (uByte(this .raw[index + 1]) == 192 /*0xC0*/
097: || uByte(this .raw[index + 1]) == 194 /*0xC2*/) {
098:
099: this .height = calcBytes(this .raw[index + 5],
100: this .raw[index + 6]);
101: this .width = calcBytes(this .raw[index + 7],
102: this .raw[index + 8]);
103:
104: if (this .raw[index + 9] == 1) {
105: this .colorSpace = ColorSpace
106: .getInstance(ColorSpace.CS_GRAY);
107: } else if (this .raw[index + 9] == 3) {
108: this .colorSpace = ColorSpace
109: .getInstance(ColorSpace.CS_LINEAR_RGB);
110: } else if (this .raw[index + 9] == 4) {
111: // howto create CMYK color space
112: /*
113: this.colorSpace = ColorSpace.getInstance(
114: ColorSpace.CS_CIEXYZ);
115: */
116: this .colorSpace = CMYKColorSpace
117: .getInstance();
118: } else {
119: log.error("Unknown ColorSpace for image: "
120: + "");
121: return false;
122: }
123:
124: if (foundICCProfile) {
125: cont = false;
126: break;
127: }
128: index += calcBytes(this .raw[index + 2],
129: this .raw[index + 3]) + 2;
130:
131: } else if (uByte(this .raw[index + 1]) == 226 /*0xE2*/
132: && this .raw.length > (index + 60)) {
133: // Check if ICC profile
134: byte[] iccString = new byte[11];
135: System.arraycopy(this .raw, index + 4,
136: iccString, 0, 11);
137:
138: if ("ICC_PROFILE".equals(new String(iccString))) {
139: int chunkSize = calcBytes(
140: this .raw[index + 2],
141: this .raw[index + 3]) + 2;
142:
143: if (iccStream == null) {
144: iccStream = new ByteArrayOutputStream();
145: }
146: iccStream.write(this .raw, index + 18,
147: chunkSize - 18);
148:
149: }
150:
151: index += calcBytes(this .raw[index + 2],
152: this .raw[index + 3]) + 2;
153: // Check for Adobe APPE Marker
154: } else if ((uByte(this .raw[index]) == 0xff
155: && uByte(this .raw[index + 1]) == 0xee
156: && uByte(this .raw[index + 2]) == 0
157: && uByte(this .raw[index + 3]) == 14 && "Adobe"
158: .equals(new String(this .raw, index + 4, 5)))) {
159: // The reason for reading the APPE marker is that Adobe Photoshop
160: // generates CMYK JPEGs with inverted values. The correct thing
161: // to do would be to interpret the values in the marker, but for now
162: // only assume that if APPE marker is present and colorspace is CMYK,
163: // the image is inverted.
164: hasAPPEMarker = true;
165:
166: index += calcBytes(this .raw[index + 2],
167: this .raw[index + 3]) + 2;
168: } else {
169: index += calcBytes(this .raw[index + 2],
170: this .raw[index + 3]) + 2;
171: }
172:
173: } else {
174: cont = false;
175: }
176: }
177: } else {
178: log.error("Error while loading "
179: + "JpegImage - Invalid JPEG Header.");
180: return false;
181: }
182: if (iccStream != null && iccStream.size() > 0) {
183: int padding = (8 - (iccStream.size() % 8)) % 8;
184: if (padding != 0) {
185: try {
186: iccStream.write(new byte[padding]);
187: } catch (Exception ex) {
188: log.error("Error while aligning ICC stream: "
189: + ex.getMessage(), ex);
190: return false;
191: }
192: }
193: try {
194: iccProfile = ICC_Profile.getInstance(iccStream
195: .toByteArray());
196: } catch (IllegalArgumentException iae) {
197: log
198: .warn("An ICC profile is present but it is invalid ("
199: + iae.getMessage()
200: + "). The color profile will be ignored. ("
201: + this .getOriginalURI() + ")");
202: }
203: } else if (this .colorSpace == null) {
204: log.error("ColorSpace not specified for JPEG image");
205: return false;
206: }
207: if (hasAPPEMarker
208: && this .colorSpace.getType() == ColorSpace.TYPE_CMYK) {
209: if (log.isDebugEnabled()) {
210: log
211: .debug("JPEG has an Adobe APPE marker. Note: CMYK Image will be inverted. ("
212: + this .getOriginalURI() + ")");
213: }
214: this .invertImage = true;
215: }
216: return true;
217: }
218:
219: /**
220: * Get the ICC profile for this Jpeg image.
221: *
222: * @return the icc profile or null if not found
223: */
224: public ICC_Profile getICCProfile() {
225: return iccProfile;
226: }
227:
228: private int calcBytes(byte bOne, byte bTwo) {
229: return (uByte(bOne) * 256) + uByte(bTwo);
230: }
231:
232: private int uByte(byte bIn) {
233: if (bIn < 0) {
234: return 256 + bIn;
235: } else {
236: return bIn;
237: }
238: }
239: }
|