/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.image.libs;
import java.awt.*;
import java.awt.color.ICC_Profile;
import java.awt.image.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.w3c.dom.Document;
import sun.awt.image.ByteInterleavedRaster;
import com.lightcrafts.image.metadata.*;
import com.lightcrafts.image.types.JPEGImageType;
import com.lightcrafts.mediax.jai.PlanarImage;
import com.lightcrafts.utils.ProgressIndicator;
import com.lightcrafts.utils.bytebuffer.ByteBufferUtil;
import com.lightcrafts.utils.thread.ProgressThread;
import com.lightcrafts.image.export.ResolutionOption;
import com.lightcrafts.image.export.ResolutionUnitOption;
import com.lightcrafts.image.types.TIFFConstants;
import com.lightcrafts.utils.xml.XMLUtil;
import static com.lightcrafts.image.libs.LCJPEGConstants.*;
import static com.lightcrafts.image.types.AdobeConstants.*;
import static com.lightcrafts.image.types.JPEGConstants.*;
/**
* An <code>LCJPEGWriter</code> is a Java wrapper around the LibJPEG library
* for writing JPEG images.
*
* @author Paul J. Lucas [paul@lightcrafts.com]
* @see <a href="http://www.ijg.org/">LibJPEG</a>
*/
public final class LCJPEGWriter {
////////// public /////////////////////////////////////////////////////////
/**
* Construct an <code>LCJPEGWriter</code>.
*
* @param fileName The name of the JPEG file to write to.
* @param width The width of the image in pixels.
* @param height The height of the image in pixels.
* @param colorsPerPixel The number of color components per pixel.
* @param colorSpace The colorspace of the input image; must be one of
* {@link LCJPEGConstants#CS_GRAYSCALE}, {@link LCJPEGConstants#CS_RGB},
* {@link LCJPEGConstants#CS_YCbRr}, {@link LCJPEGConstants#CS_CMYK}, or
* {@link LCJPEGConstants#CS_YCCK}.
* @param quality Image quality: 0-100.
* @param resolution The resolution (in pixels per unit).
* @param resolutionUnit The resolution unit; must be either
* {@link TIFFConstants#TIFF_RESOLUTION_UNIT_CM} or
* {@link TIFFConstants#TIFF_RESOLUTION_UNIT_INCH}.
*/
public LCJPEGWriter( String fileName, int width, int height,
int colorsPerPixel, int colorSpace, int quality,
int resolution, int resolutionUnit )
throws IOException, LCImageLibException
{
m_exportWidth = width;
m_exportHeight = height;
m_resolution = resolution;
m_resolutionUnit = resolutionUnit;
openForWriting(
fileName, width, height, colorsPerPixel, colorSpace, quality
);
}
/**
* Construct an <code>LCJPEGWriter</code>.
*
* @param receiver The {@link LCImageDataReceiver} to send image data to.
* @param bufSize The size of the buffer (in bytes) to use.
* @param width The width of the image in pixels.
* @param height The height of the image in pixels.
* @param colorsPerPixel The number of color components per pixel.
* @param colorSpace The colorspace of the input image; must be one of
* {@link LCJPEGConstants#CS_GRAYSCALE}, {@link LCJPEGConstants#CS_RGB},
* {@link LCJPEGConstants#CS_YCbRr}, {@link LCJPEGConstants#CS_CMYK}, or
* {@link LCJPEGConstants#CS_YCCK}.
* @param quality Image quality: 0-100.
*/
public LCJPEGWriter( LCImageDataReceiver receiver, int bufSize, int width,
int height, int colorsPerPixel, int colorSpace,
int quality )
throws LCImageLibException
{
m_exportWidth = width;
m_exportHeight = height;
m_resolution = ResolutionOption.DEFAULT_VALUE;
m_resolutionUnit = ResolutionUnitOption.DEFAULT_VALUE;
beginWrite(
receiver, bufSize, width, height, colorsPerPixel, colorSpace,
quality
);
}
/**
* Dispose of an <code>LCJPEGWriter</code>.
*/
public native void dispose();
/**
* Gets the colorspace constant used by libJPEG from the number of color
* components of an image.
*
* @param numComponents The number of color components.
* @return Returns said colorspace constant.
*/
public static int getColorSpaceFromNumComponents( int numComponents ) {
switch ( numComponents ) {
case 1:
return CS_GRAYSCALE;
case 3:
return CS_RGB;
case 4:
return CS_CMYK;
default:
return CS_UNKNOWN;
}
}
/**
* Puts an image, compressing it into a JPEG.
*
* @param image The image to compress into a JPEG.
*/
public void putImage( RenderedImage image ) throws LCImageLibException {
putImage( image, null );
}
/**
* Puts an image, compressing it into a JPEG.
*
* @param image The image to compress into a JPEG.
* @param thread The {@link ProgressThread} that is putting the JPEG.
*/
public void putImage( RenderedImage image, ProgressThread thread )
throws LCImageLibException
{
try {
final int bands = image.getSampleModel().getNumBands();
if ( bands == 4 /* CMYK */ ) {
//
// Write a mimicked APPE segment so 3rd-party applications that
// read CYMK JPEG images will think the creator is Photoshop
// and thus know to invert the image data.
//
writeAdobeSegment( ADOBE_CTT_UNKNOWN );
}
writeImage( image, thread );
}
finally {
dispose();
}
}
/**
* Puts the given {@link ImageMetadata} into the JPEG file. Only EXIF and
* IPTC metadata are put. This <i>must</i> be called only once and prior
* to {@link #putImage(RenderedImage,ProgressThread)}.
*
* @param metadata The {@link ImageMetadata} to put.
*/
public void putMetadata( ImageMetadata metadata )
throws LCImageLibException
{
metadata = metadata.prepForExport(
JPEGImageType.INSTANCE, m_exportWidth, m_exportHeight,
m_resolution, m_resolutionUnit, false
);
////////// Write EXIF metadata, if any ////////////////////////////////
//
// The binary form of EXIF metadata, if any, has to go before XMP
// metadata, otherwise Windows Explorer won't see the EXIF metadata.
//
final ImageMetadataDirectory exifDir =
metadata.getDirectoryFor( EXIFDirectory.class );
if ( exifDir != null ) {
final byte[] exifSegBuf =
EXIFEncoder.encode( metadata, true ).array();
//ByteBufferUtil.dumpToFile( exifBuf, "/tmp/jpg.exif" );
writeSegment( JPEG_APP1_MARKER, exifSegBuf );
}
////////// Write XMP metadata /////////////////////////////////////////
final Document xmpDoc = metadata.toXMP( false, true );
final byte[] xmpSegBuf = XMLUtil.encodeDocument( xmpDoc, true );
writeSegment( JPEG_APP1_MARKER, xmpSegBuf );
////////// Write IPTC metadata, if any ////////////////////////////////
final ImageMetadataDirectory iptcDir =
metadata.getDirectoryFor( IPTCDirectory.class );
if ( iptcDir != null ) {
final byte[] iptcSegBuf = ((IPTCDirectory)iptcDir).encode( true );
if ( iptcSegBuf != null )
writeSegment( JPEG_APPD_MARKER, iptcSegBuf );
}
}
/**
* Sets the ICC profile of the JPEG image. This <i>must</i> be called only
* once and prior to {@link #putImage(RenderedImage,ProgressThread)}.
*
* @param iccProfile The {@link ICC_Profile} to set.
*/
public void setICCProfile( ICC_Profile iccProfile )
throws LCImageLibException
{
final byte[] iccProfileData = iccProfile.getData();
final int chunkSize = JPEG_MAX_SEGMENT_SIZE - ICC_PROFILE_HEADER_SIZE;
//
// We must calculate the total size of all the segments including a
// header per segment.
//
int totalSize = iccProfileData.length
+ ( (iccProfileData.length - 1) / chunkSize + 1 )
* ICC_PROFILE_HEADER_SIZE;
//
// Given the total size, we can calculate the number of segments
// needed.
//
final int numSegments = (totalSize - 1) / JPEG_MAX_SEGMENT_SIZE + 1;
//
// Now split the profile data across the number of segments with a
// header per segment.
//
for ( int i = 0; i < numSegments; ++i ) {
final int segSize = Math.min( JPEG_MAX_SEGMENT_SIZE, totalSize );
final ByteBuffer buf = ByteBuffer.allocate( segSize );
ByteBufferUtil.put( buf, "ICC_PROFILE", "ASCII" );
buf.put( (byte)0 );
buf.put( (byte)(i + 1) );
buf.put( (byte)numSegments );
buf.put(
iccProfileData, i * chunkSize,
segSize - ICC_PROFILE_HEADER_SIZE
);
writeSegment( JPEG_APP2_MARKER, buf.array() );
totalSize -= JPEG_MAX_SEGMENT_SIZE;
}
}
/**
* Compresses and writes a raw set of scanlines to the JPEG image.
*
* @param buf The buffer from which to compress the image data.
* @param offset The offset into the buffer where the image data will begin
* being read.
* @param numLines The number of scanlines to compress.
* @return Returns the number of scanlines written.
*/
public native synchronized int writeScanLines( byte[] buf, int offset,
int numLines,
int lineStride )
throws LCImageLibException;
/**
* Write an APP segment to the JPEG file.
*
* @param marker The APP segment marker.
* @param buf The buffer comprising the raw binary contents for the
* segment.
*/
public native void writeSegment( int marker, byte[] buf )
throws LCImageLibException;
////////// protected //////////////////////////////////////////////////////
/**
* Finalize this class by calling {@link #dispose()}.
*/
protected void finalize() throws Throwable {
dispose();
super.finalize();
}
////////// private ////////////////////////////////////////////////////////
/**
* Begin using the {@link LCImageDataProvider} to get JPEG image data.
*
* @param receiver The {@link LCImageDataReceiver} to send image data to.
* @param bufSize The size of the buffer (in bytes) to use.
* @param width The width of the image in pixels.
* @param height The height of the image in pixels.
* @param colorsPerPixel The number of color components per pixel.
* @param colorSpace The colorspace of the input image; must be one of
* {@link LCJPEGConstants#CS_GRAYSCALE}, {@link LCJPEGConstants#CS_RGB},
* {@link LCJPEGConstants#CS_YCbRr}, {@link LCJPEGConstants#CS_CMYK}, or
* {@link LCJPEGConstants#CS_YCCK}.
* @param quality Image quality: 0-100.
*/
private native void beginWrite( LCImageDataReceiver receiver, int bufSize,
int width, int height, int colorsPerPixel,
int colorSpace, int quality )
throws LCImageLibException;
/**
* Opens a JPEG file for writing.
*
* @param fileName The name of the JPEG file to write to.
* @param width The width of the image in pixels.
* @param height The height of the image in pixels.
* @param colorsPerPixel The number of color components per pixel.
* @param colorSpace The colorspace of the input image; must be one of
* {@link LCJPEGConstants#CS_GRAYSCALE}, {@link LCJPEGConstants#CS_RGB},
* {@link LCJPEGConstants#CS_YCbRr}, {@link LCJPEGConstants#CS_CMYK}, or
* {@link LCJPEGConstants#CS_YCCK}.
* @param quality Image quality: 0-100.
*/
private native void openForWriting( String fileName, int width, int height,
int colorsPerPixel, int colorSpace,
int quality )
throws IOException, LCImageLibException;
/**
* Writes an Adobe (APPE) segment. The bytes of an Adobe segment are:
* <blockquote>
* <table border="0" cellpadding="0">
* <tr valign="top">
* <td>0-4 </td>
* <td>String: <code>Adobe</code></td>
* </tr>
* <tr valign="top">
* <td>5-6 </td>
* <td>DCTEncode/DCTDecode version number: 0x0065</td>
* </tr>
* <tr valign="top">
* <td>7-8 </td>
* <td>
* flags0 0x8000 bit:
* <blockquote>
* <table border="0" cellpadding="0">
* <tr><td>0 = </td><td>downsampling</td></tr>
* <tr><td>1 = </td><td>blend</td></tr>
* </table>
* </blockquote>
* </td>
* </tr>
* <tr valign="top">
* <td>9-10 </td>
* <td>flags1</td>
* </tr>
* <tr valign="top">
* <td>11 </td>
* <td>
* Color transformation code:
* <blockquote>
* <table border="0" cellpadding="0">
* <tr><td>0 = </td><td>unknown</td></tr>
* <tr><td>1 = </td><td>YcbCr</td></tr>
* <tr><td>2 = </td><td>YCCK</td></tr>
* </table>
* </blockquote>
* </td>
* </tr>
* </table>
* </blockquote>
* Notes:
* <ul>
* <li>
* For a color transform code of 0 (unknown), 3-channel images are
* assumed to be RGB and 4-channel images are assumed to be CMYK.
* </li>
* <li>
* Although the Adobe technical note says the version number is 0x65,
* all Adobe-generated files in existence have version 0x64.
* </li>
* </ul>
*
* @param colorTransformationCode One of <code>ADOBE_CTT_UNKNOWN</code>,
* <code>ADOBE_CTT_YCBCR</code>, or <code>ADOBE_CTT_YCCK</code>.
* @see "Adobe Technical Note #5116: Supporting the DCT Filters in
* PostScript Level 2, Adobe Systems, Inc., November 24, 1992, p. 23."
*/
private void writeAdobeSegment( byte colorTransformationCode )
throws LCImageLibException
{
final ByteBuffer buf = ByteBuffer.allocate( ADOBE_APPE_SEGMENT_SIZE );
ByteBufferUtil.put( buf, "Adobe", "ASCII" );
buf.putShort( (short)0x0064 ); // version number
buf.putShort( (short)0 ); // flags0
buf.putShort( (short)0 ); // flags1
buf.put( colorTransformationCode );
writeSegment( JPEG_APPE_MARKER, buf.array() );
}
/**
* Writes an image, compressing it into a JPEG.
*
* @param image The image to compress into a JPEG.
* @param thread The {@link ProgressThread} to use, if any.
*/
private void writeImage( RenderedImage image, ProgressThread thread )
throws LCImageLibException
{
/* if ( image.getSampleModel().getDataType() != DataBuffer.TYPE_BYTE )
throw new IllegalArgumentException( "Image data type is not byte" ); */
final int imageWidth = image.getWidth();
final int imageHeight = image.getHeight();
final Rectangle stripRect = new Rectangle();
int stripHeight = 8;
ProgressIndicator indicator = null;
if ( thread != null )
indicator = thread.getProgressIndicator();
if ( indicator != null )
indicator.setMaximum( imageHeight );
final int bands = image.getSampleModel().getNumBands();
final SampleModel sm =
new PixelInterleavedSampleModel(
DataBuffer.TYPE_BYTE,
imageWidth, stripHeight, bands, bands * imageWidth,
bands == 1 ? new int[]{ 0 } :
bands == 3 ? new int[]{ 0, 1, 2 } :
new int[]{ 0, 1, 2, 3 }
);
final ByteInterleavedRaster rasterBuffer = new ByteInterleavedRaster(sm, new Point(0, 0));
for ( int y = 0; y < imageHeight; y += stripHeight ) {
if ( thread != null && thread.isCanceled() )
return;
stripHeight = Math.min( stripHeight, imageHeight - y );
stripRect.setBounds( 0, y, imageWidth, stripHeight );
final ByteInterleavedRaster raster = (ByteInterleavedRaster) rasterBuffer.createTranslatedChild(0, y);
// Prefetch tiles, uses all CPUs
if (image instanceof PlanarImage)
((PlanarImage) image).getTiles(((PlanarImage) image).getTileIndices(raster.getBounds()));
image.copyData(raster);
final DataBufferByte db = (DataBufferByte)raster.getDataBuffer();
final int[] offsets = raster.getDataOffsets();
int offset = offsets[0];
for (int i = 1; i < offsets.length; i++)
offset = Math.min(offset, offsets[i]);
if ( bands == 4 /* CMYK */ ) {
//
// A long-standing Photoshop bug is that CMYK images are stored
// inverted. To be compatible with Photoshop, we have to
// invert CMYK images too.
//
final byte[] data = db.getData();
for ( int i = 0; i < data.length; ++i )
data[i] = (byte)~data[i];
}
final int lineStride = raster.getScanlineStride();
final int written = writeScanLines( db.getData(), offset, stripHeight, lineStride );
if ( written != stripHeight )
throw new LCImageLibException(
"something is wrong: " + written + " != " + stripHeight
);
if ( indicator != null )
indicator.incrementBy( stripHeight );
}
if ( indicator != null )
indicator.setIndeterminate( true );
}
/**
* The height of the image as exported.
*/
private final int m_exportHeight;
/**
* The width of the image as exported.
*/
private final int m_exportWidth;
/**
* The resolution (in pixels per unit) of the image as exported.
*/
private final int m_resolution;
/**
* The resolution unit of the image as exported.
*/
private final int m_resolutionUnit;
/**
* This is where the native code stores a pointer to the <code>JPEG</code>
* native data structure. Do not touch this from Java except to compare it
* to zero.
*/
@SuppressWarnings({"UNUSED_SYMBOL"})
private long m_nativePtr;
static {
System.loadLibrary( "LCJPEG" );
}
}
/* vim:set et sw=4 ts=4: */