blob: 583160ff5cbf21446180d3c4b56f14186d8565dd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* $Id$ */
package org.apache.xmlgraphics.image.loader.impl;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_Profile;
import java.io.IOException;
import java.util.Map;
import javax.imageio.stream.ImageInputStream;
import javax.xml.transform.Source;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlgraphics.image.loader.Image;
import org.apache.xmlgraphics.image.loader.ImageException;
import org.apache.xmlgraphics.image.loader.ImageFlavor;
import org.apache.xmlgraphics.image.loader.ImageInfo;
import org.apache.xmlgraphics.image.loader.ImageSessionContext;
import org.apache.xmlgraphics.image.loader.util.ImageUtil;
import org.apache.xmlgraphics.java2d.CMYKColorSpace;
import org.apache.xmlgraphics.util.MimeConstants;
/**
* ImageLoader for JPEG images consumed "raw" (undecoded). Provides a
* raw/undecoded stream.
*/
public class ImageLoaderRawJPEG extends AbstractImageLoader implements JPEGConstants {
/** logger */
protected static Log log = LogFactory.getLog(ImageLoaderRawJPEG.class);
/**
* Main constructor.
*/
public ImageLoaderRawJPEG() {
}
/** {@inheritDoc} */
public ImageFlavor getTargetFlavor() {
return ImageFlavor.RAW_JPEG;
}
/** {@inheritDoc} */
public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session)
throws ImageException, IOException {
if (!MimeConstants.MIME_JPEG.equals(info.getMimeType())) {
throw new IllegalArgumentException("ImageInfo must be from a image with MIME type: "
+ MimeConstants.MIME_JPEG);
}
ColorSpace colorSpace = null;
boolean appeFound = false;
int sofType = 0;
ByteArrayOutputStream iccStream = null;
Source src = session.needSource(info.getOriginalURI());
ImageInputStream in = ImageUtil.needImageInputStream(src);
in.mark();
try {
outer:
while (true) {
int reclen;
int segID = readMarkerSegment(in);
if (log.isDebugEnabled()) {
log.debug("Seg Marker: " + Integer.toHexString(segID));
}
switch (segID) {
case EOI:
log.debug("EOI found. Stopping.");
break outer;
case SOS:
log.debug("SOS found. Stopping early."); //TODO Not sure if this is safe
break outer;
case SOI:
case NULL:
break;
case SOF0: //baseline
case SOF2: //progressive (since PDF 1.3)
case SOFA: //progressive (since PDF 1.3)
sofType = segID;
if (log.isDebugEnabled()) {
log.debug("SOF: " + Integer.toHexString(sofType));
}
in.mark();
try {
reclen = in.readUnsignedShort();
in.skipBytes(1); //data precision
in.skipBytes(2); //height
in.skipBytes(2); //width
int numComponents = in.readUnsignedByte();
if (numComponents == 1) {
colorSpace = ColorSpace.getInstance(
ColorSpace.CS_GRAY);
} else if (numComponents == 3) {
colorSpace = ColorSpace.getInstance(
ColorSpace.CS_LINEAR_RGB);
} else if (numComponents == 4) {
colorSpace = CMYKColorSpace.getInstance();
} else {
throw new ImageException("Unsupported ColorSpace for image "
+ info
+ ". The number of components supported are 1, 3 and 4.");
}
} finally {
in.reset();
}
in.skipBytes(reclen);
break;
case APP2: //ICC (see ICC1V42.pdf)
in.mark();
try {
reclen = in.readUnsignedShort();
// Check for ICC profile
byte[] iccString = new byte[11];
in.readFully(iccString);
in.skipBytes(1); //string terminator (null byte)
if ("ICC_PROFILE".equals(new String(iccString, "US-ASCII"))) {
log.debug("JPEG has an ICC profile");
in.skipBytes(2); //chunk sequence number and total number of chunks
if (iccStream == null) {
//ICC profiles can be split into several chunks
//so collect in a byte array output stream
iccStream = new ByteArrayOutputStream();
}
byte[] buf = new byte[reclen - 18];
in.readFully(buf);
iccStream.write(buf);
}
} finally {
in.reset();
}
in.skipBytes(reclen);
break;
case APPE: //Adobe-specific (see 5116.DCT_Filter.pdf)
in.mark();
try {
reclen = in.readUnsignedShort();
// Check for Adobe header
byte[] adobeHeader = new byte[5];
in.readFully(adobeHeader);
if ("Adobe".equals(new String(adobeHeader, "US-ASCII"))) {
// The reason for reading the APPE marker is that Adobe Photoshop
// generates CMYK JPEGs with inverted values. The correct thing
// to do would be to interpret the values in the marker, but for now
// only assume that if APPE marker is present and colorspace is CMYK,
// the image is inverted.
appeFound = true;
}
} finally {
in.reset();
}
in.skipBytes(reclen);
break;
default:
reclen = in.readUnsignedShort();
in.skipBytes(reclen - 2);
}
}
} finally {
in.reset();
}
ICC_Profile iccProfile = buildICCProfile(info, colorSpace, iccStream);
if (iccProfile == null && colorSpace == null) {
throw new ImageException("ColorSpace not be identified for JPEG image " + info);
}
boolean invertImage = false;
if (appeFound && colorSpace.getType() == ColorSpace.TYPE_CMYK) {
if (log.isDebugEnabled()) {
log.debug("JPEG has an Adobe APPE marker. Note: CMYK Image will be inverted. ("
+ info.getOriginalURI() + ")");
}
invertImage = true;
}
ImageRawJPEG rawImage = new ImageRawJPEG(info,
ImageUtil.needInputStream(src),
sofType, colorSpace, iccProfile, invertImage);
return rawImage;
}
private ICC_Profile buildICCProfile(ImageInfo info, ColorSpace colorSpace,
ByteArrayOutputStream iccStream) throws IOException, ImageException {
if (iccStream != null && iccStream.size() > 0) {
int padding = (8 - (iccStream.size() % 8)) % 8;
if (padding != 0) {
try {
iccStream.write(new byte[padding]);
} catch (IOException ioe) {
throw new IOException("Error while aligning ICC stream: " + ioe.getMessage());
}
}
ICC_Profile iccProfile = null;
try {
iccProfile = ICC_Profile.getInstance(iccStream.toByteArray());
log.debug("JPEG has an ICC profile: " + iccProfile.toString());
} catch (IllegalArgumentException iae) {
log.warn("An ICC profile is present but it is invalid ("
+ iae.getMessage() + "). The color profile will be ignored. ("
+ info.getOriginalURI() + ")");
return null;
}
if (iccProfile.getNumComponents() != colorSpace.getNumComponents()) {
log.warn("The number of components of the ICC profile ("
+ iccProfile.getNumComponents()
+ ") doesn't match the image ("
+ colorSpace.getNumComponents()
+ "). Ignoring the ICC color profile.");
return null;
} else {
return iccProfile;
}
} else {
return null; //no ICC profile available
}
}
private int readMarkerSegment(ImageInputStream in) throws IOException {
int marker;
int count = 0;
long startPos = in.getStreamPosition();
do {
marker = in.read();
count++;
} while (marker != MARK);
if (count > 1) {
log.warn("no direct marker found: " + count + " at pos " + startPos);
}
int segID = in.read();
return segID;
}
}