All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.itextpdf.pdfcleanup.util.CleanUpImageUtil Maven / Gradle / Ivy

The newest version!
/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2024 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 .
 */
package com.itextpdf.pdfcleanup.util;

import com.itextpdf.kernel.geom.Rectangle;
import org.apache.commons.imaging.ImageFormat;
import org.apache.commons.imaging.ImageFormats;
import org.apache.commons.imaging.ImageInfo;
import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.ImageWriteException;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.formats.png.PngConstants;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Utility class providing methods to handle images and work with graphics.
 */
public final class CleanUpImageUtil {
    private static final Color CLEANED_AREA_FILL_COLOR = Color.WHITE;

    private CleanUpImageUtil() {
    }

    /**
     * Clean up an image using a List of Rectangles that need to be redacted.
     *
     * @param imageBytes       the image to be cleaned up
     * @param areasToBeCleaned the List of Rectangles that need to be redacted out of the image
     * @return an array of bytes of the image with cleaned areas.
     */
    public static byte[] cleanUpImage(byte[] imageBytes, List areasToBeCleaned) {
        if (areasToBeCleaned.isEmpty()) {
            return imageBytes;
        }

        try {
            final ImageInfo imageInfo = Imaging.getImageInfo(imageBytes);
            BufferedImage image = getBuffer(imageBytes, imageInfo.getFormat());
            cleanImage(image, areasToBeCleaned);
            return writeImage(image, imageInfo);
        } catch (ImageReadException | ImageWriteException | IOException e) {
            throw new CleanupImageHandlingUtilException(e.getMessage(), e);
        }
    }

    /**
     * Clean up a BufferedImage using a List of Rectangles that need to be redacted.
     *
     * @param image            the image to be cleaned up
     * @param areasToBeCleaned the List of Rectangles that need to be redacted out of the image
     */
    private static void cleanImage(BufferedImage image, List areasToBeCleaned) {
        Graphics2D graphics = image.createGraphics();
        graphics.setColor(CLEANED_AREA_FILL_COLOR);

        // A rectangle in the areasToBeCleaned list is treated to be in standard [0,1]x[0,1] image space
        // (y varies from bottom to top and x from left to right), so we should scale the rectangle and also
        // invert and shear the y axe.
        for (Rectangle rect : areasToBeCleaned) {
            int imgHeight = image.getHeight();
            int imgWidth = image.getWidth();
            int[] scaledRectToClean = CleanUpHelperUtil.getImageRectToClean(rect, imgWidth, imgHeight);

            graphics.fillRect(scaledRectToClean[0], scaledRectToClean[1], scaledRectToClean[2], scaledRectToClean[3]);
        }

        graphics.dispose();
    }

    /**
     * Reads the image bytes into a {@link BufferedImage}.
     * Isolates and catches known Apache Commons Imaging bug for JPEG:
     * https://issues.apache.org/jira/browse/IMAGING-97
     *
     * @param imageBytes the image to be read, as a byte array
     * @return a BufferedImage, independent of the reading strategy
     */
    private static BufferedImage getBuffer(byte[] imageBytes, ImageFormat imageFormat) throws IOException {
        try {
            if (imageFormat != ImageFormats.JPEG) {
                return Imaging.getBufferedImage(imageBytes);
            }
        } catch (ImageReadException ire) {
            // No actions required in the catch block, because the default return should be performed.
        }

        return ImageIO.read(new ByteArrayInputStream(imageBytes));
    }

    private static byte[] writeImage(BufferedImage imageToWrite, ImageInfo originalImageInfo)
            throws IOException, ImageWriteException {
        // Apache can only read JPEG, so we should use awt for writing in this format
        if (originalImageInfo.getFormat() == ImageFormats.JPEG) {
            return getJPGBytes(imageToWrite);
        } else {
            Map params = new ConcurrentHashMap<>();

            // At least for PNG images if the resulted image can be grayscale, then Imaging makes
            // it grayscale. As we do not want to change image format at all, then we need to
            // force true color (if the image is not grayscale, then Imaging always uses true color).
            if (originalImageInfo.getFormat() == ImageFormats.PNG
                    && originalImageInfo.getColorType() != ImageInfo.ColorType.GRAYSCALE) {
                params.put(PngConstants.PARAM_KEY_PNG_FORCE_TRUE_COLOR, true);
            }

            return Imaging.writeImageToBytes(imageToWrite, originalImageInfo.getFormat(), params);
        }
    }

    /**
     * Get the bytes of the BufferedImage (in JPG format).
     *
     * @param image input image
     */
    private static byte[] getJPGBytes(BufferedImage image) {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next();
            ImageWriteParam jpgWriteParam = jpgWriter.getDefaultWriteParam();
            jpgWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            jpgWriteParam.setCompressionQuality(1.0f);

            jpgWriter.setOutput(new MemoryCacheImageOutputStream(outputStream));
            IIOImage outputImage = new IIOImage(image, null, null);

            jpgWriter.write(null, outputImage, jpgWriteParam);
            jpgWriter.dispose();
            outputStream.flush();

            return outputStream.toByteArray();
        } catch (IOException e) {
            throw new CleanupImageHandlingUtilException(e.getMessage(), e);
        }
    }

    /**
     * Exception is thrown when errors occur during handling and cleaning up images.
     */
    public static class CleanupImageHandlingUtilException extends RuntimeException {
        /**
         * Creates a new {@link CleanUpImageUtil.CleanupImageHandlingUtilException}.
         *
         * @param msg the detail message.
         * @param e the exception to wrap.
         */
        public CleanupImageHandlingUtilException(String msg, Exception e) {
            super(msg, e);
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy