Создайте Layered Tif с помощью Java для использования в Photoshop

Мне интересно создать многослойный tif с помощью Java таким образом, чтобы Photoshop распознавал слои. Мне удалось создать многостраничный tif, но Photoshop не распознает страницы как слои. Однако страницы доступны для просмотра в Acrobat. Кто-нибудь знает, как Photoshop хранит данные слоя tif и как их можно сгенерировать с помощью Java?

Спасибо.


person jonD02    schedule 12.12.2016    source источник


Ответы (2)


Я исследовал это для своего плагина TIFF ImageIO, и, насколько я понимаю, способ, которым Photoshop хранит информацию о слоях в TIFF, является полностью проприетарным и не использует стандартные механизмы TIFF, такие как многостраничные документы, использующие связанные или вложенные IFD (330/SubIFD), или типы файлов (254/NewSubFileType) и т. д.

Вместо этого он сохраняет информацию о слое вместе с данными изображения слоя в Специальный тег TIFF для Photoshop; 37724/ImageSourceData, который имеет тип UNDEFINED (или "просто байты"). К счастью, содержимое этого тега задокументировано в Технических примечаниях к Adobe Photoshop® TIFF.

Содержимое этого тега всегда будет начинаться со строки "Adobe Photoshop Document Data Block", заканчивающейся 0. Остальное содержимое представляет собой различные ресурсы Photoshop, идентифицируемые 4-байтовым идентификатором ресурса Photoshop 8BIM, за которым следует 4-байтовый ключ ресурса и 4-байтовая длина для каждого отдельного ресурса.

Интересный ресурс в этом блоке, относящийся к слоям Photoshop, идентифицируется с ключом ресурса Layr. Это та же самая структура, которая описана в разделе информации о слоях и масках в формате файла Photoshop.

Также есть другой тег, 34377/Photoshop, который содержит другие ресурсы изображений, считываемые и записываемые Photoshop. Это также описано в разделе графических ресурсов вышеуказанного документа. Он содержит некоторую интересную информацию о слоях, но я не уверен, сколько из этого вам нужно написать. Вам, вероятно, понадобится установка Photoshop и тестирование с использованием «настоящей вещи».

У меня есть код для чтения обеих этих структур в плагин PSD ImageIO, на который стоит обратить внимание, но он пока не поддерживает запись.

Когда вы сможете записать содержимое в теги Photoshop TIFF, вы сможете передать его в TIFFImageWriter как часть TIFF IIOMetadata, и автор запишет его вместе с любыми другими переданными вами метаданными и данными пикселей.


Итак, как вы видите, все это (в основном) задокументировано и наверняка выполнимо на Java, но все же не совсем тривиально.

person Harald K    schedule 13.12.2016
comment
Спасибо за исследование, подробности и ссылки. Обеспечивает отличную основу для начала процесса реализации. - person jonD02; 15.12.2016
comment
@ jonD02 Есть ли шанс, что вы это реализовали? - person NateS; 01.08.2018

Я начал решение на основе TinyTIFF, ответ от @haraldK на этот SO вопрос, спецификация TIFF и Photoshop TIFF спец.. Речь идет о простейшем способе записи TIFF. Я добавил код для написания раздела Photoshop, но он не закончен.

Обратите внимание, что Photoshop использует изображение в формате TIFF в качестве изображения «предварительного просмотра», подобно сглаженному составному изображению в самом конце PSD-файла. Раздел Photoshop TIFF — это то, что содержит данные о пикселях для всех слоев (опять же, аналогично PSD). Использование Adobe TIFF таким образом довольно грязно. С тем же успехом вы могли бы просто использовать (тоже ужасный) формат PSD, поскольку преобразование данных PSD в формат TIFF только усложняет работу без какой-либо пользы. Вот почему я не закончил код ниже. Если закончишь, выложи сюда.

Класс Output взят из Kryo. pixmap.getPixels() составляет 4 байта на пиксель, 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();
    }
}
person NateS    schedule 02.08.2018