PngChunks.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2025, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*/
package net.sourceforge.plantuml.klimt.drawing.raster;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
/**
* Adds to a PNG the chunks PlantUML writes in its PNG images (see
* {@code PngIOMetadata}): the copyleft text, the source of the diagram
* (compressed iTXt "plantuml", read back by {@code -metadata}), and the
* resolution when it is not 96 dpi.
*/
public final class PngChunks {
private static final String COPYLEFT = "Generated by https://plantuml.com";
private static final byte[] SIGNATURE = { (byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' };
private PngChunks() {
}
/**
* @param png a PNG starting with its IHDR chunk, as encoders write it
* @param metadata the source of the diagram, or null
* @param dpi the resolution of the image
*/
public static byte[] addPlantUmlChunks(byte[] png, String metadata, int dpi) throws IOException {
for (int i = 0; i < SIGNATURE.length; i++)
if (png.length < SIGNATURE.length || png[i] != SIGNATURE[i])
throw new IOException("Not a PNG");
// The IHDR chunk always comes first: 4 (length) + 4 (type) + 13 + 4 (CRC)
final int afterHeader = SIGNATURE.length + 4 + 4 + 13 + 4;
if (png.length < afterHeader || "IHDR".equals(new String(png, 12, 4, StandardCharsets.ISO_8859_1)) == false)
throw new IOException("No IHDR chunk");
final ByteArrayOutputStream result = new ByteArrayOutputStream(png.length + 1024);
result.write(png, 0, afterHeader);
// Same order as ImageIO: pHYs, tEXt, iTXt, before the image data
if (dpi != 96)
writeChunk(result, "pHYs", physicalDimensions(dpi));
writeChunk(result, "tEXt", text("copyleft", COPYLEFT));
if (metadata != null)
writeChunk(result, "iTXt", compressedInternationalText("plantuml", metadata));
result.write(png, afterHeader, png.length - afterHeader);
return result.toByteArray();
}
private static byte[] physicalDimensions(int dpi) {
final long pixelsPerMeter = Math.round(dpi / 0.0254);
final ByteArrayOutputStream data = new ByteArrayOutputStream();
writeInt(data, (int) pixelsPerMeter);
writeInt(data, (int) pixelsPerMeter);
data.write(1); // unit: meter
return data.toByteArray();
}
private static byte[] text(String keyword, String value) {
final ByteArrayOutputStream data = new ByteArrayOutputStream();
final byte[] key = keyword.getBytes(StandardCharsets.ISO_8859_1);
data.write(key, 0, key.length);
data.write(0);
final byte[] text = value.getBytes(StandardCharsets.ISO_8859_1);
data.write(text, 0, text.length);
return data.toByteArray();
}
private static byte[] compressedInternationalText(String keyword, String value) {
final ByteArrayOutputStream data = new ByteArrayOutputStream();
final byte[] key = keyword.getBytes(StandardCharsets.ISO_8859_1);
data.write(key, 0, key.length);
data.write(0);
data.write(1); // compression flag
data.write(0); // compression method: zlib
data.write(0); // empty language tag
data.write(0); // empty translated keyword
final byte[] compressed = deflate(value.getBytes(StandardCharsets.UTF_8));
data.write(compressed, 0, compressed.length);
return data.toByteArray();
}
private static byte[] deflate(byte[] input) {
final Deflater deflater = new Deflater();
try {
deflater.setInput(input);
deflater.finish();
final ByteArrayOutputStream result = new ByteArrayOutputStream(input.length / 2 + 64);
final byte[] buffer = new byte[8192];
while (deflater.finished() == false) {
final int n = deflater.deflate(buffer);
result.write(buffer, 0, n);
}
return result.toByteArray();
} finally {
deflater.end();
}
}
private static void writeChunk(ByteArrayOutputStream os, String type, byte[] data) {
final byte[] typeBytes = type.getBytes(StandardCharsets.ISO_8859_1);
writeInt(os, data.length);
os.write(typeBytes, 0, typeBytes.length);
os.write(data, 0, data.length);
final CRC32 crc = new CRC32();
crc.update(typeBytes);
crc.update(data);
writeInt(os, (int) crc.getValue());
}
private static void writeInt(ByteArrayOutputStream os, int value) {
os.write(value >>> 24);
os.write(value >>> 16);
os.write(value >>> 8);
os.write(value);
}
}