/* * 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. */ package org.apache.sanselan.formats.tiff; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.sanselan.FormatCompliance; import org.apache.sanselan.ImageFormat; import org.apache.sanselan.ImageInfo; import org.apache.sanselan.ImageParser; import org.apache.sanselan.ImageReadException; import org.apache.sanselan.ImageWriteException; import org.apache.sanselan.common.IImageMetadata; import org.apache.sanselan.common.byteSources.ByteSource; import org.apache.sanselan.formats.tiff.constants.TiffConstants; import org.apache.sanselan.formats.tiff.datareaders.DataReader; import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreter; import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterBiLevel; import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterCIELAB; import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterCMYK; import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterLogLUV; import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterPalette; import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterRGB; import org.apache.sanselan.formats.tiff.photometricinterpreters.PhotometricInterpreterYCbCr; import org.apache.sanselan.formats.tiff.write.TiffImageWriterLossy; import com.google.code.appengine.awt.Dimension; import com.google.code.appengine.awt.image.BufferedImage; public class TiffImageParser extends ImageParser implements TiffConstants { public TiffImageParser() { // setDebug(true); } public String getName() { return "Tiff-Custom"; } public String getDefaultExtension() { return DEFAULT_EXTENSION; } private static final String DEFAULT_EXTENSION = ".tif"; private static final String ACCEPTED_EXTENSIONS[] = { ".tif", ".tiff", }; protected String[] getAcceptedExtensions() { return ACCEPTED_EXTENSIONS; } protected ImageFormat[] getAcceptedTypes() { return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_TIFF, // }; } public byte[] getICCProfileBytes(ByteSource byteSource, Map params) throws ImageReadException, IOException { FormatCompliance formatCompliance = FormatCompliance.getDefault(); TiffContents contents = new TiffReader(isStrict(params)) .readFirstDirectory(byteSource, params, false, formatCompliance); TiffDirectory directory = (TiffDirectory) contents.directories.get(0); TiffField field = directory.findField(EXIF_TAG_ICC_PROFILE); if (null == field) return null; return field.oversizeValue; } public Dimension getImageSize(ByteSource byteSource, Map params) throws ImageReadException, IOException { FormatCompliance formatCompliance = FormatCompliance.getDefault(); TiffContents contents = new TiffReader(isStrict(params)) .readFirstDirectory(byteSource, params, false, formatCompliance); TiffDirectory directory = (TiffDirectory) contents.directories.get(0); int width = directory.findField(TIFF_TAG_IMAGE_WIDTH).getIntValue(); int height = directory.findField(TIFF_TAG_IMAGE_LENGTH).getIntValue(); return new Dimension(width, height); } public byte[] embedICCProfile(byte image[], byte profile[]) { return null; } public boolean embedICCProfile(File src, File dst, byte profile[]) { return false; } public IImageMetadata getMetadata(ByteSource byteSource, Map params) throws ImageReadException, IOException { FormatCompliance formatCompliance = FormatCompliance.getDefault(); TiffContents contents = new TiffReader(isStrict(params)).readContents( byteSource, params, formatCompliance); ArrayList directories = contents.directories; TiffImageMetadata result = new TiffImageMetadata(contents); for (int i = 0; i < directories.size(); i++) { TiffDirectory dir = (TiffDirectory) directories.get(i); TiffImageMetadata.Directory metadataDirectory = new TiffImageMetadata.Directory( dir); ArrayList entries = dir.getDirectoryEntrys(); for (int j = 0; j < entries.size(); j++) { TiffField entry = (TiffField) entries.get(j); metadataDirectory.add(entry); } result.add(metadataDirectory); } return result; } public ImageInfo getImageInfo(ByteSource byteSource, Map params) throws ImageReadException, IOException { FormatCompliance formatCompliance = FormatCompliance.getDefault(); TiffContents contents = new TiffReader(isStrict(params)) .readDirectories(byteSource, false, formatCompliance); TiffDirectory directory = (TiffDirectory) contents.directories.get(0); TiffField widthField = directory.findField(TIFF_TAG_IMAGE_WIDTH, true); TiffField heightField = directory .findField(TIFF_TAG_IMAGE_LENGTH, true); if ((widthField == null) || (heightField == null)) throw new ImageReadException("TIFF image missing size info."); int height = heightField.getIntValue(); int width = widthField.getIntValue(); // ------------------- TiffField resolutionUnitField = directory .findField(TIFF_TAG_RESOLUTION_UNIT); int resolutionUnit = 2; // Inch if ((resolutionUnitField != null) && (resolutionUnitField.getValue() != null)) resolutionUnit = resolutionUnitField.getIntValue(); double unitsPerInch = -1; switch (resolutionUnit) { case 1: break; case 2: // Inch unitsPerInch = 1.0; break; case 3: // Meter unitsPerInch = 0.0254; break; default: break; } TiffField xResolutionField = directory.findField(TIFF_TAG_XRESOLUTION); TiffField yResolutionField = directory.findField(TIFF_TAG_YRESOLUTION); int physicalWidthDpi = -1; float physicalWidthInch = -1; int physicalHeightDpi = -1; float physicalHeightInch = -1; if (unitsPerInch > 0) { if ((xResolutionField != null) && (xResolutionField.getValue() != null)) { double XResolutionPixelsPerUnit = xResolutionField .getDoubleValue(); physicalWidthDpi = (int) (XResolutionPixelsPerUnit / unitsPerInch); physicalWidthInch = (float) (width / (XResolutionPixelsPerUnit * unitsPerInch)); } if ((yResolutionField != null) && (yResolutionField.getValue() != null)) { double YResolutionPixelsPerUnit = yResolutionField .getDoubleValue(); physicalHeightDpi = (int) (YResolutionPixelsPerUnit / unitsPerInch); physicalHeightInch = (float) (height / (YResolutionPixelsPerUnit * unitsPerInch)); } } // ------------------- TiffField bitsPerSampleField = directory .findField(TIFF_TAG_BITS_PER_SAMPLE); int bitsPerSample = -1; if ((bitsPerSampleField != null) && (bitsPerSampleField.getValue() != null)) bitsPerSample = bitsPerSampleField.getIntValueOrArraySum(); int bitsPerPixel = bitsPerSample; // assume grayscale; // dunno if this handles colormapped images correctly. // ------------------- ArrayList comments = new ArrayList(); ArrayList entries = directory.entries; for (int i = 0; i < entries.size(); i++) { TiffField field = (TiffField) entries.get(i); String comment = field.toString(); comments.add(comment); } ImageFormat format = ImageFormat.IMAGE_FORMAT_TIFF; String formatName = "TIFF Tag-based Image File Format"; String mimeType = "image/tiff"; int numberOfImages = contents.directories.size(); // not accurate ... only reflects first boolean isProgressive = false; // is TIFF ever interlaced/progressive? String formatDetails = "Tiff v." + contents.header.tiffVersion; boolean isTransparent = false; // TODO: wrong boolean usesPalette = false; TiffField colorMapField = directory.findField(TIFF_TAG_COLOR_MAP); if (colorMapField != null) usesPalette = true; int colorType = ImageInfo.COLOR_TYPE_RGB; int compression = directory.findField(TIFF_TAG_COMPRESSION) .getIntValue(); String compressionAlgorithm; switch (compression) { case TIFF_COMPRESSION_UNCOMPRESSED_1: compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_NONE; break; case TIFF_COMPRESSION_CCITT_1D: compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_CCITT_1D; break; case TIFF_COMPRESSION_CCITT_GROUP_3: compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_CCITT_GROUP_3; break; case TIFF_COMPRESSION_CCITT_GROUP_4: compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_CCITT_GROUP_4; break; case TIFF_COMPRESSION_LZW: compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_LZW; break; case TIFF_COMPRESSION_JPEG: compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_JPEG; break; case TIFF_COMPRESSION_UNCOMPRESSED_2: compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_NONE; break; case TIFF_COMPRESSION_PACKBITS: compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_PACKBITS; break; default: compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_UNKNOWN; break; } ImageInfo result = new ImageInfo(formatDetails, bitsPerPixel, comments, format, formatName, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch, physicalWidthDpi, physicalWidthInch, width, isProgressive, isTransparent, usesPalette, colorType, compressionAlgorithm); return result; } public String getXmpXml(ByteSource byteSource, Map params) throws ImageReadException, IOException { FormatCompliance formatCompliance = FormatCompliance.getDefault(); TiffContents contents = new TiffReader(isStrict(params)) .readDirectories(byteSource, false, formatCompliance); TiffDirectory directory = (TiffDirectory) contents.directories.get(0); TiffField xmpField = directory.findField(TIFF_TAG_XMP, false); if (xmpField == null) return null; byte bytes[] = xmpField.getByteArrayValue(); try { // segment data is UTF-8 encoded xml. String xml = new String(bytes, "utf-8"); return xml; } catch (UnsupportedEncodingException e) { throw new ImageReadException("Invalid JPEG XMP Segment."); } } public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource) throws ImageReadException, IOException { try { pw.println("tiff.dumpImageFile"); { ImageInfo imageData = getImageInfo(byteSource); if (imageData == null) return false; imageData.toString(pw, ""); } pw.println(""); // try { FormatCompliance formatCompliance = FormatCompliance .getDefault(); Map params = null; TiffContents contents = new TiffReader(true).readContents( byteSource, params, formatCompliance); ArrayList directories = contents.directories; if (directories == null) return false; for (int d = 0; d < directories.size(); d++) { TiffDirectory directory = (TiffDirectory) directories .get(d); ArrayList entries = directory.entries; if (entries == null) return false; // Debug.debug("directory offset", directory.offset); for (int i = 0; i < entries.size(); i++) { TiffField field = (TiffField) entries.get(i); field.dump(pw, d + ""); } } pw.println(""); } // catch (Exception e) // { // Debug.debug(e); // pw.println(""); // return false; // } return true; } finally { pw.println(""); } } public FormatCompliance getFormatCompliance(ByteSource byteSource) throws ImageReadException, IOException { FormatCompliance formatCompliance = FormatCompliance.getDefault(); Map params = null; new TiffReader(isStrict(params)).readContents(byteSource, params, formatCompliance); return formatCompliance; } public List collectRawImageData(ByteSource byteSource, Map params) throws ImageReadException, IOException { FormatCompliance formatCompliance = FormatCompliance.getDefault(); TiffContents contents = new TiffReader(isStrict(params)) .readDirectories(byteSource, true, formatCompliance); List result = new ArrayList(); for (int i = 0; i < contents.directories.size(); i++) { TiffDirectory directory = (TiffDirectory) contents.directories .get(i); List dataElements = directory.getTiffRawImageDataElements(); for (int j = 0; j < dataElements.size(); j++) { TiffDirectory.ImageDataElement element = (TiffDirectory.ImageDataElement) dataElements .get(j); byte bytes[] = byteSource.getBlock(element.offset, element.length); result.add(bytes); } } return result; } public BufferedImage getBufferedImage(ByteSource byteSource, Map params) throws ImageReadException, IOException { FormatCompliance formatCompliance = FormatCompliance.getDefault(); TiffContents contents = new TiffReader(isStrict(params)) .readFirstDirectory(byteSource, params, true, formatCompliance); TiffDirectory directory = (TiffDirectory) contents.directories.get(0); BufferedImage result = directory.getTiffImage(params); if (null == result) throw new ImageReadException("TIFF does not contain an image."); return result; } protected BufferedImage getBufferedImage(TiffDirectory directory, Map params) throws ImageReadException, IOException { ArrayList entries = directory.entries; if (entries == null) throw new ImageReadException("TIFF missing entries"); int photometricInterpretation = directory.findField( TIFF_TAG_PHOTOMETRIC_INTERPRETATION, true).getIntValue(); int compression = directory.findField(TIFF_TAG_COMPRESSION, true) .getIntValue(); int width = directory.findField(TIFF_TAG_IMAGE_WIDTH, true) .getIntValue(); int height = directory.findField(TIFF_TAG_IMAGE_LENGTH, true) .getIntValue(); int samplesPerPixel = directory.findField(TIFF_TAG_SAMPLES_PER_PIXEL, true).getIntValue(); int bitsPerSample[] = directory.findField(TIFF_TAG_BITS_PER_SAMPLE, true).getIntArrayValue(); // TODO: why are we using bits per sample twice? because one is a sum. int bitsPerPixel = directory.findField(TIFF_TAG_BITS_PER_SAMPLE, true) .getIntValueOrArraySum(); // int bitsPerPixel = getTagAsValueOrArraySum(entries, // TIFF_TAG_BITS_PER_SAMPLE); int predictor = -1; { // dumpOptionalNumberTag(entries, TIFF_TAG_FILL_ORDER); // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_BYTE_COUNTS); // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_OFFSETS); // dumpOptionalNumberTag(entries, TIFF_TAG_ORIENTATION); // dumpOptionalNumberTag(entries, TIFF_TAG_PLANAR_CONFIGURATION); TiffField predictorField = directory.findField(TIFF_TAG_PREDICTOR); if (null != predictorField) predictor = predictorField.getIntValueOrArraySum(); } if (samplesPerPixel != bitsPerSample.length) throw new ImageReadException("Tiff: samplesPerPixel (" + samplesPerPixel + ")!=fBitsPerSample.length (" + bitsPerSample.length + ")"); boolean hasAlpha = false; BufferedImage result = getBufferedImageFactory(params) .getColorBufferedImage(width, height, hasAlpha); PhotometricInterpreter photometricInterpreter = getPhotometricInterpreter( directory, photometricInterpretation, bitsPerPixel, bitsPerSample, predictor, samplesPerPixel, width, height); TiffImageData imageData = directory.getTiffImageData(); DataReader dataReader = imageData.getDataReader(entries, photometricInterpreter, bitsPerPixel, bitsPerSample, predictor, samplesPerPixel, width, height, compression); dataReader.readImageData(result); photometricInterpreter.dumpstats(); return result; } private PhotometricInterpreter getPhotometricInterpreter( TiffDirectory directory, int photometricInterpretation, int bitsPerPixel, int bitsPerSample[], int predictor, int samplesPerPixel, int width, int height) throws IOException, ImageReadException { switch (photometricInterpretation) { case 0: case 1: boolean invert = photometricInterpretation == 0; return new PhotometricInterpreterBiLevel(bitsPerPixel, samplesPerPixel, bitsPerSample, predictor, width, height, invert); case 3: // Palette { int colorMap[] = directory.findField(TIFF_TAG_COLOR_MAP, true) .getIntArrayValue(); int expected_colormap_size = 3 * (1 << bitsPerPixel); if (colorMap.length != expected_colormap_size) throw new ImageReadException("Tiff: fColorMap.length (" + colorMap.length + ")!=expected_colormap_size (" + expected_colormap_size + ")"); return new PhotometricInterpreterPalette(samplesPerPixel, bitsPerSample, predictor, width, height, colorMap); } case 2: // RGB return new PhotometricInterpreterRGB(samplesPerPixel, bitsPerSample, predictor, width, height); case 5: // CMYK return new PhotometricInterpreterCMYK(samplesPerPixel, bitsPerSample, predictor, width, height); case 6: // { double yCbCrCoefficients[] = directory.findField( TIFF_TAG_YCBCR_COEFFICIENTS, true).getDoubleArrayValue(); int yCbCrPositioning[] = directory.findField( TIFF_TAG_YCBCR_POSITIONING, true).getIntArrayValue(); int yCbCrSubSampling[] = directory.findField( TIFF_TAG_YCBCR_SUB_SAMPLING, true).getIntArrayValue(); double referenceBlackWhite[] = directory.findField( TIFF_TAG_REFERENCE_BLACK_WHITE, true).getDoubleArrayValue(); return new PhotometricInterpreterYCbCr(yCbCrCoefficients, yCbCrPositioning, yCbCrSubSampling, referenceBlackWhite, samplesPerPixel, bitsPerSample, predictor, width, height); } case 8: return new PhotometricInterpreterCIELAB(samplesPerPixel, bitsPerSample, predictor, width, height); case 32844: case 32845: { boolean yonly = (photometricInterpretation == 32844); return new PhotometricInterpreterLogLUV(samplesPerPixel, bitsPerSample, predictor, width, height, yonly); } default: throw new ImageReadException( "TIFF: Unknown fPhotometricInterpretation: " + photometricInterpretation); } } public void writeImage(BufferedImage src, OutputStream os, Map params) throws ImageWriteException, IOException { new TiffImageWriterLossy().writeImage(src, os, params); } }