最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

APNG Generation with Java - Stack Overflow

programmeradmin0浏览0评论

I'm trying to create an Animated Portable Network Graphics (APNG) using Java, but unfortunately, all of my attempts so far have been unsuccessful.

I couldn't find any Java libraries that support APNG creation, so I decided to look for some example code online. After some searching, I came across a code snippet that seemed to offer a solution. However, I'm still facing issues and would appreciate any guidance or help from the community.

Here’s the Java code I’m working with:

public static void createAPNGAnimation(ArrayList<BufferedImage> images, int delayMs, boolean infinite, FileOutputStream output) throws IOException {
    // APNG signature (same as PNG)
    byte[] pngSignature = new byte[]{(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
    output.write(pngSignature);

    // Write IHDR chunk (Image Header)
    byte[] ihdr = createIHDRChunk(images.get(0));
    output.write(ihdr);

    // Write acTL chunk (Animation Control)
    byte[] acTL = createACTLChunk(images.size(), infinite ? 0 : 1);
    output.write(acTL);

    // Write frames
    for (int i = 0; i < images.size(); i++) {
        BufferedImage image = images.get(i);

        // Write fcTL chunk (Frame Control)
        byte[] fcTL = createFCTLChunk(i, delayMs, image.getWidth(), image.getHeight());
        output.write(fcTL);

        // Write IDAT or fdAT chunk (Image Data)
        byte[] imageData = createIDATChunk(image);
        if (i == 0) {
            output.write(imageData); // First frame uses IDAT
        } else {
            byte[] fdAT = createFDATChunk(i, imageData);
            output.write(fdAT); // Subsequent frames use fdAT
        }
    }

    // Write IEND chunk (Image End)
    byte[] iend = createIENDChunk();
    output.write(iend);

    System.out.println("APNG animation created successfully!");
}

private static byte[] createIHDRChunk(BufferedImage image) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(13)); // IHDR data length
    baos.write("IHDR".getBytes()); // Chunk type
    baos.write(intToBytes(image.getWidth())); // Width
    baos.write(intToBytes(image.getHeight())); // Height
    baos.write(8); // Bit depth
    baos.write(6); // Color type (RGBA)
    baos.write(0); // Compression method
    baos.write(0); // Filter method
    baos.write(0); // Interlace method
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

private static byte[] createACTLChunk(int numFrames, int numPlays) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(8)); // acTL data length
    baos.write("acTL".getBytes()); // Chunk type
    baos.write(intToBytes(numFrames)); // Number of frames
    baos.write(intToBytes(numPlays)); // Number of plays (0 = infinite)
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

private static byte[] createFCTLChunk(int sequenceNumber, int delayMs, int width, int height) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(26)); // fcTL data length
    baos.write("fcTL".getBytes()); // Chunk type
    baos.write(intToBytes(sequenceNumber)); // Sequence number
    baos.write(intToBytes(width)); // Width
    baos.write(intToBytes(height)); // Height
    baos.write(intToBytes(delayMs)); // Delay numerator
    baos.write(intToBytes(1000)); // Delay denominator
    baos.write(0); // Dispose op
    baos.write(0); // Blend op
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

private static byte[] createIDATChunk(BufferedImage image) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ImageIO.write(image, "png", baos);
    byte[] pngData = baos.toByteArray();

    // Extract IDAT chunk from PNG data
    int idatStart = findChunk(pngData, "IDAT");
    if (idatStart == -1) {
        throw new IOException("IDAT chunk not found in PNG data");
    }

    // Extract the IDAT chunk (length + type + data + CRC)
    int length = ByteBuffer.wrap(pngData, idatStart - 4, 4).order(ByteOrder.BIG_ENDIAN).getInt();
    byte[] idatChunk = new byte[length + 12]; // 4 (length) + 4 (type) + length (data) + 4 (CRC)
    System.arraycopy(pngData, idatStart - 4, idatChunk, 0, idatChunk.length);

    return idatChunk;
}

private static byte[] createFDATChunk(int sequenceNumber, byte[] imageData) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(imageData.length - 8)); // fdAT data length (excluding sequence number)
    baos.write("fdAT".getBytes()); // Chunk type
    baos.write(intToBytes(sequenceNumber)); // Sequence number
    baos.write(imageData, 8, imageData.length - 8); // Image data (excluding length and type)
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

private static byte[] createIENDChunk() throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(0)); // IEND data length
    baos.write("IEND".getBytes()); // Chunk type
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

private static byte[] intToBytes(int value) {
    return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array();
}

private static int crc32(byte[] data, int offset, int length) {
    CRC32 crc = new CRC32();
    crc.update(data, offset, length);
    return (int) crc.getValue();
}

private static int findChunk(byte[] pngData, String chunkType) {
    byte[] typeBytes = chunkType.getBytes();
    for (int i = 8; i < pngData.length - 4; i++) {
        if (pngData[i] == typeBytes[0] &&
                pngData[i + 1] == typeBytes[1] &&
                pngData[i + 2] == typeBytes[2] &&
                pngData[i + 3] == typeBytes[3]) {
            return i; // Return the start of the chunk type
        }
    }
    return -1; // Chunk not found
}

I'm using this code to generate an APNG animation, and while the process seems to be working for creating the various chunks (IHDR, acTL, fcTL, IDAT, etc.), I am still encountering problems with the output, which doesn't seem to display the animation as expected.

Could anyone spot potential issues in this code or suggest improvements for making it work properly? Any advice on how to debug or common pitfalls in APNG creation would be greatly appreciated.

I'm trying to create an Animated Portable Network Graphics (APNG) using Java, but unfortunately, all of my attempts so far have been unsuccessful.

I couldn't find any Java libraries that support APNG creation, so I decided to look for some example code online. After some searching, I came across a code snippet that seemed to offer a solution. However, I'm still facing issues and would appreciate any guidance or help from the community.

Here’s the Java code I’m working with:

public static void createAPNGAnimation(ArrayList<BufferedImage> images, int delayMs, boolean infinite, FileOutputStream output) throws IOException {
    // APNG signature (same as PNG)
    byte[] pngSignature = new byte[]{(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
    output.write(pngSignature);

    // Write IHDR chunk (Image Header)
    byte[] ihdr = createIHDRChunk(images.get(0));
    output.write(ihdr);

    // Write acTL chunk (Animation Control)
    byte[] acTL = createACTLChunk(images.size(), infinite ? 0 : 1);
    output.write(acTL);

    // Write frames
    for (int i = 0; i < images.size(); i++) {
        BufferedImage image = images.get(i);

        // Write fcTL chunk (Frame Control)
        byte[] fcTL = createFCTLChunk(i, delayMs, image.getWidth(), image.getHeight());
        output.write(fcTL);

        // Write IDAT or fdAT chunk (Image Data)
        byte[] imageData = createIDATChunk(image);
        if (i == 0) {
            output.write(imageData); // First frame uses IDAT
        } else {
            byte[] fdAT = createFDATChunk(i, imageData);
            output.write(fdAT); // Subsequent frames use fdAT
        }
    }

    // Write IEND chunk (Image End)
    byte[] iend = createIENDChunk();
    output.write(iend);

    System.out.println("APNG animation created successfully!");
}

private static byte[] createIHDRChunk(BufferedImage image) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(13)); // IHDR data length
    baos.write("IHDR".getBytes()); // Chunk type
    baos.write(intToBytes(image.getWidth())); // Width
    baos.write(intToBytes(image.getHeight())); // Height
    baos.write(8); // Bit depth
    baos.write(6); // Color type (RGBA)
    baos.write(0); // Compression method
    baos.write(0); // Filter method
    baos.write(0); // Interlace method
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

private static byte[] createACTLChunk(int numFrames, int numPlays) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(8)); // acTL data length
    baos.write("acTL".getBytes()); // Chunk type
    baos.write(intToBytes(numFrames)); // Number of frames
    baos.write(intToBytes(numPlays)); // Number of plays (0 = infinite)
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

private static byte[] createFCTLChunk(int sequenceNumber, int delayMs, int width, int height) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(26)); // fcTL data length
    baos.write("fcTL".getBytes()); // Chunk type
    baos.write(intToBytes(sequenceNumber)); // Sequence number
    baos.write(intToBytes(width)); // Width
    baos.write(intToBytes(height)); // Height
    baos.write(intToBytes(delayMs)); // Delay numerator
    baos.write(intToBytes(1000)); // Delay denominator
    baos.write(0); // Dispose op
    baos.write(0); // Blend op
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

private static byte[] createIDATChunk(BufferedImage image) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ImageIO.write(image, "png", baos);
    byte[] pngData = baos.toByteArray();

    // Extract IDAT chunk from PNG data
    int idatStart = findChunk(pngData, "IDAT");
    if (idatStart == -1) {
        throw new IOException("IDAT chunk not found in PNG data");
    }

    // Extract the IDAT chunk (length + type + data + CRC)
    int length = ByteBuffer.wrap(pngData, idatStart - 4, 4).order(ByteOrder.BIG_ENDIAN).getInt();
    byte[] idatChunk = new byte[length + 12]; // 4 (length) + 4 (type) + length (data) + 4 (CRC)
    System.arraycopy(pngData, idatStart - 4, idatChunk, 0, idatChunk.length);

    return idatChunk;
}

private static byte[] createFDATChunk(int sequenceNumber, byte[] imageData) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(imageData.length - 8)); // fdAT data length (excluding sequence number)
    baos.write("fdAT".getBytes()); // Chunk type
    baos.write(intToBytes(sequenceNumber)); // Sequence number
    baos.write(imageData, 8, imageData.length - 8); // Image data (excluding length and type)
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

private static byte[] createIENDChunk() throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(0)); // IEND data length
    baos.write("IEND".getBytes()); // Chunk type
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

private static byte[] intToBytes(int value) {
    return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array();
}

private static int crc32(byte[] data, int offset, int length) {
    CRC32 crc = new CRC32();
    crc.update(data, offset, length);
    return (int) crc.getValue();
}

private static int findChunk(byte[] pngData, String chunkType) {
    byte[] typeBytes = chunkType.getBytes();
    for (int i = 8; i < pngData.length - 4; i++) {
        if (pngData[i] == typeBytes[0] &&
                pngData[i + 1] == typeBytes[1] &&
                pngData[i + 2] == typeBytes[2] &&
                pngData[i + 3] == typeBytes[3]) {
            return i; // Return the start of the chunk type
        }
    }
    return -1; // Chunk not found
}

I'm using this code to generate an APNG animation, and while the process seems to be working for creating the various chunks (IHDR, acTL, fcTL, IDAT, etc.), I am still encountering problems with the output, which doesn't seem to display the animation as expected.

Could anyone spot potential issues in this code or suggest improvements for making it work properly? Any advice on how to debug or common pitfalls in APNG creation would be greatly appreciated.

Share Improve this question edited Mar 2 at 10:42 Basil Bourque 342k123 gold badges936 silver badges1.3k bronze badges asked Mar 2 at 9:01 toplay 4freetoplay 4free 312 bronze badges 5
  • 1 I suggest that you edit your question and post a screen capture of your output and explain why you think that it doesn't seem to display the animation as expected. Also, if possible, post a screen capture showing the output you wish to get. – Abra Commented Mar 2 at 12:03
  • It doesnt load it at all, if I put it in a apng validator it only says "Warning: Corrupted PNG image" (without explaination) – toplay 4free Commented Mar 2 at 12:16
  • The output I would wish to get is the images behind each other animated – toplay 4free Commented Mar 2 at 12:49
  • I suspect the size and CRC of an fdAT chunk need to account for the sequence number. – VGR Commented Mar 6 at 21:36
  • createFCTLChunk has some errors: it does not write a total of 26 bytes. It does not write x_offset or y_offset (32-bit integers which both should be zero), and the delay numerator and denominator need to be 16-bit values, not 32-bit values. See w3./TR/png/#fcTL-chunk. – VGR Commented Mar 7 at 14:21
Add a comment  | 

1 Answer 1

Reset to default 0

Your createFCTLChunk method is correctly writing a size of 26 bytes, but the method does not actually write 26 bytes of header data.

The specification states that an fcTL chunk has this structure:

Name Size
sequence_number 4 bytes
width 4 bytes
height 4 bytes
x_offset 4 bytes
y_offset 4 bytes
delay_num 2 bytes
delay_den 2 bytes
dispose_op 1 byte
blend_op 1 byte

Your createFCTLChunk method is missing x_offset and y_offset values. And it needs to write the delay values as 16 bits, not as 32 bits:

private static byte[] createFCTLChunk(int sequenceNumber, int delayMs, int width, int height) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(intToBytes(26)); // fcTL data length
    baos.write("fcTL".getBytes()); // Chunk type
    baos.write(intToBytes(sequenceNumber)); // Sequence number
    baos.write(intToBytes(width)); // Width
    baos.write(intToBytes(height)); // Height
    baos.write(intToBytes(0)); // x_offset
    baos.write(intToBytes(0)); // y_offset
    baos.write((delayMs >> 8) & 0xff); // Delay numerator
    baos.write(delayMs & 0xff); // Delay numerator
    baos.write((1000 >> 8) & 0xff); // Delay denominator
    baos.write(1000 & 0xff); // Delay denominator
    baos.write(0); // Dispose op
    baos.write(0); // Blend op
    byte[] chunkData = baos.toByteArray();
    baos.write(intToBytes(crc32(chunkData, 4, chunkData.length - 4))); // CRC
    return baos.toByteArray();
}

A secondary problem is that the code is copying IDAT chunks out of PNG data written by ImageIO.write. We don’t know if ImageIO will write a PNG image with the same bit depth, color type, filter method, or interlace method as the APNG we’re currently writing, so the chunk may not be valid data.

I recommend writing the IDAT chunk yourself using a DeflaterOutputStream:

private static byte[] createIDATChunk(BufferedImage image) throws IOException {
    int width = image.getWidth();
    int height = image.getHeight();

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(new byte[4]); // Placeholder for length
    baos.write("IDAT".getBytes());
    try (DeflaterOutputStream deflater = new DeflaterOutputStream(baos)) {
        for (int y = 0; y < height; y++) {
            // filter type 0 (not the same as filter method)
            deflater.write(0);

            for (int x = 0; x < width; x++) {
                int argb = image.getRGB(x, y);
                int alpha = (argb >> 24) & 0xff;
                int red = (argb >> 16) & 0xff;
                int green = (argb >> 8) & 0xff;
                int blue = argb & 0xff;
                deflater.write(red);
                deflater.write(green);
                deflater.write(blue);
                deflater.write(alpha);
            }
        }
    }
    baos.write(new byte[4]); // Placeholder for CRC32

    byte[] chunkData = baos.toByteArray();
    int length = chunkData.length - 12;
    int crc32 = crc32(chunkData, 4, chunkData.length - 8);

    ByteBuffer chunkBuffer = ByteBuffer.wrap(chunkData);
    chunkBuffer.putInt(0, length);
    chunkBuffer.putInt(8 + length, crc32);

    return chunkData;
}

You don’t need to set a ByteBuffer’s order, by the way. From the ByteBuffer documentation:

The initial order of a byte buffer is always BIG_ENDIAN.

This is true on all platforms, regardless of their native byte order.

Finally, it should be noted that using DataOutputStream would make your task easier and would make the code cleaner:

private static byte[] createIHDRChunk(BufferedImage image) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try (DataOutputStream data = new DataOutputStream(baos)) {
        data.writeInt(13); // IHDR data length
        data.writeBytes("IHDR"); // Chunk type
        data.writeInt(image.getWidth()); // Width
        data.writeInt(image.getHeight()); // Height
        data.write(8); // Bit depth
        data.write(6); // Color type (RGBA)
        data.write(0); // Compression method
        data.write(0); // Filter method
        data.write(0); // Interlace method
        data.flush();
        byte[] chunkData = baos.toByteArray();
        data.writeInt(crc32(chunkData, 4, chunkData.length - 4)); // CRC
    }
    return baos.toByteArray();
}

private static byte[] createACTLChunk(int numFrames, int numPlays) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try (DataOutputStream data = new DataOutputStream(baos)) {
        data.writeInt(8); // acTL data length
        data.writeBytes("acTL"); // Chunk type
        data.writeInt(numFrames); // Number of frames
        data.writeInt(numPlays); // Number of plays (0 = infinite)
        data.flush();
        byte[] chunkData = baos.toByteArray();
        data.writeInt(crc32(chunkData, 4, chunkData.length - 4)); // CRC
    }
    return baos.toByteArray();
}

private static byte[] createFCTLChunk(int sequenceNumber, int delayMs, int width, int height) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try (DataOutputStream data = new DataOutputStream(baos)) {
        data.writeInt(26); // fcTL data length
        data.writeBytes("fcTL"); // Chunk type
        data.writeInt(sequenceNumber); // Sequence number
        data.writeInt(width); // Width
        data.writeInt(height); // Height
        data.writeInt(0); // x_offset
        data.writeInt(0); // y_offset
        data.writeShort(delayMs); // Delay numerator
        data.writeShort(1000); // Delay denominator
        data.write(0); // Dispose op
        data.write(0); // Blend op
        data.flush();
        byte[] chunkData = baos.toByteArray();
        data.writeInt(crc32(chunkData, 4, chunkData.length - 4)); // CRC
    }
    return baos.toByteArray();
}

private static byte[] createFDATChunk(int sequenceNumber, byte[] imageData) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try (DataOutputStream data = new DataOutputStream(baos)) {
        data.writeInt(imageData.length - 8 + 4); // fdAT data length (excluding sequence number)
        data.writeBytes("fdAT"); // Chunk type
        data.writeInt(sequenceNumber); // Sequence number
        data.write(imageData, 8, imageData.length - 8); // Image data (excluding length and type)
        data.flush();
        byte[] chunkData = baos.toByteArray();
        data.writeInt(crc32(chunkData, 4, chunkData.length - 4)); // CRC
    }
    return baos.toByteArray();
}

private static byte[] createIENDChunk() throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try (DataOutputStream data = new DataOutputStream(baos)) {
        data.writeInt(0); // IEND data length
        data.writeBytes("IEND"); // Chunk type
        data.flush();
        byte[] chunkData = baos.toByteArray();
        data.writeInt(crc32(chunkData, 4, chunkData.length - 4)); // CRC
    }
    return baos.toByteArray();
}

DataOutputStream is documented as always writing data in big-endian format.

发布评论

评论列表(0)

  1. 暂无评论