QuantUtils.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2024, 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
* With assistance from ChatGPT (OpenAI)
*
*/
package net.sourceforge.plantuml.png.quant;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
/**
* Utility functions for image handling in the quantization process.
*/
public final class QuantUtils {
/**
* Converts any {@link RenderedImage} into a {@link BufferedImage} of type
* {@link BufferedImage#TYPE_INT_ARGB}.
* <p>
* - If the input is already a {@link BufferedImage} with a supported type
* (ARGB, RGB, 3BYTE_BGR, or 4BYTE_ABGR), it is returned as-is. <br>
* - Otherwise, an {@link IllegalArgumentException} is thrown.
* </p>
*
* <p>
* This method is intended to normalize different image types into a single
* consistent format (ARGB) before performing color quantization.
* </p>
*
* @param src the input image (must be a {@link BufferedImage})
* @return the same image if it already has a supported format
* @throws IllegalArgumentException if the image is not a {@link BufferedImage}
* or if its type is unsupported
*/
public static BufferedImage toBufferedARGB(RenderedImage src) {
if (src instanceof BufferedImage) {
final BufferedImage bi = (BufferedImage) src;
final int type = bi.getType();
// Only accept common formats that can be processed safely
if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB
|| type == BufferedImage.TYPE_3BYTE_BGR || type == BufferedImage.TYPE_4BYTE_ABGR)
return bi;
// This already indexed
if (type == BufferedImage.TYPE_BYTE_INDEXED)
return null;
throw new IllegalArgumentException("BufferedImage type=" + type);
}
throw new IllegalArgumentException();
}
/**
* Bitwise, branch-free LSB -> MSB "compression" for packed {@code 0xAARRGGBB} pixels.
*
* <p>Replaces the least-significant bit (LSB) of each byte (A, R, G, B) with that byte’s
* most-significant bit (MSB). This slightly reduces per-channel entropy with negligible
* visual impact while keeping pure black and pure white unchanged.</p>
*
* <p>Formally, for each 8-bit component {@code x} in {A,R,G,B}:
* {@code x' = (x & 0xFE) | ((x >>> 7) & 0x01)}.</p>
*
* <p>This packed implementation is equivalent to applying the above transform independently
* to A, R, G, and B and then reassembling the pixel:</p>
*
* <pre>{@code
* // Reference (per-channel) form:
* int a = (argb >>> 24) & 0xFF;
* int r = (argb >>> 16) & 0xFF;
* int g = (argb >>> 8) & 0xFF;
* int b = argb & 0xFF;
*
* a = (a & 0xFE) | ((a >>> 7) & 1);
* r = (r & 0xFE) | ((r >>> 7) & 1);
* g = (g & 0xFE) | ((g >>> 7) & 1);
* b = (b & 0xFE) | ((b >>> 7) & 1);
*
* return (a << 24) | (r << 16) | (g << 8) | b;
* }</pre>
*
* <h4>Perceptual & compression rationale</h4>
* <ul>
* <li><b>Visually negligible:</b> Flipping at most one LSB per channel changes the value by <=1/255
* (~0.4%) per component. Under normal viewing conditions and typical display gamma, such changes
* are below the just-noticeable difference for flat regions, lines, and text—effectively invisible
* to the naked eye. Pure black ({@code 0x00}) and pure white ({@code 0xFF}) remain unchanged.</li>
* <li><b>More compressible:</b> Enforcing a relation between each byte’s MSB and its LSB removes high-frequency
* “LSB noise,” which increases redundancy. PNG’s scanline filters (Sub/Up/Average/Paeth) then produce
* residuals with more zeros and repeated patterns, yielding better DEFLATE (LZ77+Huffman) matches.
* In practice this often reduces output size without any visible degradation. The same intuition applies
* to other lossless codecs that benefit from lower entropy and longer runs.</li>
* </ul>
*
* <h4>Properties</h4>
* <ul>
* <li><b>Idempotent</b>: {@code f(f(x)) == f(x)}.</li>
* <li><b>Only LSBs may change</b>: {@code x ^ f(x)} can have bits set only at positions 0, 8, 16, 24.</li>
* <li><b>New LSB equals old MSB</b> for each byte.</li>
* <li><b>Invariants</b>: {@code 0x00000000} (black) and {@code 0xFFFFFFFF} (white) remain unchanged.</li>
* <li><b>Branchless and fast</b>: two ANDs, one logical right shift, one OR; very JIT- and SIMD-friendly.</li>
* </ul>
*
* <h4>Usage notes</h4>
* <ul>
* <li>Input must be a packed 32-bit ARGB pixel ({@code 0xAARRGGBB}).</li>
* <li>Prefer non-premultiplied images (e.g., {@code TYPE_INT_ARGB}). On premultiplied data
* ({@code TYPE_INT_ARGB_PRE}), altering RGB independently by 1 LSB slightly breaks the A·RGB invariant.</li>
* <li>This transform is intentionally <b>not reversible</b>; it loses at most one bit per channel.</li>
* <li>On synthetic gradients specifically crafted to reveal banding, any LSB change could, in theory,
* be measurable; for diagrams/UI/flat fills and typical photography, it is effectively invisible.</li>
* </ul>
*
* <h4>Examples</h4>
* <pre>{@code
* // Byte 0x7F (0111_1111) -> 0x7E (0111_1110)
* // Byte 0x80 (1000_0000) -> 0x81 (1000_0001)
* int p = 0x7F80_55AA; // A=0x7F, R=0x80, G=0x55, B=0xAA
* int q = compressPackedARGB(p); // q == 0x7E81_54AB
* }</pre>
*
* @param argb packed 32-bit pixel in ARGB order ({@code 0xAARRGGBB}).
* @return the pixel with each channel’s LSB replaced by its MSB.
* @implNote Packed equivalent of the per-channel form:
* {@code (argb & 0xFEFEFEFE) | ((argb >>> 7) & 0x01010101)}.
* @see #smartCompress(int)
*/
public static int compressPackedARGB(int argb) {
return (argb & 0xFEFEFEFE) | ((argb >>> 7) & 0x01010101);
}
}