/* * * This file is part of the iText (R) project. Copyright (c) 1998-2017 iText Group NV * Authors: Bruno Lowagie, Paulo Soares, et al. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License version 3 * as published by the Free Software Foundation with the addition of the * following permission added to Section 15 as permitted in Section 7(a): * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY * ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT * OF THIRD PARTY RIGHTS * * 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 http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA, 02110-1301 USA, or download the license from the following URL: * http://itextpdf.com/terms-of-use/ * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License. * * In accordance with Section 7(b) of the GNU Affero General Public License, * a covered work must retain the producer line in every PDF that is created * or manipulated using iText. * * You can be released from the requirements of the license by purchasing * a commercial license. Buying such a license is mandatory as soon as you * develop commercial activities involving the iText software without * disclosing the source code of your own applications. * These activities include: offering paid services to customers as an ASP, * serving PDFs on the fly in a web application, shipping iText with a closed * source product. * * For more information, please contact iText Software Corp. at this * address: sales@itextpdf.com */ package com.itextpdf.text.pdf.pdfcleanup; import com.itextpdf.text.Image; import com.itextpdf.text.Rectangle; import com.itextpdf.text.exceptions.UnsupportedPdfException; import com.itextpdf.text.pdf.PdfBoolean; import com.itextpdf.text.pdf.PdfContentByte; import com.itextpdf.text.pdf.PdfDictionary; import com.itextpdf.text.pdf.PdfName; import com.itextpdf.text.pdf.PdfObject; import com.itextpdf.text.pdf.PdfStamper; import com.itextpdf.text.pdf.parser.ExtRenderListener; import com.itextpdf.text.pdf.parser.ImageRenderInfo; import com.itextpdf.text.pdf.parser.LineDashPattern; import com.itextpdf.text.pdf.parser.LineSegment; import com.itextpdf.text.pdf.parser.Matrix; import com.itextpdf.text.pdf.parser.Path; import com.itextpdf.text.pdf.parser.PathConstructionRenderInfo; import com.itextpdf.text.pdf.parser.PathPaintingRenderInfo; import com.itextpdf.text.pdf.parser.PdfImageObject; import com.itextpdf.text.pdf.parser.TextRenderInfo; 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.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; import org.apache.commons.imaging.ImageFormats; import org.apache.commons.imaging.ImageInfo; import org.apache.commons.imaging.Imaging; import org.apache.commons.imaging.ImagingConstants; import org.apache.commons.imaging.formats.tiff.constants.TiffConstants; class PdfCleanUpRenderListener implements ExtRenderListener { private static final Color CLEANED_AREA_FILL_COLOR = Color.WHITE; private PdfStamper pdfStamper; private PdfCleanUpRegionFilter filter; private List<PdfCleanUpContentChunk> chunks = new ArrayList<PdfCleanUpContentChunk>(); private Stack<PdfCleanUpContext> contextStack = new Stack<PdfCleanUpContext>(); private int strNumber = 1; // Represents ordinal number of string under processing. Needed for processing TJ operator. // Represents current path as if there were no segments to cut private Path unfilteredCurrentPath = new Path(); // Represents actual current path to be stroked ("actual" means that it is filtered current path) private Path currentStrokePath = new Path(); // Represents actual current path to be filled. private Path currentFillPath = new Path(); // Represents the latest path used as a clipping path in the new content stream. private Path newClippingPath; private boolean clipPath; private int clippingRule; public PdfCleanUpRenderListener(PdfStamper pdfStamper, PdfCleanUpRegionFilter filter) { this.pdfStamper = pdfStamper; this.filter = filter; } public void renderText(TextRenderInfo renderInfo) { if (renderInfo.getPdfString().toUnicodeString().length() == 0) { return; } for (TextRenderInfo ri : renderInfo.getCharacterRenderInfos()) { boolean isAllowed = filter.allowText(ri); LineSegment baseline = ri.getUnscaledBaseline(); chunks.add(new PdfCleanUpContentChunk.Text(ri.getPdfString(), baseline.getStartPoint(), baseline.getEndPoint(), isAllowed, strNumber)); } ++strNumber; } public void renderImage(ImageRenderInfo renderInfo) { List<Rectangle> areasToBeCleaned = getImageAreasToBeCleaned(renderInfo); if (areasToBeCleaned == null) { chunks.add(new PdfCleanUpContentChunk.Image(false, null)); } else if ( areasToBeCleaned.size() > 0) { try { PdfImageObject pdfImage = renderInfo.getImage(); byte[] imageBytes = processImage(pdfImage.getImageAsBytes(), areasToBeCleaned); if (renderInfo.getRef() == null && pdfImage != null) { // true => inline image PdfDictionary dict = pdfImage.getDictionary(); PdfObject imageMask = dict.get(PdfName.IMAGEMASK); Image image = Image.getInstance(imageBytes); if (imageMask == null) { imageMask = dict.get(PdfName.IM); } if (imageMask != null && imageMask.equals(PdfBoolean.PDFTRUE)) { image.makeMask(); } PdfContentByte canvas = getContext().getCanvas(); canvas.addImage(image, 1, 0, 0, 1, 0, 0, true); } else if (pdfImage != null && imageBytes != pdfImage.getImageAsBytes()) { chunks.add(new PdfCleanUpContentChunk.Image(true, imageBytes)); } } catch (UnsupportedPdfException pdfException) { chunks.add(new PdfCleanUpContentChunk.Image(false, null)); } catch (Exception e) { throw new RuntimeException(e); } } } public void beginTextBlock() { } public void endTextBlock() { } public void modifyPath(PathConstructionRenderInfo renderInfo) { List<Float> segmentData = renderInfo.getSegmentData(); switch (renderInfo.getOperation()) { case PathConstructionRenderInfo.MOVETO: unfilteredCurrentPath.moveTo(segmentData.get(0), segmentData.get(1)); break; case PathConstructionRenderInfo.LINETO: unfilteredCurrentPath.lineTo(segmentData.get(0), segmentData.get(1)); break; case PathConstructionRenderInfo.CURVE_123: unfilteredCurrentPath.curveTo(segmentData.get(0), segmentData.get(1), segmentData.get(2), segmentData.get(3), segmentData.get(4), segmentData.get(5)); break; case PathConstructionRenderInfo.CURVE_23: unfilteredCurrentPath.curveTo(segmentData.get(0), segmentData.get(1), segmentData.get(2), segmentData.get(3)); break; case PathConstructionRenderInfo.CURVE_13: unfilteredCurrentPath.curveFromTo(segmentData.get(0), segmentData.get(1), segmentData.get(2), segmentData.get(3)); break; case PathConstructionRenderInfo.CLOSE: unfilteredCurrentPath.closeSubpath(); break; case PathConstructionRenderInfo.RECT: unfilteredCurrentPath.rectangle(segmentData.get(0), segmentData.get(1), segmentData.get(2), segmentData.get(3)); break; } } public Path renderPath(PathPaintingRenderInfo renderInfo) { boolean stroke = (renderInfo.getOperation() & PathPaintingRenderInfo.STROKE) != 0; boolean fill = (renderInfo.getOperation() & PathPaintingRenderInfo.FILL) != 0; float lineWidth = renderInfo.getLineWidth(); int lineCapStyle = renderInfo.getLineCapStyle(); int lineJoinStyle = renderInfo.getLineJoinStyle(); float miterLimit = renderInfo.getMiterLimit(); LineDashPattern lineDashPattern = renderInfo.getLineDashPattern(); if (stroke) { currentStrokePath = filterCurrentPath(renderInfo.getCtm(), true, -1, lineWidth, lineCapStyle, lineJoinStyle, miterLimit, lineDashPattern); } if (fill) { currentFillPath = filterCurrentPath(renderInfo.getCtm(), false, renderInfo.getRule(), lineWidth, lineCapStyle, lineJoinStyle, miterLimit, lineDashPattern); } if (clipPath) { if (fill && renderInfo.getRule() == clippingRule) { newClippingPath = currentFillPath; } else { newClippingPath = filterCurrentPath(renderInfo.getCtm(), false, clippingRule, lineWidth, lineCapStyle, lineJoinStyle, miterLimit, lineDashPattern); } } unfilteredCurrentPath = new Path(); return newClippingPath; } public void clipPath(int rule) { clipPath = true; clippingRule = rule; } public boolean isClipped() { return clipPath; } public void setClipped(boolean clipPath) { this.clipPath = clipPath; } public int getClippingRule() { return clippingRule; } public Path getCurrentStrokePath() { return currentStrokePath; } public Path getCurrentFillPath() { return currentFillPath; } public Path getNewClipPath() { return newClippingPath; } public List<PdfCleanUpContentChunk> getChunks() { return chunks; } public PdfCleanUpContext getContext() { return contextStack.peek(); } public void registerNewContext(PdfDictionary resources, PdfContentByte canvas) { canvas = canvas == null ? new PdfContentByte(pdfStamper.getWriter()) : canvas; contextStack.push(new PdfCleanUpContext(resources, canvas)); } public void popContext() { contextStack.pop(); } public void clearChunks() { chunks.clear(); strNumber = 1; } /** * @return null if the image is not allowed (either it is fully covered or ctm == null). * List of covered image areas otherwise. */ private List<Rectangle> getImageAreasToBeCleaned(ImageRenderInfo renderInfo) { return filter.getCoveredAreas(renderInfo); } private byte[] processImage(byte[] imageBytes, List<Rectangle> areasToBeCleaned) { if (areasToBeCleaned.isEmpty()) { return imageBytes; } try { BufferedImage image = Imaging.getBufferedImage(imageBytes); ImageInfo imageInfo = Imaging.getImageInfo(imageBytes); cleanImage(image, areasToBeCleaned); // Apache can only read JPEG, so we should use awt for writing in this format if (imageInfo.getFormat() == ImageFormats.JPEG) { return getJPGBytes(image); } else { Map<String, Object> params = new HashMap<String, Object>(); if (imageInfo.getFormat() == ImageFormats.TIFF) { params.put(ImagingConstants.PARAM_KEY_COMPRESSION, TiffConstants.TIFF_COMPRESSION_LZW); } return Imaging.writeImageToBytes(image, imageInfo.getFormat(), params); } } catch (Exception e) { throw new RuntimeException(e); } } private void cleanImage(BufferedImage image, List<Rectangle> 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 scaledBottomY = (int) Math.ceil(rect.getBottom() * image.getHeight()); int scaledTopY = (int) Math.floor(rect.getTop() * image.getHeight()); int x = (int) Math.ceil(rect.getLeft() * image.getWidth()); int y = scaledTopY * -1 + image.getHeight(); int width = (int) Math.floor(rect.getRight() * image.getWidth()) - x; int height = scaledTopY - scaledBottomY; graphics.fillRect(x, y, width, height); } graphics.dispose(); } private byte[] getJPGBytes(BufferedImage image) { ByteArrayOutputStream outputStream = null; try { ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next(); ImageWriteParam jpgWriteParam = jpgWriter.getDefaultWriteParam(); jpgWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); jpgWriteParam.setCompressionQuality(1.0f); outputStream = new ByteArrayOutputStream(); 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 (Exception e) { throw new RuntimeException(e); } finally { closeOutputStream(outputStream); } } /** * @param fillingRule If the path is contour, pass any value. */ private Path filterCurrentPath(Matrix ctm, boolean stroke, int fillingRule, float lineWidth, int lineCapStyle, int lineJoinStyle, float miterLimit, LineDashPattern lineDashPattern) { Path path = new Path(unfilteredCurrentPath.getSubpaths()); if (stroke) { return filter.filterStrokePath(path, ctm, lineWidth, lineCapStyle, lineJoinStyle, miterLimit, lineDashPattern); } else { return filter.filterFillPath(path, ctm, fillingRule); } } private void closeOutputStream(OutputStream os) { if (os != null) { try { os.close(); } catch (IOException e) { throw new RuntimeException(e); } } } }