BitmapImagePixels.java

/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2025 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package com.itextpdf.kernel.utils;

import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfNumber;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.colorspace.PdfColorSpace;
import com.itextpdf.kernel.pdf.xobject.PdfImageXObject;

import java.util.Arrays;

/**
 * Class allows to process pixels of the bitmap image stored as byte array according to PDF
 * specification.
 */
public class BitmapImagePixels {
    private static final int BITS_IN_BYTE = 8;
    private static final int DEFAULT_BITS_PER_COMPONENT = 8;
    private static final int BYTE_WITH_LEADING_BIT = 0b10000000;

    // (x / 8) == (x >>> 3)
    private static final int BITS_IN_BYTE_LOG = 3;
    // (x % 8) == (x & 0b00000111)
    private static final int BIT_MASK = 0b00000111;

    private final int width;
    // Pdf spec: each row of sample data shall begin on a byte boundary. If the number of data bits
    // per row is not a multiple of 8, the end of the row is padded with extra bits to fill out the
    // last byte. A conforming reader shall ignore these padding bits.
    private final int bitsInRow;
    private final int height;
    private final int bitsPerComponent;
    private final int maxComponentValue;
    private final int numberOfComponents;
    private final byte[] data;

    /**
     * Creates a representation of empty image.
     *
     * @param width is a width of the image
     * @param height is a height of the image
     * @param bitsPerComponent is an amount of bits representing each color component of a pixel
     * @param numberOfComponents is a number of components representing a pixel
     */
    public BitmapImagePixels(int width, int height, int bitsPerComponent, int numberOfComponents) {
        this(width, height, bitsPerComponent, numberOfComponents, null);
    }

    /**
     * Creates a representation of an image presented as {@link PdfImageXObject}.
     *
     * @param image is an image as {@link PdfImageXObject}
     */
    public BitmapImagePixels(PdfImageXObject image) {
        this(
                (int) Math.round(image.getWidth()),
                (int) Math.round(image.getHeight()),
                obtainBitsPerComponent(image),
                obtainNumberOfComponents(image),
                image.getPdfObject().getBytes()
        );
    }

    /**
     * Creates a representation of an image presented as bytes array.
     *
     * @param width is a width of the image
     * @param height is a height of the image
     * @param bitsPerComponent is an amount of bits representing each color component of a pixel
     * @param numberOfComponents is a number of components representing a pixel
     * @param data is an image data
     */
    public BitmapImagePixels(int width, int height, int bitsPerComponent, int numberOfComponents, byte[] data) {
        this.width = width;
        this.height = height;
        this.bitsPerComponent = bitsPerComponent;
        this.maxComponentValue = (1 << this.bitsPerComponent) - 1;
        this.numberOfComponents = numberOfComponents;
        int rowLength = width * bitsPerComponent * numberOfComponents;
        if (rowLength % BITS_IN_BYTE != 0) {
            rowLength += BITS_IN_BYTE - (rowLength & BIT_MASK);
        }
        bitsInRow = rowLength;
        if (data == null) {
            this.data = new byte[(bitsInRow * height) >>> BITS_IN_BYTE_LOG];
        } else {
            final int expectedLength = bitsInRow * height;
            final int actualLength = data.length * BITS_IN_BYTE;
            if (expectedLength != actualLength) {
                throw new IllegalArgumentException(MessageFormatUtil.format(
                        KernelExceptionMessageConstant.INVALID_DATA_LENGTH, expectedLength, actualLength));
            }

            this.data = Arrays.copyOf(data, data.length);
        }
    }

    /**
     * Gets pixel of the image.
     *
     * @param x is an x-coordinate of a pixel to update
     * @param y is a y-coordinate of a pixel to update
     * @return an array representing pixel color according to used color space
     */
    public double[] getPixel(int x, int y) {
        final long[] longArray = getPixelAsLongs(x, y);
        double[] pixelArray = new double[longArray.length];
        for (int i = 0; i < pixelArray.length; i++) {
            pixelArray[i] = (double) longArray[i] / maxComponentValue;
        }
        return pixelArray;
    }

    /**
     * Gets pixel of the image presented as long values.
     *
     * @param x is an x-coordinate of a pixel to update
     * @param y is a y-coordinate of a pixel to update
     * @return an array representing pixel color according to used color space
     */
    public long[] getPixelAsLongs(int x, int y) {
        checkCoordinates(x, y);
        final long[] pixelArray = new long[numberOfComponents];
        for (int i = 0; i < pixelArray.length; i++) {
            pixelArray[i] = readNumber(
                    // skip y rows from 0 to y-1
                    y * bitsInRow +
                            // skip x pixels from 0 to (x-1)
                            x * bitsPerComponent * numberOfComponents +
                            // skip i components of the current pixel from 0 to (i-1)
                            i * bitsPerComponent);
        }
        return pixelArray;
    }

    /**
     * Updates a pixel of the image.
     *
     * @param x is an x-coordinate of a pixel to update
     * @param y is a y-coordinate of a pixel to update
     * @param value is a pixel color. Pixel should be presented as double array according to used
     *              color space. Each value should be in range [0., 1.] (otherwise negative value
     *              will be replaced with 0. and large numbers are replaced with 1.)
     */
    public void setPixel(int x, int y, double[] value) {
        final long[] longArray = new long[value.length];
        for (int i = 0; i < value.length; i++) {
            longArray[i] =(long) Math.round(value[i] * maxComponentValue);
        }
        setPixel(x, y, longArray);
    }

    /**
     * Updates a pixel of the image.
     *
     * @param x is an x-coordinate of a pixel to update
     * @param y is a y-coordinate of a pixel to update
     * @param value is a pixel color. Pixel should be presented as long array according to used
     *              color space. Each value should be in range
     *              [0, <code>2 ^ bitsPerComponent</code> - 1] (otherwise negative value
     *              will be replaced with 0. and large numbers are replaced with maximum allowed
     *              value.)
     */
    public void setPixel(int x, int y, long[] value) {
        checkCoordinates(x, y);
        checkPixel(value);
        for (int i = 0; i < value.length; i++) {
            writeNumber(value[i],
                    // skip y rows from 0 to y-1
                    y * bitsInRow +
                            // skip x pixels from 0 to (x-1)
                            x * bitsPerComponent * numberOfComponents +
                            // skip i components of the current pixel from 0 to (i-1)
                            i * bitsPerComponent);
        }
    }

    /**
     * Getter for a width of the image.
     *
     * @return width of the image
     */
    public int getWidth() {
        return width;
    }

    /**
     * Getter for a height of the image.
     *
     * @return height of the image
     */
    public int getHeight() {
        return height;
    }

    /**
     * Getter for bits per component parameter of the image.
     *
     * @return bits per component parameter of the image
     */
    public int getBitsPerComponent() {
        return bitsPerComponent;
    }

    /**
     * Getter for number of components parameter of the image.
     *
     * @return number of components of the image
     */
    public int getNumberOfComponents() {
        return numberOfComponents;
    }

    /**
     * Getter for byte representation of the image.
     *
     * @return image data as byte array
     */
    public byte[] getData() {
        return data;
    }

    /**
     * Gets the maximum value for the component.
     *
     * @return maximum value of the component
     */
    public int getMaxComponentValue() {
        return maxComponentValue;
    }

    private long readNumber(int index) {
        long result = 0;
        for (int i = 0; i < bitsPerComponent; i++) {
            result = (result << 1) + booleanToInt(getBit(index + i));
        }
        return result;
    }

    private void writeNumber(long number, int index) {
        for (int bitNumber = 0; bitNumber < bitsPerComponent; bitNumber ++) {
            final int actualBitMask = 1 << (bitsPerComponent - bitNumber - 1);
            setBit(index + bitNumber, (number & actualBitMask) != 0);
        }
    }

    private boolean getBit(int index) {
        return (data[index >>> BITS_IN_BYTE_LOG] & 0xff
                & (BYTE_WITH_LEADING_BIT >>> (index & BIT_MASK))) != 0;
    }

    private void setBit(int index, boolean value) {
        if (value) {
            data[index >>> BITS_IN_BYTE_LOG] |= (byte) (BYTE_WITH_LEADING_BIT >>> (index & BIT_MASK));
        } else {
            data[index >>> BITS_IN_BYTE_LOG] &= (byte) ~(BYTE_WITH_LEADING_BIT >>> (index & BIT_MASK));
        }
    }

    private void checkCoordinates(int x, int y) {
        if (x < 0 || x >= width || y < 0 || y > height) {
            throw new IllegalArgumentException(
                    MessageFormatUtil.format(
                            KernelExceptionMessageConstant.PIXEL_OUT_OF_BORDERS, x, y, width, height));
        }
    }
    private void checkPixel(long[] pixel) {
        if (pixel.length != numberOfComponents) {
            throw new IllegalArgumentException(
                    MessageFormatUtil.format(
                            KernelExceptionMessageConstant.LENGTH_OF_ARRAY_SHOULD_MATCH_NUMBER_OF_COMPONENTS,
                            pixel.length, numberOfComponents));
        }

        for (int i = 0; i < pixel.length; i++) {
            if (pixel[i] < 0) {
                pixel[i] = 0;
            }
            if (pixel[i] > maxComponentValue) {
                pixel[i] = maxComponentValue;
            }
        }
    }

    private static int obtainBitsPerComponent(PdfImageXObject objectToProcess) {
        final PdfStream imageStream = objectToProcess.getPdfObject();
        final PdfNumber bpc = imageStream.getAsNumber(PdfName.BitsPerComponent);
        if (bpc == null) {
            return DEFAULT_BITS_PER_COMPONENT;
        } else {
            return bpc.intValue();
        }
    }

    private static int obtainNumberOfComponents(PdfImageXObject objectToProcess) {
        return PdfColorSpace.makeColorSpace(
                objectToProcess.getPdfObject().get(PdfName.ColorSpace)).getNumberOfComponents();
    }

    private static int booleanToInt(boolean value) {
        return value ? 1 : 0;
    }
}