skip to Main Content

I’m interesting in creating a layered tif with Java in a way that Photoshop will recognize the layers. I was able to create a multi-page tif, but Photoshop does not recognize the pages as layers. The pages are viewable with Acrobat though. Anyone know how Photoshop stores tif layer data and how that could be generated with Java?

Thanks.

2

Answers


  1. I have researched this for my TIFF ImageIO plugin, and as far as I understand, the way Photoshop stores layer information in TIFFs is completely proprietary and not using standard TIFF mechanisms, like multi-page documents utilizing linked or nested IFDs (330/SubIFD), or file types (254/NewSubFileType), etc.

    Instead, it stores the layer information,
    along with the layer image data, in a Photoshop specific TIFF tag; 37724/ImageSourceData, which has type UNDEFINED (or “just bytes”). Luckily, the contents of this tag is documented in Adobe Photoshop®
    TIFF Technical Notes
    .

    The content of this tag will always start with the 0-terminated string "Adobe Photoshop Document Data Block". The rest of the contents is various Photoshop resources, identified by the Photoshop 4 byte resource identifier 8BIM, followed 4 bytes resource key and 4 bytes length for each individual resource.

    The interesting resource in this block, with regards to Photoshop layers, is the one identified with the resource key Layr. This is the same structure documented in Layer and Mask Information Section in the Photoshop File Format.

    There’s also a different tag, 34377/Photoshop, which contains other image resources read and written by Photoshop. It’s also documented in the Image Resources Section of the above document. It does contain some information which is interesting in regards to layers, but I’m not sure how much of this you need to write. You will probably need a Photoshop installation and test using the “real thing”.

    I do have code to read both of these structures in the PSD ImageIO plugin, which might be worth looking at, but it doesn’t yet support writing.

    When you can write the contents Photoshop TIFF tags, you should be able to pass it to the TIFFImageWriter as part of the TIFF IIOMetadata and the writer will write it along with any other metadata and pixel data you pass.


    So, as you see, this is all (mostly) documented and for sure doable in Java, but still not completely trivial.

    Login or Signup to reply.
  2. I started a solution based on TinyTIFF, the answer from @haraldK on this SO question, the TIFF spec, and the Photoshop TIFF spec. It is about the simplest possible way to write a TIFF. I put in the code to write the Photoshop section, but it is not finished.

    Note that Photoshop uses the TIFF image as the “preview” image, similar to the flattened composite image at the very end of a PSD file. The Photoshop TIFF section is what contains the pixel data for all the layers (again similar to a PSD). Adobe’s use of TIFF in this way is pretty dirty. You might as well just use the (also terrible) PSD format, since smashing PSD data into the TIFF format just adds complexity for no benefit. This is why I did not finish the code below. If you do finish it, please post it here.

    The Output class is from Kryo. pixmap.getPixels() is 4 bytes per pixel, RGBA.

    /* Copyright (c) 2008-2015 Jan W. Krieger (<[email protected]>, <[email protected]>), German Cancer Research Center (DKFZ) & IWR, University of Heidelberg
     * Copyright (c) 2018, Nathan Sweet, Esoteric Software LLC
     * All rights reserved.
     * 
     * This software is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
     * License (LGPL) as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later
     * version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
     * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You
     * should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */
    
    public class TiffWriter {
        private Output out;
        private int width, height;
    
        private int ifdCount, ifdLastOffset, ifdData, headerStart;
        private Output header;
    
        public void start (OutputStream output, int width, int height) throws IOException {
            this.out = new Output(output);
            this.width = width;
            this.height = height;
    
            out.writeByte('M'); // Big endian.
            out.writeByte('M');
            out.writeShort(42); // Magic number.
            ifdLastOffset = out.total();
            out.writeInt(8); // Offset of first IFD.
        }
    
        public void frame (Pixmap pixmap, String name, int frame, int endFrame) throws IOException {
            ByteBuffer pixels = pixmap.getPixels();
    
            headerStart = out.total();
            ifdData = 2 + TIFF_HEADER_MAX_ENTRIES * 12;
            ifdCount = 0;
            header = new Output(TIFF_HEADER_SIZE + 2);
            header.setPosition(2);
    
            writeLongIFD(TIFF_FIELD_IMAGEWIDTH, width);
            writeLongIFD(TIFF_FIELD_IMAGELENGTH, height);
            writeShortIFD(TIFF_FIELD_BITSPERSAMPLE, 8, 8, 8);
            writeShortIFD(TIFF_FIELD_COMPRESSION, COMPRESSION_NO);
            writeShortIFD(TIFF_FIELD_PHOTOMETRICINTERPRETATION, PHOTOMETRIC_INTERPRETATION_RGB);
            writeLongIFD(TIFF_FIELD_STRIPOFFSETS, headerStart + 2 + TIFF_HEADER_SIZE);
            writeShortIFD(TIFF_FIELD_SAMPLESPERPIXEL, 4);
            writeLongIFD(TIFF_FIELD_ROWSPERSTRIP, height);
            writeLongIFD(TIFF_FIELD_STRIPBYTECOUNTS, width * height);
            writeRationalIFD(TIFF_FIELD_XRESOLUTION, 720000, 10000);
            writeRationalIFD(TIFF_FIELD_YRESOLUTION, 720000, 10000);
            writeShortIFD(TIFF_FIELD_PLANARCONFIG, PLANAR_CONFIGURATION_CHUNKY);
            writeShortIFD(TIFF_FIELD_RESOLUTIONUNIT, RESOLUTION_UNIT_INCH);
            writeShortIFD(TIFF_FIELD_EXTRASAMPLES, 1); // Adds alpha to last samples per pixel.
            // writeIFDEntrySHORT(TIFF_FIELD_SAMPLEFORMAT, SAMPLE_FORMAT_FLOAT);
    
            // Photoshop layer entry.
            ifdCount++;
            header.writeShort(TIFF_FIELD_PHOTOSHOP_IMAGESOURCEDATA);
            header.writeShort(TIFF_TYPE_UNDEFINED);
            int sizePosition = header.position();
            header.writeInt(0); // Size in bytes.
            header.writeInt(ifdData + headerStart);
            int pos = header.position();
            header.setPosition(ifdData);
            writeString(header, "Adobe Photoshop Document Data Block");
            // Unfinished!
            int size = header.position() - ifdData;
            ifdData = header.position();
            header.setPosition(sizePosition);
            header.writeInt(size);
            header.setPosition(pos);
    
            if (ifdCount > TIFF_HEADER_MAX_ENTRIES) throw new RuntimeException();
    
            header.setPosition(0);
            header.writeShort(ifdCount);
    
            header.setPosition(2 + ifdCount * 12); // header start + 12 bytes per IFD entry
            header.writeInt(headerStart + 2 + TIFF_HEADER_SIZE + width * height);
    
            out.writeBytes(header.getBuffer(), 0, TIFF_HEADER_SIZE + 2);
    
            ifdLastOffset = headerStart + 2 + ifdCount * 12;
    
            pixels.position(0);
            for (int i = 0, n = width * height * 4; i < n; i += 4) {
                byte a = pixels.get(i + 3);
                float pma = (a & 0xff) / 255f;
                out.writeByte((byte)((pixels.get(i) & 0xff) * pma));
                out.writeByte((byte)((pixels.get(i + 1) & 0xff) * pma));
                out.writeByte((byte)((pixels.get(i + 2) & 0xff) * pma));
                out.writeByte(a);
            }
            pixels.position(0);
        }
    
        public void end () throws IOException {
            out.close();
    
            // Erase last IFD offset.
            RandomAccessFile file = new RandomAccessFile("test.tif", "rw");
            file.seek(ifdLastOffset);
            file.write((byte)0);
            file.write((byte)0);
            file.write((byte)0);
            file.write((byte)0);
            file.close();
        }
    
        public void close () throws IOException {
            end();
        }
    
        private void writeString (Output output, String value) {
            for (int i = 0, n = value.length(); i < n; i++)
                output.writeByte(value.charAt(i));
            output.writeByte(0);
        }
    
        private void writeLongIFD (int tag, int data) {
            ifdCount++;
            header.writeShort(tag);
            header.writeShort(TIFF_TYPE_LONG);
            header.writeInt(1);
            header.writeInt(data);
        }
    
        private void writeShortIFD (int tag, int data) {
            ifdCount++;
            header.writeShort(tag);
            header.writeShort(TIFF_TYPE_SHORT);
            header.writeInt(1);
            header.writeShort(data);
            header.writeShort(0); // Pad bytes.
        }
    
        private void writeShortIFD (int tag, int... data) {
            ifdCount++;
            header.writeShort(tag);
            header.writeShort(TIFF_TYPE_SHORT);
            header.writeInt(data.length);
            if (data.length == 1)
                header.writeInt(data[0]);
            else {
                header.writeInt(ifdData + headerStart);
                int pos = header.position();
                header.setPosition(ifdData);
                for (int value : data)
                    header.writeShort(value);
                ifdData = header.position();
                header.setPosition(pos);
            }
        }
    
        private void writeRationalIFD (int tag, int numerator, int denominator) {
            ifdCount++;
            header.writeShort(tag);
            header.writeShort(TIFF_TYPE_RATIONAL);
            header.writeInt(1);
            header.writeInt(ifdData + headerStart);
            int pos = header.position();
            header.setPosition(ifdData);
            header.writeInt(numerator);
            header.writeInt(denominator);
            ifdData = header.position();
            header.setPosition(pos);
        }
    
        static private final int TIFF_HEADER_SIZE = 510;
        static private final int TIFF_HEADER_MAX_ENTRIES = 16;
    
        static private final int TIFF_FIELD_IMAGEWIDTH = 256;
        static private final int TIFF_FIELD_IMAGELENGTH = 257;
        static private final int TIFF_FIELD_BITSPERSAMPLE = 258;
        static private final int TIFF_FIELD_COMPRESSION = 259;
        static private final int TIFF_FIELD_PHOTOMETRICINTERPRETATION = 262;
        static private final int TIFF_FIELD_IMAGEDESCRIPTION = 270;
        static private final int TIFF_FIELD_STRIPOFFSETS = 273;
        static private final int TIFF_FIELD_SAMPLESPERPIXEL = 277;
        static private final int TIFF_FIELD_ROWSPERSTRIP = 278;
        static private final int TIFF_FIELD_STRIPBYTECOUNTS = 279;
        static private final int TIFF_FIELD_XRESOLUTION = 282;
        static private final int TIFF_FIELD_YRESOLUTION = 283;
        static private final int TIFF_FIELD_PLANARCONFIG = 284;
        static private final int TIFF_FIELD_RESOLUTIONUNIT = 296;
        static private final int TIFF_FIELD_EXTRASAMPLES = 338;
        static private final int TIFF_FIELD_SAMPLEFORMAT = 339;
        static private final int TIFF_FIELD_PHOTOSHOP_IMAGESOURCEDATA = 37724;
    
        static private final int TIFF_TYPE_BYTE = 1;
        static private final int TIFF_TYPE_ASCII = 2;
        static private final int TIFF_TYPE_SHORT = 3;
        static private final int TIFF_TYPE_LONG = 4;
        static private final int TIFF_TYPE_RATIONAL = 5;
        static private final int TIFF_TYPE_UNDEFINED = 7;
    
        static private final int SAMPLE_FORMAT_UNSIGNED_INT = 1;
        static private final int SAMPLE_FORMAT_SIGNED_INT = 2;
        static private final int SAMPLE_FORMAT_FLOAT = 3;
        static private final int SAMPLE_FORMAT_UNDEFINED = 4;
    
        static private final int COMPRESSION_NO = 1;
        static private final int COMPRESSION_CCITT_HUFFMAN = 2;
        static private final int COMPRESSION_T4 = 3;
        static private final int COMPRESSION_T6 = 4;
        static private final int COMPRESSION_LZW = 5;
        static private final int COMPRESSION_JPEG_OLD = 6;
        static private final int COMPRESSION_JPEG_NEW = 7;
        static private final int COMPRESSION_DEFLATE = 8;
    
        static private final int PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO = 0;
        static private final int PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO = 1;
        static private final int PHOTOMETRIC_INTERPRETATION_RGB = 2;
        static private final int PHOTOMETRIC_INTERPRETATION_PALETTE = 3;
        static private final int PHOTOMETRIC_INTERPRETATION_TRANSPARENCY = 4;
    
        static private final int PLANAR_CONFIGURATION_CHUNKY = 1;
        static private final int PLANAR_CONFIGURATION_PLANAR = 2;
    
        static private final int RESOLUTION_UNIT_NO = 1;
        static private final int RESOLUTION_UNIT_INCH = 2;
        static private final int RESOLUTION_UNIT_CENTIMETER = 3;
    
        static public void main (String[] args) throws Exception {
            FileOutputStream output = new FileOutputStream("test.tif");
            TiffWriter writer = new TiffWriter();
            writer.start(output, imageWidth, imageHeight);
            for (int i = 0; i < 16; i++) {
                Pixmap pixmap = new Pixmap(...);
                writer.frame(pixmap, "run", i, 16);
            }
            writer.end();
            writer.close();
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search