diff -Nru libpdfbox2-java-2.0.9/app/pom.xml libpdfbox2-java-2.0.13/app/pom.xml --- libpdfbox2-java-2.0.9/app/pom.xml 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/app/pom.xml 2018-11-28 17:18:34.000000000 +0000 @@ -23,7 +23,7 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 ../parent/pom.xml @@ -68,7 +68,7 @@ true *;scope=provided;inline=org/apache/**|org/bouncycastle/**|META-INF/services/** ${project.url} - !junit.framework,!junit.textui,javax.*;resolution:=optional,org.apache.avalon.framework.logger;resolution:=optional,org.apache.log;resolution:=optional,org.apache.log4j;resolution:=optional,* + !junit.framework,!junit.textui,javax.*;resolution:=optional,org.apache.avalon.framework.logger;resolution:=optional,org.apache.log;resolution:=optional,* org.apache.pdfbox.tools.PDFBox diff -Nru libpdfbox2-java-2.0.9/debian/changelog libpdfbox2-java-2.0.13/debian/changelog --- libpdfbox2-java-2.0.9/debian/changelog 2018-04-15 17:31:40.000000000 +0000 +++ libpdfbox2-java-2.0.13/debian/changelog 2019-02-27 12:49:08.000000000 +0000 @@ -1,3 +1,45 @@ +libpdfbox2-java (2.0.13-2~18.04) bionic; urgency=medium + + * Backport for OpenJDK 11 (dependency of pdfsam). LP: #1814133. + + -- Matthias Klose Wed, 27 Feb 2019 13:49:08 +0100 + +libpdfbox2-java (2.0.13-2) unstable; urgency=medium + + * Team upload. + * Build the tools module + * Standards-Version updated to 4.3.0 + * Use salsa.debian.org Vcs-* URLs + + -- Emmanuel Bourg Thu, 17 Jan 2019 22:32:22 +0100 + +libpdfbox2-java (2.0.13-1) unstable; urgency=medium + + * New upstream version 2.0.13. + + -- Markus Koschany Sat, 08 Dec 2018 15:50:15 +0100 + +libpdfbox2-java (2.0.12-1) unstable; urgency=medium + + * New upstream version 2.0.12. + - Fix CVE-2018-11797: denial-of-service via specially crafted PDF file. + (Closes: #910391) + * Declare compliance with Debian Policy 4.2.1. + + -- Markus Koschany Sat, 06 Oct 2018 12:05:00 +0200 + +libpdfbox2-java (2.0.11-1) unstable; urgency=medium + + * New upstream version 2.0.11. + + -- Markus Koschany Sat, 30 Jun 2018 01:50:14 +0200 + +libpdfbox2-java (2.0.10-1) unstable; urgency=medium + + * New upstream version 2.0.10. + + -- Markus Koschany Thu, 28 Jun 2018 23:55:29 +0200 + libpdfbox2-java (2.0.9-1) unstable; urgency=medium * New upstream version 2.0.9. diff -Nru libpdfbox2-java-2.0.9/debian/control libpdfbox2-java-2.0.13/debian/control --- libpdfbox2-java-2.0.9/debian/control 2018-04-15 17:31:40.000000000 +0000 +++ libpdfbox2-java-2.0.13/debian/control 2019-01-17 21:31:30.000000000 +0000 @@ -18,9 +18,9 @@ libmaven-javadoc-plugin-java, libpdfbox2-java, maven-debian-helper -Standards-Version: 4.1.4 -Vcs-Git: https://anonscm.debian.org/git/pkg-java/libpdfbox2-java.git -Vcs-Browser: https://anonscm.debian.org/git/pkg-java/libpdfbox2-java.git +Standards-Version: 4.3.0 +Vcs-Git: https://salsa.debian.org/java-team/libpdfbox2-java.git +Vcs-Browser: https://salsa.debian.org/java-team/libpdfbox2-java Homepage: http://pdfbox.apache.org Package: libpdfbox2-java diff -Nru libpdfbox2-java-2.0.9/debian/libpdfbox2-java.poms libpdfbox2-java-2.0.13/debian/libpdfbox2-java.poms --- libpdfbox2-java-2.0.9/debian/libpdfbox2-java.poms 2018-04-15 17:31:40.000000000 +0000 +++ libpdfbox2-java-2.0.13/debian/libpdfbox2-java.poms 2019-01-17 21:27:49.000000000 +0000 @@ -34,6 +34,6 @@ preflight-app/pom.xml --ignore app/pom.xml --ignore examples/pom.xml --ignore -tools/pom.xml --ignore +tools/pom.xml --no-parent --usj-name=pdfbox2-tools --package=libpdfbox2-java --java-lib debugger/pom.xml --ignore debugger-app/pom.xml --ignore diff -Nru libpdfbox2-java-2.0.9/debian/maven.publishedRules libpdfbox2-java-2.0.13/debian/maven.publishedRules --- libpdfbox2-java-2.0.9/debian/maven.publishedRules 2018-04-15 17:31:40.000000000 +0000 +++ libpdfbox2-java-2.0.13/debian/maven.publishedRules 2019-01-17 21:30:03.000000000 +0000 @@ -2,3 +2,4 @@ org.apache.pdfbox fontbox s/jar/bundle/ s/.*/2.x/ * * org.apache.pdfbox pdfbox s/jar/bundle/ s/.*/2.x/ * * org.apache.pdfbox pdfbox-reactor pom s/.*/2.x/ * * +org.apache.pdfbox pdfbox-tools jar s/.*/2.x/ * * diff -Nru libpdfbox2-java-2.0.9/debian/maven.rules libpdfbox2-java-2.0.13/debian/maven.rules --- libpdfbox2-java-2.0.9/debian/maven.rules 2018-04-15 17:31:40.000000000 +0000 +++ libpdfbox2-java-2.0.13/debian/maven.rules 2019-01-17 21:28:36.000000000 +0000 @@ -7,5 +7,7 @@ org.bouncycastle s/bcprov-jdk15on/bcprov/ * s/.*/debian/ * * org.apache.pdfbox fontbox s/jar/bundle/ s/.*/2.x/ * * org.apache.pdfbox pdfbox s/jar/bundle/ s/.*/2.x/ * * +org.apache.pdfbox pdfbox-debugger * s/.*/2.x/ * * org.apache.pdfbox pdfbox-reactor pom s/.*/2.x/ * * +org.apache.pdfbox pdfbox-tools * s/.*/2.x/ * * diff -Nru libpdfbox2-java-2.0.9/debian/patches/disable-debugger.patch libpdfbox2-java-2.0.13/debian/patches/disable-debugger.patch --- libpdfbox2-java-2.0.9/debian/patches/disable-debugger.patch 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/debian/patches/disable-debugger.patch 2019-01-17 21:10:47.000000000 +0000 @@ -0,0 +1,58 @@ +Description: Disable the debugger support in the tools component +Author: Emmanuel Bourg +Forwarded: not-needed +--- a/tools/src/main/java/org/apache/pdfbox/tools/PDFBox.java ++++ b/tools/src/main/java/org/apache/pdfbox/tools/PDFBox.java +@@ -16,8 +16,6 @@ + */ + package org.apache.pdfbox.tools; + +-import org.apache.pdfbox.debugger.PDFDebugger; +- + /** + * Simple wrapper around all the command line utilities included in PDFBox. + * Used as the main class in the runnable standalone PDFBox jar. +@@ -71,11 +69,6 @@ + { + PrintPDF.main(arguments); + } +- else if (command.equals("PDFDebugger") || command.equals("PDFReader")) +- { +- PDFDebugger.main(arguments); +- exitAfterCallingMain = false; +- } + else if (command.equals("PDFMerger")) + { + PDFMerger.main(arguments); +@@ -122,7 +115,6 @@ + + " ExtractImages\n" + + " OverlayPDF\n" + + " PrintPDF\n" +- + " PDFDebugger\n" + + " PDFMerger\n" + + " PDFReader\n" + + " PDFSplit\n" +--- a/tools/pom.xml ++++ b/tools/pom.xml +@@ -39,16 +39,16 @@ + + + ++ ${project.groupId} ++ pdfbox ++ ${project.version} ++ ++ + org.bouncycastle + bcmail-jdk15on + true + + +- ${project.groupId} +- pdfbox-debugger +- ${project.version} +- +- + junit + junit + test diff -Nru libpdfbox2-java-2.0.9/debian/patches/series libpdfbox2-java-2.0.13/debian/patches/series --- libpdfbox2-java-2.0.9/debian/patches/series 2018-04-15 17:31:40.000000000 +0000 +++ libpdfbox2-java-2.0.13/debian/patches/series 2019-01-17 21:26:05.000000000 +0000 @@ -1 +1,2 @@ jar-packaging.patch +disable-debugger.patch diff -Nru libpdfbox2-java-2.0.9/debian/rules libpdfbox2-java-2.0.13/debian/rules --- libpdfbox2-java-2.0.9/debian/rules 2018-04-15 17:31:40.000000000 +0000 +++ libpdfbox2-java-2.0.13/debian/rules 2019-01-17 21:30:43.000000000 +0000 @@ -9,6 +9,3 @@ override_dh_installexamples: dh_installexamples find $(CURDIR)/debian/libpdfbox2-java-doc/ -type d -empty -delete - -get-orig-source: - uscan --download-current-version --force-download --repack --rename --compression xz diff -Nru libpdfbox2-java-2.0.9/debian/watch libpdfbox2-java-2.0.13/debian/watch --- libpdfbox2-java-2.0.9/debian/watch 2018-04-15 17:31:40.000000000 +0000 +++ libpdfbox2-java-2.0.13/debian/watch 2019-01-17 21:31:20.000000000 +0000 @@ -1,12 +1,14 @@ -version=3 +version=4 # http://pdfbox.apache.org/download.html # points to a page for mirror selection opts=\ +repack,\ +compression=xz,\ dversionmangle=s/(?:\.|\+)dfsg$//,\ downloadurlmangle=s/pdfbox\/([\d.]+)\//pdfbox\/$1\/pdfbox-$1-src.zip/,\ filenamemangle=s/([\d.]+)\//pdfbox-$1-src.zip/,\ pgpsigurlmangle=s/$/.asc/ \ - http://archive.apache.org/dist/pdfbox/ ([\d.]+)/ + https://archive.apache.org/dist/pdfbox/ ([\d.]+)/ diff -Nru libpdfbox2-java-2.0.9/debugger/pom.xml libpdfbox2-java-2.0.13/debugger/pom.xml --- libpdfbox2-java-2.0.9/debugger/pom.xml 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/pom.xml 2018-11-28 17:18:38.000000000 +0000 @@ -23,7 +23,7 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 ../parent/pom.xml diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/colorpane/CSSeparation.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/colorpane/CSSeparation.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/colorpane/CSSeparation.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/colorpane/CSSeparation.java 2018-11-28 17:18:38.000000000 +0000 @@ -90,7 +90,7 @@ slider.setMajorTickSpacing(50); slider.setPaintTicks(true); - Dictionary labelTable = new Hashtable(); + Dictionary labelTable = new Hashtable(); JLabel lightest = new JLabel("lightest"); lightest.setFont(new Font(Font.MONOSPACED, Font.BOLD, 10)); JLabel darkest = new JLabel("darkest"); diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/flagbitspane/FlagBitsPane.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/flagbitspane/FlagBitsPane.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/flagbitspane/FlagBitsPane.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/flagbitspane/FlagBitsPane.java 2018-11-28 17:18:38.000000000 +0000 @@ -20,6 +20,7 @@ import javax.swing.JPanel; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; /** * @author Khyrul Bashar @@ -29,14 +30,16 @@ public class FlagBitsPane { private FlagBitsPaneView view; + private final PDDocument document; /** * Constructor. * @param dictionary COSDictionary instance. * @param flagType COSName instance. */ - public FlagBitsPane(final COSDictionary dictionary, COSName flagType) + public FlagBitsPane(PDDocument document, final COSDictionary dictionary, COSName flagType) { + this.document = document; createPane(dictionary, flagType); } @@ -79,7 +82,7 @@ } if (COSName.SIG_FLAGS.equals(flagType)) { - flag = new SigFlag(dictionary); + flag = new SigFlag(document, dictionary); view = new FlagBitsPaneView( flag.getFlagType(), flag.getFlagValue(), flag.getFlagBits(), flag.getColumnNames()); } diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/flagbitspane/SigFlag.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/flagbitspane/SigFlag.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/flagbitspane/SigFlag.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/flagbitspane/SigFlag.java 2018-11-28 17:18:38.000000000 +0000 @@ -19,6 +19,7 @@ import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; /** @@ -28,16 +29,18 @@ */ public class SigFlag extends Flag { - private final COSDictionary acroformDictionary; + private final PDDocument document; + private final COSDictionary acroFormDictionary; /** * Constructor * * @param acroFormDictionary COSDictionary instance. */ - SigFlag(COSDictionary acroFormDictionary) + SigFlag(PDDocument document, COSDictionary acroFormDictionary) { - acroformDictionary = acroFormDictionary; + this.document = document; + this.acroFormDictionary = acroFormDictionary; } @Override @@ -49,13 +52,13 @@ @Override String getFlagValue() { - return "Flag value: " + acroformDictionary.getInt(COSName.SIG_FLAGS); + return "Flag value: " + acroFormDictionary.getInt(COSName.SIG_FLAGS); } @Override Object[][] getFlagBits() { - PDAcroForm acroForm = new PDAcroForm(null, acroformDictionary); + PDAcroForm acroForm = new PDAcroForm(document, acroFormDictionary); return new Object[][]{ new Object[]{1, "SignaturesExist", acroForm.isSignaturesExist()}, new Object[]{2, "AppendOnly", acroForm.isAppendOnly()}, diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/hexviewer/HexEditor.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/hexviewer/HexEditor.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/hexviewer/HexEditor.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/hexviewer/HexEditor.java 2018-11-28 17:18:38.000000000 +0000 @@ -43,7 +43,7 @@ /** * @author Khyrul Bashar * - * This class hosts all the UI components of Hex view and cordinate among them. + * This class hosts all the UI components of Hex view and coordinates among them. */ class HexEditor extends JPanel implements SelectionChangeListener { diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/hexviewer/HexModel.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/hexviewer/HexModel.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/hexviewer/HexModel.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/hexviewer/HexModel.java 2018-11-28 17:18:38.000000000 +0000 @@ -23,7 +23,7 @@ /** * @author Khyrul Bashar * - * A class that acts as a model for the hex viewer. It holds the data and provide the data as ncessary. + * A class that acts as a model for the hex viewer. It holds the data and provide the data as necessary. * It'll let listen for any underlying data changes. */ class HexModel implements HexChangeListener diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java 2018-11-28 17:18:38.000000000 +0000 @@ -28,6 +28,8 @@ import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ExecutionException; import javax.swing.BoxLayout; import javax.swing.JLabel; @@ -44,6 +46,10 @@ import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.debugger.PDFDebugger; import org.apache.pdfbox.debugger.ui.HighResolutionImageIcon; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDField; /** * Display the page number and a page rendering. @@ -61,6 +67,10 @@ private RotationMenu rotationMenu; private final JLabel statuslabel; private final PDPage page; + private String labelText = ""; + private final Map rectMap = new HashMap(); + private final AffineTransform defaultTransform = GraphicsEnvironment.getLocalGraphicsEnvironment(). + getDefaultScreenDevice().getDefaultConfiguration().getDefaultTransform(); public PagePane(PDDocument document, COSDictionary pageDict, JLabel statuslabel) { @@ -69,6 +79,27 @@ this.document = document; this.statuslabel = statuslabel; initUI(); + initRectMap(); + } + + private void initRectMap() + { + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + if (acroForm == null) + { + return; + } + for (PDField field : acroForm.getFieldTree()) + { + String fullyQualifiedName = field.getFullyQualifiedName(); + for (PDAnnotationWidget widget : field.getWidgets()) + { + if (page.equals(widget.getPage())) + { + rectMap.put(widget.getRectangle(), fullyQualifiedName); + } + } + } } private void initUI() @@ -185,8 +216,8 @@ float offsetX = page.getCropBox().getLowerLeftX(); float offsetY = page.getCropBox().getLowerLeftY(); float zoomScale = zoomMenu.getPageZoomScale(); - float x = e.getX() / zoomScale; - float y = e.getY() / zoomScale; + float x = e.getX() / zoomScale * (float) defaultTransform.getScaleX(); + float y = e.getY() / zoomScale * (float) defaultTransform.getScaleY(); int x1, y1; switch ((RotationMenu.getRotationDegrees() + page.getRotation()) % 360) { @@ -208,7 +239,19 @@ y1 = (int) (height - y + offsetY); break; } - statuslabel.setText(x1 + "," + y1); + String text = "x: " + x1 + ", y: " + y1; + + // are we in a field widget? + for (Map.Entry entry : rectMap.entrySet()) + { + if (entry.getKey().contains(x1, y1)) + { + text += ", field: " + rectMap.get(entry.getKey()); + break; + } + } + + statuslabel.setText(text); } @Override @@ -234,7 +277,7 @@ @Override public void mouseExited(MouseEvent e) { - statuslabel.setText(""); + statuslabel.setText(labelText); } /** @@ -257,14 +300,16 @@ protected BufferedImage doInBackground() throws IOException { label.setIcon(null); - label.setText("Rendering..."); + labelText = "Rendering..."; + label.setText(labelText); PDFRenderer renderer = new PDFRenderer(document); renderer.setSubsamplingAllowed(allowSubsampling); long t0 = System.currentTimeMillis(); - statuslabel.setText("Rendering..."); + statuslabel.setText(labelText); BufferedImage bim = renderer.renderImage(pageIndex, scale); float t = (System.currentTimeMillis() - t0) / 1000f; - statuslabel.setText("Rendered in " + t + " second" + (t > 1 ? "s" : "")); + labelText = "Rendered in " + t + " second" + (t > 1 ? "s" : ""); + statuslabel.setText(labelText); return ImageUtil.getRotatedImage(bim, rotation); } @@ -280,10 +325,8 @@ // a smaller size than the image to compensate that the // image is scaled up with some screen configurations (e.g. 125% on windows). // See PDFBOX-3665 for more sample code and discussion. - AffineTransform tx = GraphicsEnvironment.getLocalGraphicsEnvironment(). - getDefaultScreenDevice().getDefaultConfiguration().getDefaultTransform(); - label.setSize((int) Math.ceil(image.getWidth() / tx.getScaleX()), - (int) Math.ceil(image.getHeight() / tx.getScaleY())); + label.setSize((int) Math.ceil(image.getWidth() / defaultTransform.getScaleX()), + (int) Math.ceil(image.getHeight() / defaultTransform.getScaleY())); label.setIcon(new HighResolutionImageIcon(image, label.getWidth(), label.getHeight())); label.setText(null); } diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/PDFDebugger.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/PDFDebugger.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/PDFDebugger.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/PDFDebugger.java 2018-11-28 17:18:38.000000000 +0000 @@ -18,6 +18,7 @@ import java.awt.BorderLayout; import java.awt.Component; +import java.awt.Cursor; import java.awt.Dimension; import java.awt.FileDialog; import java.awt.Toolkit; @@ -42,7 +43,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.StringTokenizer; +import javax.imageio.spi.IIORegistry; import javax.print.attribute.HashPrintRequestAttributeSet; import javax.print.attribute.PrintRequestAttributeSet; import javax.print.attribute.standard.Sides; @@ -105,11 +106,11 @@ import org.apache.pdfbox.debugger.ui.RotationMenu; import org.apache.pdfbox.debugger.ui.Tree; import org.apache.pdfbox.debugger.ui.ZoomMenu; +import org.apache.pdfbox.filter.FilterFactory; import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.common.PDPageLabels; import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException; -import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceCMYK; import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB; @@ -300,10 +301,20 @@ } }); + initGlobalEventHandlers(); + } + + /** + * Initialize application global event handlers. Protected to allow + * subclasses to override this method if they don't want the global event + * handler overridden. + */ + @SuppressWarnings("WeakerAccess") + protected void initGlobalEventHandlers() + { // Mac OS X file open/quit handler - if (IS_MAC_OS && !isMinJdk9()) + if (IS_MAC_OS) { - //TODO this needs to be rewritten for JDK9, see PDFBOX-4013 try { Method osxOpenFiles = getClass().getDeclaredMethod("osxOpenFiles", String.class); @@ -415,11 +426,8 @@ } }); - if (!IS_MAC_OS) - { - fileMenu.addSeparator(); - fileMenu.add(printMenuItem); - } + fileMenu.addSeparator(); + fileMenu.add(printMenuItem); JMenuItem exitMenuItem = new JMenuItem("Exit"); exitMenuItem.setAccelerator(KeyStroke.getKeyStroke("alt F4")); @@ -630,7 +638,7 @@ openDialog.setVisible(true); if (openDialog.getFile() != null) { - readPDFFile(openDialog.getFile(), ""); + readPDFFile(new File(openDialog.getDirectory(),openDialog.getFile()), ""); } } else @@ -898,7 +906,9 @@ { selectedNode = ((MapEntry)selectedNode).getKey(); selectedNode = getUnderneathObject(selectedNode); - FlagBitsPane flagBitsPane = new FlagBitsPane((COSDictionary) parentNode, (COSName) selectedNode); + FlagBitsPane flagBitsPane = new FlagBitsPane(document, + (COSDictionary) parentNode, + (COSName) selectedNode); replaceRightComponent(flagBitsPane.getPane()); } } @@ -1075,7 +1085,7 @@ return data; } - private void exitMenuItemActionPerformed(ActionEvent evt) + private void exitMenuItemActionPerformed(ActionEvent ignored) { if( document != null ) { @@ -1093,6 +1103,16 @@ throw new RuntimeException(e); } } + performApplicationExit(); + } + + /** + * Exit the application after the window is closed. This is protected to let + * subclasses override the behavior. + */ + @SuppressWarnings("WeakerAccess") + protected void performApplicationExit() + { System.exit(0); } @@ -1127,7 +1147,15 @@ } if (job.printDialog(pras)) { - job.print(pras); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + try + { + job.print(pras); + } + finally + { + setCursor(Cursor.getDefaultCursor()); + } } } catch (PrinterException e) @@ -1141,23 +1169,7 @@ */ private void exitForm(WindowEvent evt) { - if( document != null ) - { - try - { - document.close(); - if (!currentFilePath.startsWith("http")) - { - recentFiles.addFile(currentFilePath); - } - recentFiles.close(); - } - catch( IOException e ) - { - throw new RuntimeException(e); - } - } - System.exit(0); + exitMenuItemActionPerformed(null); } /** @@ -1202,6 +1214,8 @@ // Yes this is always true PDDeviceCMYK.INSTANCE.toRGB(new float[] { 0, 0, 0, 0} ); PDDeviceRGB.INSTANCE.toRGB(new float[] { 0, 0, 0 } ); + IIORegistry.getDefaultInstance(); + FilterFactory.INSTANCE.getFilter(COSName.FLATE_DECODE); } // open file, if any @@ -1249,7 +1263,7 @@ readPDFFile(file, password); } - private void readPDFFile(File file, String password) throws IOException + private void readPDFFile(final File file, String password) throws IOException { if( document != null ) { @@ -1261,7 +1275,17 @@ } currentFilePath = file.getPath(); recentFiles.removeFile(file.getPath()); - parseDocument( file, password ); + DocumentOpener documentOpener = new DocumentOpener(password) + { + @Override + PDDocument open() throws IOException + { + return PDDocument.load(file, password); + } + }; + document = documentOpener.parse(); + printMenuItem.setEnabled(true); + reopenMenuItem.setEnabled(true); initTree(); @@ -1277,7 +1301,7 @@ addRecentFileItems(); } - private void readPDFurl(String urlString, String password) throws IOException + private void readPDFurl(final String urlString, String password) throws IOException { if (document != null) { @@ -1288,8 +1312,15 @@ } } currentFilePath = urlString; - URL url = new URL(urlString); - document = PDDocument.load(url.openStream(), password); + DocumentOpener documentOpener = new DocumentOpener(password) + { + @Override + PDDocument open() throws IOException + { + return PDDocument.load(new URL(urlString).openStream(), password); + } + }; + document = documentOpener.parse(); printMenuItem.setEnabled(true); reopenMenuItem.setEnabled(true); @@ -1327,45 +1358,65 @@ tree.setSelectionPath(treeStatus.getPathForString("Root")); } } - + /** - * This will parse a document. - * - * @param file The file addressing the document. - * - * @throws IOException If there is an error parsing the document. + * Internal class to avoid double code in password entry loop. */ - private void parseDocument( File file, String password )throws IOException + abstract class DocumentOpener { - while (true) + String password; + + DocumentOpener(String password) { - try - { - document = PDDocument.load(file, password); - } - catch (InvalidPasswordException ipe) + this.password = password; + } + + /** + * Override to load the actual input type (File, URL, stream), don't call it directly! + * + * @return + * @throws IOException + */ + abstract PDDocument open() throws IOException; + + /** + * Call this! + * + * @return + * @throws IOException + */ + final PDDocument parse() throws IOException + { + while (true) { - // https://stackoverflow.com/questions/8881213/joptionpane-to-get-password - JPanel panel = new JPanel(); - JLabel label = new JLabel("Password:"); - JPasswordField pass = new JPasswordField(10); - panel.add(label); - panel.add(pass); - String[] options = new String[] {"OK", "Cancel"}; - int option = JOptionPane.showOptionDialog(null, panel, "Enter password", - JOptionPane.NO_OPTION, JOptionPane.PLAIN_MESSAGE, - null, options, ""); - if (option == 0) + try + { + return open(); + } + catch (InvalidPasswordException ipe) { - password = new String(pass.getPassword()); - continue; + // https://stackoverflow.com/questions/8881213/joptionpane-to-get-password + JPanel panel = new JPanel(); + JLabel label = new JLabel("Password:"); + JPasswordField pass = new JPasswordField(10); + panel.add(label); + panel.add(pass); + String[] options = new String[] + { + "OK", "Cancel" + }; + int option = JOptionPane.showOptionDialog(null, panel, "Enter password", + JOptionPane.NO_OPTION, JOptionPane.PLAIN_MESSAGE, + null, options, ""); + if (option == 0) + { + password = new String(pass.getPassword()); + continue; + } + throw ipe; } - throw ipe; } - break; - } - printMenuItem.setEnabled(true); - reopenMenuItem.setEnabled(true); + } } private void addRecentFileItems() @@ -1446,26 +1497,4 @@ } return null; } - - private static boolean isMinJdk9() - { - // strategy from lucene-solr/lucene/core/src/java/org/apache/lucene/util/Constants.java - String version = System.getProperty("java.specification.version"); - final StringTokenizer st = new StringTokenizer(version, "."); - try - { - int major = Integer.parseInt(st.nextToken()); - int minor = 0; - if (st.hasMoreTokens()) - { - minor = Integer.parseInt(st.nextToken()); - } - return major > 1 || (major == 1 && minor >= 9); - } - catch (NumberFormatException nfe) - { - // maybe some new numbering scheme in the 22nd century - return true; - } - } } diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/streampane/StreamPane.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/streampane/StreamPane.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/streampane/StreamPane.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/streampane/StreamPane.java 2018-11-28 17:18:38.000000000 +0000 @@ -26,11 +26,10 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Map; +import java.util.Vector; import java.util.concurrent.ExecutionException; -import javax.imageio.ImageIO; import javax.swing.BoxLayout; import javax.swing.JComboBox; import javax.swing.JComponent; @@ -146,8 +145,15 @@ } tabbedPane = new JTabbedPane(); - tabbedPane.add("Text view", view.getStreamPanel()); - tabbedPane.add("Hex view", hexView.getPane()); + if (stream.isImage()) + { + tabbedPane.add("Image view", view.getStreamPanel()); + } + else + { + tabbedPane.add("Text view", view.getStreamPanel()); + tabbedPane.add("Hex view", hexView.getPane()); + } panel.add(tabbedPane); } @@ -159,7 +165,7 @@ private JPanel createHeaderPanel(List availableFilters, String i, ActionListener actionListener) { - JComboBox filters = new JComboBox(availableFilters.toArray()); + JComboBox filters = new JComboBox(new Vector(availableFilters)); filters.setSelectedItem(i); filters.addActionListener(actionListener); @@ -182,8 +188,13 @@ if (currentFilter.equals(Stream.IMAGE)) { requestImageShowing(); + tabbedPane.removeAll(); + tabbedPane.add("Image view", view.getStreamPanel()); return; } + tabbedPane.removeAll(); + tabbedPane.add("Text view", view.getStreamPanel()); + tabbedPane.add("Hex view", hexView.getPane()); requestStreamText(currentFilter); } catch (IOException e) @@ -208,13 +219,6 @@ return; } view.showStreamImage(image); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ImageIO.write(image, "jpg", baos); - baos.flush(); - byte[] bytes = baos.toByteArray(); - baos.close(); - hexView.changeData(bytes); } } diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/ui/OSXAdapter.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/ui/OSXAdapter.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/ui/OSXAdapter.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/ui/OSXAdapter.java 2018-11-28 17:18:38.000000000 +0000 @@ -18,7 +18,7 @@ /* * This file includes code under the following terms: - * + * * Version: 2.0 * * Disclaimer: IMPORTANT: This Apple software is supplied to you by @@ -64,10 +64,14 @@ package org.apache.pdfbox.debugger.ui; +import java.awt.Desktop; +import java.io.File; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.StringTokenizer; +import java.util.List; /** * Hooks existing preferences/about/quit functionality from an @@ -87,10 +91,63 @@ protected String proxySignature; static Object macOSXApplication; + + private static boolean isMinJdk9() + { + // strategy from lucene-solr/lucene/core/src/java/org/apache/lucene/util/Constants.java + String version = System.getProperty("java.specification.version"); + final StringTokenizer st = new StringTokenizer(version, "."); + try + { + int major = Integer.parseInt(st.nextToken()); + int minor = 0; + if (st.hasMoreTokens()) + { + minor = Integer.parseInt(st.nextToken()); + } + return major > 1 || (major == 1 && minor >= 9); + } + catch (NumberFormatException nfe) + { + // maybe some new numbering scheme in the 22nd century + return true; + } + } // Pass this method an Object and Method equipped to perform application shutdown logic // The method passed should return a boolean stating whether or not the quit should occur - public static void setQuitHandler(Object target, Method quitHandler) { + public static void setQuitHandler(final Object target, final Method quitHandler) + { + if (isMinJdk9()) + { + try + { + Desktop desktopObject = Desktop.getDesktop(); + Class filesHandlerClass = Class.forName("java.awt.desktop.QuitHandler"); + final Method setQuitHandlerMethod = desktopObject.getClass().getMethod("setQuitHandler", filesHandlerClass); + Object osxAdapterProxy = Proxy.newProxyInstance(OSXAdapter.class.getClassLoader(), + new Class[] { filesHandlerClass }, new InvocationHandler() + { + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable + { + if ("handleQuitRequestWith".equals(method.getName())) + { + // We just call our own quit handler + quitHandler.invoke(target); + } + return null; + } + }); + setQuitHandlerMethod.invoke(desktopObject, osxAdapterProxy); + } + catch (Exception e) + { + e.printStackTrace(); + } + return; + } setHandler(new OSXAdapter("handleQuit", target, quitHandler)); } @@ -133,10 +190,59 @@ // Pass this method an Object and a Method equipped to handle document events from the Finder // Documents are registered with the Finder via the CFBundleDocumentTypes dictionary in the // application bundle's Info.plist - public static void setFileHandler(Object target, Method fileHandler) { - setHandler(new OSXAdapter("handleOpenFile", target, fileHandler) { + public static void setFileHandler(Object target, Method fileHandler) + { + if (isMinJdk9()) + { + try + { + Desktop desktopObject = Desktop.getDesktop(); + Class filesHandlerClass = Class.forName("java.awt.desktop.OpenFilesHandler"); + Method setOpenFileHandlerMethod = desktopObject.getClass().getMethod("setOpenFileHandler", filesHandlerClass); + Object osxAdapterProxy = Proxy.newProxyInstance(OSXAdapter.class.getClassLoader(), + new Class[] + { + filesHandlerClass + }, new OSXAdapter("openFiles", target, fileHandler) + { + // Override OSXAdapter.callTarget to send information on the + // file to be opened + @Override + public boolean callTarget(Object openFilesEvent) + { + if (openFilesEvent != null) + { + try + { + Method getFilesMethod = openFilesEvent.getClass().getDeclaredMethod("getFiles", + (Class[]) null); + @SuppressWarnings("unchecked") + List files = (List) getFilesMethod.invoke(openFilesEvent, + (Object[]) null); + this.targetMethod.invoke(this.targetObject, files.get(0).getAbsolutePath()); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + } + return true; + } + }); + setOpenFileHandlerMethod.invoke(desktopObject, osxAdapterProxy); + } + catch (Exception e) + { + e.printStackTrace(); + } + return; + } + /* JDK <= 1.8, using Apple classes */ + setHandler(new OSXAdapter("handleOpenFile", target, fileHandler) + { // Override OSXAdapter.callTarget to send information on the // file to be opened + @Override public boolean callTarget(Object appleEvent) { if (appleEvent != null) { try { diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/ui/PageEntry.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/ui/PageEntry.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/ui/PageEntry.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/ui/PageEntry.java 2018-11-28 17:18:38.000000000 +0000 @@ -18,6 +18,7 @@ package org.apache.pdfbox.debugger.ui; import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; @@ -63,8 +64,18 @@ COSDictionary node = dict; while (node.containsKey(COSName.PARENT)) { - COSDictionary parent = (COSDictionary)node.getDictionaryObject(COSName.PARENT); - COSArray kids = (COSArray)parent.getDictionaryObject(COSName.KIDS); + COSBase base = node.getDictionaryObject(COSName.PARENT); + if (!(base instanceof COSDictionary)) + { + return ""; + } + COSDictionary parent = (COSDictionary) base; + base = parent.getDictionaryObject(COSName.KIDS); + if (!(base instanceof COSArray)) + { + return ""; + } + COSArray kids = (COSArray) base; int idx = kids.indexOfObject(node); sb.append("/Kids/[").append(idx).append("]"); node = parent; diff -Nru libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/ui/PDFTreeCellRenderer.java libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/ui/PDFTreeCellRenderer.java --- libpdfbox2-java-2.0.9/debugger/src/main/java/org/apache/pdfbox/debugger/ui/PDFTreeCellRenderer.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger/src/main/java/org/apache/pdfbox/debugger/ui/PDFTreeCellRenderer.java 2018-11-28 17:18:38.000000000 +0000 @@ -205,13 +205,19 @@ if (dict.containsKey(COSName.TYPE)) { COSName type = dict.getCOSName(COSName.TYPE); - sb.append(" /T:").append(type.getName()); + if (type != null) + { + sb.append(" /T:").append(type.getName()); + } } - + if (dict.containsKey(COSName.SUBTYPE)) { COSName subtype = dict.getCOSName(COSName.SUBTYPE); - sb.append(" /S:").append(subtype.getName()); + if (subtype != null) + { + sb.append(" /S:").append(subtype.getName()); + } } return sb.toString(); } diff -Nru libpdfbox2-java-2.0.9/debugger-app/pom.xml libpdfbox2-java-2.0.13/debugger-app/pom.xml --- libpdfbox2-java-2.0.9/debugger-app/pom.xml 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/debugger-app/pom.xml 2018-11-28 17:18:40.000000000 +0000 @@ -23,7 +23,7 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 ../parent/pom.xml @@ -68,7 +68,7 @@ true *;scope=provided;inline=org/apache/**|org/bouncycastle/**|META-INF/services/** ${project.url} - !junit.framework,!junit.textui,javax.*;resolution:=optional,org.apache.avalon.framework.logger;resolution:=optional,org.apache.log;resolution:=optional,org.apache.log4j;resolution:=optional,* + !junit.framework,!junit.textui,javax.*;resolution:=optional,org.apache.avalon.framework.logger;resolution:=optional,org.apache.log;resolution:=optional,* org.apache.pdfbox.debugger.PDFDebugger diff -Nru libpdfbox2-java-2.0.9/examples/pom.xml libpdfbox2-java-2.0.13/examples/pom.xml --- libpdfbox2-java-2.0.9/examples/pom.xml 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/pom.xml 2018-11-28 17:18:40.000000000 +0000 @@ -23,7 +23,7 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 ../parent/pom.xml @@ -42,6 +42,26 @@ + + + + [11,) + + + + javax.xml.bind + jaxb-api + provided + + + javax.activation + activation + provided + + + + + org.bouncycastle @@ -77,7 +97,7 @@ org.apache.ant ant - 1.9.6 + 1.10.5 junit @@ -85,9 +105,15 @@ test - org.apache.wink - wink-component-test-support - 1.4 + javax.servlet + javax.servlet-api + 4.0.1 + test + + + org.apache.geronimo.specs + geronimo-jaxrs_1.1_spec + 1.0 test diff -Nru libpdfbox2-java-2.0.9/examples/src/main/appended-resources/META-INF/LICENSE libpdfbox2-java-2.0.13/examples/src/main/appended-resources/META-INF/LICENSE --- libpdfbox2-java-2.0.9/examples/src/main/appended-resources/META-INF/LICENSE 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/appended-resources/META-INF/LICENSE 2018-11-28 17:18:40.000000000 +0000 @@ -169,14 +169,111 @@ copyright holder. The file "sRGB Color Space Profile.icm" is: -Copyright (c) 1998 Hewlett-Packard Company -To anyone who acknowledges that the file "sRGB Color Space Profile.icm" -is provided "AS IS" WITH NO EXPRESS OR IMPLIED WARRANTY: -permission to use, copy and distribute this file for any purpose is hereby -granted without fee, provided that the file is not changed including the HP -copyright notice tag, and that the name of Hewlett-Packard Company not be -used in advertising or publicity pertaining to distribution of the software -without specific, written prior permission. Hewlett-Packard Company makes -no representations about the suitability of this software for any purpose. + Copyright (c) 1998 Hewlett-Packard Company + To anyone who acknowledges that the file "sRGB Color Space Profile.icm" + is provided "AS IS" WITH NO EXPRESS OR IMPLIED WARRANTY: + permission to use, copy and distribute this file for any purpose is hereby + granted without fee, provided that the file is not changed including the HP + copyright notice tag, and that the name of Hewlett-Packard Company not be + used in advertising or publicity pertaining to distribution of the software + without specific, written prior permission. Hewlett-Packard Company makes + no representations about the suitability of this software for any purpose. + +Lohit-Bengali font (https://pagure.io/lohit): + + Copyright 2011-13 Lohit Fonts Project contributors + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. + This license is copied below, and is also available with a FAQ at: + http://scripts.sil.org/OFL + + + ----------------------------------------------------------- + SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + ----------------------------------------------------------- + + PREAMBLE + The goals of the Open Font License (OFL) are to stimulate worldwide + development of collaborative font projects, to support the font creation + efforts of academic and linguistic communities, and to provide a free and + open framework in which fonts may be shared and improved in partnership + with others. + + The OFL allows the licensed fonts to be used, studied, modified and + redistributed freely as long as they are not sold by themselves. The + fonts, including any derivative works, can be bundled, embedded, + redistributed and/or sold with any software provided that any reserved + names are not used by derivative works. The fonts and derivatives, + however, cannot be released under any other type of license. The + requirement for fonts to remain under this license does not apply + to any document created using the fonts or their derivatives. + + DEFINITIONS + "Font Software" refers to the set of files released by the Copyright + Holder(s) under this license and clearly marked as such. This may + include source files, build scripts and documentation. + + "Reserved Font Name" refers to any names specified as such after the + copyright statement(s). + + "Original Version" refers to the collection of Font Software components as + distributed by the Copyright Holder(s). + + "Modified Version" refers to any derivative made by adding to, deleting, + or substituting -- in part or in whole -- any of the components of the + Original Version, by changing formats or by porting the Font Software to a + new environment. + + "Author" refers to any designer, engineer, programmer, technical + writer or other person who contributed to the Font Software. + + PERMISSION & CONDITIONS + Permission is hereby granted, free of charge, to any person obtaining + a copy of the Font Software, to use, study, copy, merge, embed, modify, + redistribute, and sell modified and unmodified copies of the Font + Software, subject to the following conditions: + + 1) Neither the Font Software nor any of its individual components, + in Original or Modified Versions, may be sold by itself. + + 2) Original or Modified Versions of the Font Software may be bundled, + redistributed and/or sold with any software, provided that each copy + contains the above copyright notice and this license. These can be + included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or + binary files as long as those fields can be easily viewed by the user. + + 3) No Modified Version of the Font Software may use the Reserved Font + Name(s) unless explicit written permission is granted by the corresponding + Copyright Holder. This restriction only applies to the primary font name as + presented to the users. + + 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + Software shall not be used to promote, endorse or advertise any + Modified Version, except to acknowledge the contribution(s) of the + Copyright Holder(s) and the Author(s) or with their explicit written + permission. + + 5) The Font Software, modified or unmodified, in part or in whole, + must be distributed entirely under this license, and must not be + distributed under any other license. The requirement for fonts to + remain under this license does not apply to any document created + using the Font Software. + + TERMINATION + This license becomes null and void if any of the above conditions are + not met. + + DISCLAIMER + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT + OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE + COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL + DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM + OTHER DEALINGS IN THE FONT SOFTWARE. diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/ant/package.html libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/ant/package.html --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/ant/package.html 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/ant/package.html 2018-11-28 17:18:40.000000000 +0000 @@ -21,14 +21,14 @@ ANT tasks that utilize PDFBox features can be found in this package. -This is an example of using the PDF2Text task:

+This is an example of using the PDF2Text task:

-<taskdef name="pdf2text" classname="org.apache.pdfbox.ant.PDFToTextTask" classpathref="build.classpath" />
+<taskdef name="pdf2text" classname="org.apache.pdfbox.ant.PDFToTextTask" classpathref="build.classpath" />
-<pdf2text>
-   <fileset dir="test">
-     <include name="**/*.pdf" />
-   </fileset>
-</pdf2text>
+<pdf2text>
+   <fileset dir="test">
+     <include name="**/*.pdf" />
+   </fileset>
+</pdf2text>
diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/AddAnnotations.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/AddAnnotations.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/AddAnnotations.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/AddAnnotations.java 2018-11-28 17:18:40.000000000 +0000 @@ -18,6 +18,9 @@ import java.io.IOException; import java.util.List; +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSFloat; +import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; @@ -31,6 +34,7 @@ import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationMarkup; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationSquareCircle; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationTextMarkup; import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; @@ -70,6 +74,7 @@ // Some basic reusable objects/constants // Annotations themselves can only be used once! PDColor red = new PDColor(new float[] { 1, 0, 0 }, PDDeviceRGB.INSTANCE); + PDColor green = new PDColor(new float[] { 0, 1, 0 }, PDDeviceRGB.INSTANCE); PDColor blue = new PDColor(new float[] { 0, 0, 1 }, PDDeviceRGB.INSTANCE); PDColor black = new PDColor(new float[] { 0, 0, 0 }, PDDeviceRGB.INSTANCE); @@ -235,8 +240,30 @@ dest.setPage(page3); actionGoto.setDestination(dest); pageLink.setAction(actionGoto); - annotations.add(pageLink); - + annotations.add(pageLink); + + // create a polygon annotation. Yes this is clunky, it will be easier in 3.0 + PDAnnotationMarkup polygon = new PDAnnotationMarkup(); + polygon.getCOSObject().setName(COSName.SUBTYPE, PDAnnotationMarkup.SUB_TYPE_POLYGON); + position = new PDRectangle(); + position.setLowerLeftX(pw - INCH); + position.setLowerLeftY(ph - INCH); + position.setUpperRightX(pw - 2 * INCH); + position.setUpperRightY(ph - 2 * INCH); + polygon.setRectangle(position); + polygon.setColor(blue); // border color + polygon.getCOSObject().setItem(COSName.IC, green.toCOSArray()); // interior color + COSArray verticesArray = new COSArray(); + verticesArray.add(new COSFloat(pw - INCH)); + verticesArray.add(new COSFloat(ph - 2 * INCH)); + verticesArray.add(new COSFloat(pw - INCH * 1.5f)); + verticesArray.add(new COSFloat(ph - INCH)); + verticesArray.add(new COSFloat(pw - 2 * INCH)); + verticesArray.add(new COSFloat(ph - 2 * INCH)); + polygon.getCOSObject().setItem(COSName.VERTICES, verticesArray); + polygon.setBorderStyle(borderThick); + polygon.setContents("Polygon annotation"); + annotations.add(polygon); showPageNo(document, page1, "Page 1"); showPageNo(document, page2, "Page 2"); diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/CreateBookmarks.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/CreateBookmarks.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/CreateBookmarks.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/CreateBookmarks.java 2018-11-28 17:18:40.000000000 +0000 @@ -21,6 +21,7 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PageMode; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitWidthDestination; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; @@ -84,6 +85,9 @@ } pagesOutline.openNode(); outline.openNode(); + + // optional: show the outlines when opening the file + document.getDocumentCatalog().setPageMode(PageMode.USE_OUTLINES); document.save( args[1] ); } diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerificationException.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerificationException.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerificationException.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerificationException.java 2018-11-28 17:18:40.000000000 +0000 @@ -0,0 +1,40 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.pdfbox.examples.signature.cert; + +/** + * Copied from Apache CXF 2.4.9, initial version: + * https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.9/distribution/src/main/release/samples/sts_issue_operation/src/main/java/demo/sts/provider/cert/ + * + */ +public class CertificateVerificationException extends Exception +{ + private static final long serialVersionUID = 1L; + + public CertificateVerificationException(String message, Throwable cause) + { + super(message, cause); + } + + public CertificateVerificationException(String message) + { + super(message); + } +} diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerificationResult.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerificationResult.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerificationResult.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerificationResult.java 2018-11-28 17:18:40.000000000 +0000 @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.pdfbox.examples.signature.cert; + +import java.security.cert.PKIXCertPathBuilderResult; + +/** + * Copied from Apache CXF 2.4.9, initial version: + * https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.9/distribution/src/main/release/samples/sts_issue_operation/src/main/java/demo/sts/provider/cert/ + * + */ +public class CertificateVerificationResult +{ + private boolean valid; + private PKIXCertPathBuilderResult result; + private Throwable exception; + + /** + * Constructs a certificate verification result for valid certificate by + * given certification path. + */ + public CertificateVerificationResult(PKIXCertPathBuilderResult result) + { + this.valid = true; + this.result = result; + } + + public CertificateVerificationResult(Throwable exception) + { + this.valid = false; + this.exception = exception; + } + + public boolean isValid() + { + return valid; + } + + public PKIXCertPathBuilderResult getResult() + { + return result; + } + + public Throwable getException() + { + return exception; + } +} diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerifier.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerifier.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerifier.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerifier.java 2018-11-28 17:18:40.000000000 +0000 @@ -0,0 +1,401 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.pdfbox.examples.signature.cert; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathBuilderException; +import java.security.cert.CertStore; +import java.security.cert.CertificateException; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.TrustAnchor; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.pdmodel.encryption.SecurityProvider; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERTaggedObject; +import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.X509ObjectIdentifiers; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.OCSPException; +import org.bouncycastle.cert.ocsp.OCSPResp; + +/** + * Copied from Apache CXF 2.4.9, initial version: + * https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.9/distribution/src/main/release/samples/sts_issue_operation/src/main/java/demo/sts/provider/cert/ + * + */ +public final class CertificateVerifier +{ + private static final Log LOG = LogFactory.getLog(CertificateVerifier.class); + + private CertificateVerifier() + { + + } + + /** + * Attempts to build a certification chain for given certificate and to + * verify it. Relies on a set of root CA certificates and intermediate + * certificates that will be used for building the certification chain. The + * verification process assumes that all self-signed certificates in the set + * are trusted root CA certificates and all other certificates in the set + * are intermediate certificates. + * + * @param cert - certificate for validation + * @param additionalCerts - set of trusted root CA certificates that will be + * used as "trust anchors" and intermediate CA certificates that will be + * used as part of the certification chain. All self-signed certificates are + * considered to be trusted root CA certificates. All the rest are + * considered to be intermediate CA certificates. + * @param verifySelfSignedCert true if a self-signed certificate is accepted, false if not. + * @param signDate the date when the signing took place + * @return the certification chain (if verification is successful) + * @throws CertificateVerificationException - if the certification is not + * successful (e.g. certification path cannot be built or some certificate + * in the chain is expired or CRL checks are failed) + */ + public static PKIXCertPathBuilderResult verifyCertificate( + X509Certificate cert, Set additionalCerts, + boolean verifySelfSignedCert, Date signDate) + throws CertificateVerificationException + { + try + { + // Check for self-signed certificate + if (!verifySelfSignedCert && isSelfSigned(cert)) + { + throw new CertificateVerificationException("The certificate is self-signed."); + } + + // Prepare a set of trust anchors (set of root CA certificates) + // and a set of intermediate certificates + Set intermediateCerts = new HashSet(); + Set trustAnchors = new HashSet(); + for (X509Certificate additionalCert : additionalCerts) + { + if (isSelfSigned(additionalCert)) + { + trustAnchors.add(new TrustAnchor(additionalCert, null)); + } + else + { + intermediateCerts.add(additionalCert); + } + } + + if (trustAnchors.isEmpty()) + { + throw new CertificateVerificationException("No root certificate in the chain"); + } + + // Attempt to build the certification chain and verify it + PKIXCertPathBuilderResult verifiedCertChain = verifyCertificate( + cert, trustAnchors, intermediateCerts, signDate); + + LOG.info("Certification chain verified successfully"); + + checkRevocations(cert, additionalCerts, signDate); + + return verifiedCertChain; + } + catch (CertPathBuilderException certPathEx) + { + throw new CertificateVerificationException( + "Error building certification path: " + + cert.getSubjectX500Principal(), certPathEx); + } + catch (CertificateVerificationException cvex) + { + throw cvex; + } + catch (Exception ex) + { + throw new CertificateVerificationException( + "Error verifying the certificate: " + + cert.getSubjectX500Principal(), ex); + } + } + + private static void checkRevocations(X509Certificate cert, + Set additionalCerts, + Date signDate) + throws IOException, CertificateVerificationException, OCSPException, + RevokedCertificateException, GeneralSecurityException + { + if (isSelfSigned(cert)) + { + // root, we're done + return; + } + X509Certificate issuerCert = null; + for (X509Certificate additionalCert : additionalCerts) + { + if (cert.getIssuerX500Principal().equals(additionalCert.getSubjectX500Principal())) + { + issuerCert = additionalCert; + break; + } + } + // issuerCert is never null here. If it hadn't been found, then there wouldn't be a + // verifiedCertChain earlier. + + // Try checking the certificate through OCSP (faster than CRL) + String ocspURL = extractOCSPURL(cert); + if (ocspURL != null) + { + OcspHelper ocspHelper = new OcspHelper(cert, signDate, issuerCert, additionalCerts, ocspURL); + try + { + verifyOCSP(ocspHelper, additionalCerts); + } + catch (IOException ex) + { + // happens with 021496.pdf because OCSP responder no longer exists + LOG.warn("IOException trying OCSP, will try CRL", ex); + CRLVerifier.verifyCertificateCRLs(cert, signDate, additionalCerts); + } + } + else + { + LOG.info("OCSP not available, will try CRL"); + + // Check whether the certificate is revoked by the CRL + // given in its CRL distribution point extension + CRLVerifier.verifyCertificateCRLs(cert, signDate, additionalCerts); + } + + // now check the issuer + checkRevocations(issuerCert, additionalCerts, signDate); + } + + /** + * Checks whether given X.509 certificate is self-signed. + * @param cert The X.509 certificate to check. + * @return true if the certificate is self-signed, false if not. + * @throws java.security.GeneralSecurityException + */ + public static boolean isSelfSigned(X509Certificate cert) throws GeneralSecurityException + { + try + { + // Try to verify certificate signature with its own public key + PublicKey key = cert.getPublicKey(); + cert.verify(key, SecurityProvider.getProvider().getName()); + return true; + } + catch (SignatureException ex) + { + // Invalid signature --> not self-signed + LOG.debug("Couldn't get signature information - returning false", ex); + return false; + } + catch (InvalidKeyException ex) + { + // Invalid signature --> not self-signed + LOG.debug("Couldn't get signature information - returning false", ex); + return false; + } + catch (IOException ex) + { + // Invalid signature --> not self-signed + LOG.debug("Couldn't get signature information - returning false", ex); + return false; + } + } + + /** + * Attempts to build a certification chain for given certificate and to + * verify it. Relies on a set of root CA certificates (trust anchors) and a + * set of intermediate certificates (to be used as part of the chain). + * + * @param cert - certificate for validation + * @param trustAnchors - set of trust anchors + * @param intermediateCerts - set of intermediate certificates + * @param signDate the date when the signing took place + * @return the certification chain (if verification is successful) + * @throws GeneralSecurityException - if the verification is not successful + * (e.g. certification path cannot be built or some certificate in the chain + * is expired) + */ + private static PKIXCertPathBuilderResult verifyCertificate( + X509Certificate cert, Set trustAnchors, + Set intermediateCerts, Date signDate) + throws GeneralSecurityException + { + // Create the selector that specifies the starting certificate + X509CertSelector selector = new X509CertSelector(); + selector.setCertificate(cert); + + // Configure the PKIX certificate builder algorithm parameters + PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, selector); + + // Disable CRL checks (this is done manually as additional step) + pkixParams.setRevocationEnabled(false); + + // not doing this brings + // "SunCertPathBuilderException: unable to find valid certification path to requested target" + // (when using -Djava.security.debug=certpath: "critical policy qualifiers present in certificate") + // for files like 021496.pdf that have the "Adobe CDS Certificate Policy" 1.2.840.113583.1.2.1 + // CDS = "Certified Document Services" + // https://www.adobe.com/misc/pdfs/Adobe_CDS_CP.pdf + pkixParams.setPolicyQualifiersRejected(false); + // However, maybe there is still work to do: + // "If the policyQualifiersRejected flag is set to false, it is up to the application + // to validate all policy qualifiers in this manner in order to be PKIX compliant." + + pkixParams.setDate(signDate); + + // Specify a list of intermediate certificates + CertStore intermediateCertStore = CertStore.getInstance("Collection", + new CollectionCertStoreParameters(intermediateCerts)); + pkixParams.addCertStore(intermediateCertStore); + + // Build and verify the certification chain + // If this doesn't work although it should, it can be debugged + // by starting java with -Djava.security.debug=certpath + // see also + // https://docs.oracle.com/javase/8/docs/technotes/guides/security/troubleshooting-security.html + CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); + return (PKIXCertPathBuilderResult) builder.build(pkixParams); + } + + /** + * Extract the OCSP URL from an X.509 certificate if available. + * + * @param cert X.509 certificate + * @return the URL of the OCSP validation service + * @throws IOException + */ + private static String extractOCSPURL(X509Certificate cert) throws IOException + { + byte[] authorityExtensionValue = cert.getExtensionValue(Extension.authorityInfoAccess.getId()); + if (authorityExtensionValue != null) + { + // copied from CertInformationHelper.getAuthorityInfoExtensionValue() + // DRY refactor should be done some day + ASN1Sequence asn1Seq = (ASN1Sequence) JcaX509ExtensionUtils.parseExtensionValue(authorityExtensionValue); + Enumeration objects = asn1Seq.getObjects(); + while (objects.hasMoreElements()) + { + // AccessDescription + ASN1Sequence obj = (ASN1Sequence) objects.nextElement(); + ASN1ObjectIdentifier oid = (ASN1ObjectIdentifier) obj.getObjectAt(0); + // accessLocation + DERTaggedObject location = (DERTaggedObject) obj.getObjectAt(1); + if (oid.equals(X509ObjectIdentifiers.id_ad_ocsp) + && location.getTagNo() == GeneralName.uniformResourceIdentifier) + { + DEROctetString url = (DEROctetString) location.getObject(); + String ocspURL = new String(url.getOctets()); + LOG.info("OCSP URL: " + ocspURL); + return ocspURL; + } + } + } + return null; + } + + /** + * Verify whether the certificate has been revoked at signing date, and verify whether the + * certificate of the responder has been revoked now. + * + * @param ocspHelper the OCSP helper. + * @param additionalCerts + * @throws RevokedCertificateException + * @throws IOException + * @throws OCSPException + * @throws CertificateVerificationException + */ + private static void verifyOCSP(OcspHelper ocspHelper, Set additionalCerts) + throws RevokedCertificateException, IOException, OCSPException, CertificateVerificationException + { + Date now = Calendar.getInstance().getTime(); + OCSPResp ocspResponse; + ocspResponse = ocspHelper.getResponseOcsp(); + if (ocspResponse.getStatus() != OCSPResp.SUCCESSFUL) + { + throw new CertificateVerificationException("OCSP check not successful, status: " + + ocspResponse.getStatus()); + } + LOG.info("OCSP check successful"); + + BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResponse.getResponseObject(); + X509Certificate ocspResponderCertificate = ocspHelper.getOcspResponderCertificate(); + if (ocspResponderCertificate.getExtensionValue(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck.getId()) != null) + { + // https://tools.ietf.org/html/rfc6960#section-4.2.2.2.1 + // A CA may specify that an OCSP client can trust a responder for the + // lifetime of the responder's certificate. The CA does so by + // including the extension id-pkix-ocsp-nocheck. + LOG.info("Revocation check of OCSP responder certificate skipped (id-pkix-ocsp-nocheck is set)"); + return; + } + + LOG.info("Revocation check of OCSP responder certificate"); + Set additionalCerts2 = new HashSet(additionalCerts); + JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); + for (X509CertificateHolder certHolder : basicResponse.getCerts()) + { + try + { + X509Certificate cert = certificateConverter.getCertificate(certHolder); + if (!ocspResponderCertificate.equals(cert)) + { + additionalCerts2.add(cert); + } + } + catch (CertificateException ex) + { + // unlikely to happen because the certificate existed as an object + LOG.error(ex, ex); + } + } + try + { + checkRevocations(ocspResponderCertificate, additionalCerts2, now); + } + catch (GeneralSecurityException ex) + { + throw new CertificateVerificationException(ex.getMessage(), ex); + } + LOG.info("Revocation check of OCSP responder certificate done"); + } +} diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CRLVerifier.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CRLVerifier.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CRLVerifier.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CRLVerifier.java 2018-11-28 17:18:40.000000000 +0000 @@ -0,0 +1,317 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.pdfbox.examples.signature.cert; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.PublicKey; +import java.security.cert.CRLException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509CRL; +import java.security.cert.X509CRLEntry; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.Hashtable; +import java.util.List; +import java.util.Set; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.pdmodel.encryption.SecurityProvider; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.x509.CRLDistPoint; +import org.bouncycastle.asn1.x509.DistributionPoint; +import org.bouncycastle.asn1.x509.DistributionPointName; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; + +/** + * Copied from Apache CXF 2.4.9, initial version: + * https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.9/distribution/src/main/release/samples/sts_issue_operation/src/main/java/demo/sts/provider/cert/ + * + */ +public final class CRLVerifier +{ + private static final Log LOG = LogFactory.getLog(CRLVerifier.class); + + private CRLVerifier() + { + } + + /** + * Extracts the CRL distribution points from the certificate (if available) + * and checks the certificate revocation status against the CRLs coming from + * the distribution points. Supports HTTP, HTTPS, FTP and LDAP based URLs. + * + * @param cert the certificate to be checked for revocation + * @param signDate the date when the signing took place + * @param additionalCerts set of trusted root CA certificates that will be + * used as "trust anchors" and intermediate CA certificates that will be + * used as part of the certification chain. + * @throws CertificateVerificationException if the certificate could not be verified + * @throws RevokedCertificateException if the certificate is revoked + */ + public static void verifyCertificateCRLs(X509Certificate cert, Date signDate, + Set additionalCerts) + throws CertificateVerificationException, RevokedCertificateException + { + try + { + Exception firstException = null; + List crlDistributionPointsURLs = getCrlDistributionPoints(cert); + for (String crlDistributionPointsURL : crlDistributionPointsURLs) + { + LOG.info("Checking distribution point URL: " + crlDistributionPointsURL); + X509CRL crl; + try + { + crl = downloadCRL(crlDistributionPointsURL); + } + catch (Exception ex) + { + // e.g. LDAP behind corporate proxy + LOG.warn("Caught " + ex.getClass().getSimpleName() + " downloading CRL, will try next distribution point if available"); + if (firstException == null) + { + firstException = ex; + } + continue; + } + + // Verify CRL, see wikipedia: + // "To validate a specific CRL prior to relying on it, + // the certificate of its corresponding CA is needed" + PublicKey issuerKey = null; + for (X509Certificate additionalCert : additionalCerts) + { + if (crl.getIssuerX500Principal().equals( + additionalCert.getSubjectX500Principal())) + { + issuerKey = additionalCert.getPublicKey(); + } + } + if (issuerKey == null) + { + throw new CertificateVerificationException( + "Certificate for " + crl.getIssuerX500Principal() + + "not found in certificate chain, so the CRL at " + + crlDistributionPointsURL + " could not be verified"); + } + crl.verify(issuerKey, SecurityProvider.getProvider().getName()); + + checkRevocation(crl, cert, signDate, crlDistributionPointsURL); + + // https://tools.ietf.org/html/rfc5280#section-4.2.1.13 + // If the DistributionPointName contains multiple values, + // each name describes a different mechanism to obtain the same + // CRL. For example, the same CRL could be available for + // retrieval through both LDAP and HTTP. + // + // => thus no need to check several protocols + return; + } + if (firstException != null) + { + throw firstException; + } + } + catch (CertificateVerificationException ex) + { + throw ex; + } + catch (RevokedCertificateException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new CertificateVerificationException( + "Cannot verify CRL for certificate: " + + cert.getSubjectX500Principal(), ex); + + } + } + + /** + * Check whether the certificate was revoked at signing time. + * + * @param crl certificate revocation list + * @param cert certificate to be checked + * @param signDate date the certificate was used for signing + * @param crlDistributionPointsURL URL for log message or exception text + * @throws RevokedCertificateException if the certificate was revoked at signing time + */ + public static void checkRevocation( + X509CRL crl, X509Certificate cert, Date signDate, String crlDistributionPointsURL) + throws RevokedCertificateException + { + X509CRLEntry revokedCRLEntry = crl.getRevokedCertificate(cert); + if (revokedCRLEntry != null && + revokedCRLEntry.getRevocationDate().compareTo(signDate) <= 0) + { + throw new RevokedCertificateException( + "The certificate was revoked by CRL " + + crlDistributionPointsURL + " on " + revokedCRLEntry.getRevocationDate(), + revokedCRLEntry.getRevocationDate()); + } + else if (revokedCRLEntry != null) + { + LOG.info("The certificate was revoked after signing by CRL " + + crlDistributionPointsURL + " on " + revokedCRLEntry.getRevocationDate()); + } + else + { + LOG.info("The certificate was not revoked by CRL " + crlDistributionPointsURL); + } + } + + /** + * Downloads CRL from given URL. Supports http, https, ftp and ldap based URLs. + */ + private static X509CRL downloadCRL(String crlURL) throws IOException, + CertificateException, CRLException, + CertificateVerificationException, NamingException + { + if (crlURL.startsWith("http://") || crlURL.startsWith("https://") + || crlURL.startsWith("ftp://")) + { + return downloadCRLFromWeb(crlURL); + } + else if (crlURL.startsWith("ldap://")) + { + return downloadCRLFromLDAP(crlURL); + } + else + { + throw new CertificateVerificationException( + "Can not download CRL from certificate " + + "distribution point: " + crlURL); + } + } + + /** + * Downloads a CRL from given LDAP url, e.g. + * ldap://ldap.infonotary.com/dc=identity-ca,dc=infonotary,dc=com + */ + private static X509CRL downloadCRLFromLDAP(String ldapURL) throws CertificateException, + NamingException, CRLException, + CertificateVerificationException + { + Hashtable env = new Hashtable(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.PROVIDER_URL, ldapURL); + + // https://docs.oracle.com/javase/jndi/tutorial/ldap/connect/create.html + // don't wait forever behind corporate proxy + env.put("com.sun.jndi.ldap.connect.timeout", "1000"); + + DirContext ctx = new InitialDirContext(env); + Attributes avals = ctx.getAttributes(""); + Attribute aval = avals.get("certificateRevocationList;binary"); + byte[] val = (byte[]) aval.get(); + if (val == null || val.length == 0) + { + throw new CertificateVerificationException("Can not download CRL from: " + ldapURL); + } + else + { + InputStream inStream = new ByteArrayInputStream(val); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509CRL) cf.generateCRL(inStream); + } + } + + /** + * Downloads a CRL from given HTTP/HTTPS/FTP URL, e.g. + * http://crl.infonotary.com/crl/identity-ca.crl + */ + public static X509CRL downloadCRLFromWeb(String crlURL) + throws IOException, CertificateException, CRLException + { + InputStream crlStream = new URL(crlURL).openStream(); + try + { + return (X509CRL) CertificateFactory.getInstance("X.509").generateCRL(crlStream); + } + finally + { + crlStream.close(); + } + } + + /** + * Extracts all CRL distribution point URLs from the "CRL Distribution + * Point" extension in a X.509 certificate. If CRL distribution point + * extension is unavailable, returns an empty list. + * @param cert + * @return List of CRL distribution point URLs. + * @throws java.io.IOException + */ + public static List getCrlDistributionPoints(X509Certificate cert) + throws IOException + { + byte[] crldpExt = cert.getExtensionValue(Extension.cRLDistributionPoints.getId()); + if (crldpExt == null) + { + return new ArrayList(); + } + ASN1InputStream oAsnInStream = new ASN1InputStream(new ByteArrayInputStream(crldpExt)); + ASN1Primitive derObjCrlDP = oAsnInStream.readObject(); + DEROctetString dosCrlDP = (DEROctetString) derObjCrlDP; + byte[] crldpExtOctets = dosCrlDP.getOctets(); + ASN1InputStream oAsnInStream2 = new ASN1InputStream(new ByteArrayInputStream(crldpExtOctets)); + ASN1Primitive derObj2 = oAsnInStream2.readObject(); + CRLDistPoint distPoint = CRLDistPoint.getInstance(derObj2); + List crlUrls = new ArrayList(); + for (DistributionPoint dp : distPoint.getDistributionPoints()) + { + DistributionPointName dpn = dp.getDistributionPoint(); + // Look for URIs in fullName + if (dpn != null && dpn.getType() == DistributionPointName.FULL_NAME) + { + // Look for an URI + for (GeneralName genName : GeneralNames.getInstance(dpn.getName()).getNames()) + { + if (genName.getTagNo() == GeneralName.uniformResourceIdentifier) + { + String url = DERIA5String.getInstance(genName.getName()).getString(); + crlUrls.add(url); + } + } + } + } + return crlUrls; + } +} diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/OcspHelper.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/OcspHelper.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/OcspHelper.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/OcspHelper.java 2018-11-28 17:18:40.000000000 +0000 @@ -0,0 +1,552 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.examples.signature.cert; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; +import java.util.Random; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.examples.signature.SigUtils; +import org.apache.pdfbox.io.IOUtils; +import org.apache.pdfbox.pdmodel.encryption.SecurityProvider; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DLSequence; +import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; +import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; +import org.bouncycastle.asn1.ocsp.ResponderID; +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.CertificateID; +import org.bouncycastle.cert.ocsp.CertificateStatus; +import org.bouncycastle.cert.ocsp.OCSPException; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPReqBuilder; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.RevokedStatus; +import org.bouncycastle.cert.ocsp.SingleResp; +import org.bouncycastle.operator.ContentVerifierProvider; +import org.bouncycastle.operator.DigestCalculator; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; + +/** + * Helper Class for OCSP-Operations with bouncy castle. + * + * @author Alexis Suter + */ +public class OcspHelper +{ + private static final Log LOG = LogFactory.getLog(OcspHelper.class); + + private final X509Certificate issuerCertificate; + private final Date signDate; + private final X509Certificate certificateToCheck; + private final Set additionalCerts; + private final String ocspUrl; + private DEROctetString encodedNonce; + private X509Certificate ocspResponderCertificate; + private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); + + /** + * @param checkCertificate Certificate to be OCSP-checked + * @param signDate the date when the signing took place + * @param issuerCertificate Certificate of the issuer + * @param additionalCerts Set of trusted root CA certificates that will be used as "trust + * anchors" and intermediate CA certificates that will be used as part of the certification + * chain. All self-signed certificates are considered to be trusted root CA certificates. All + * the rest are considered to be intermediate CA certificates. + * @param ocspUrl where to fetch for OCSP + */ + public OcspHelper(X509Certificate checkCertificate, Date signDate, X509Certificate issuerCertificate, + Set additionalCerts, String ocspUrl) + { + this.certificateToCheck = checkCertificate; + this.signDate = signDate; + this.issuerCertificate = issuerCertificate; + this.additionalCerts = additionalCerts; + this.ocspUrl = ocspUrl; + } + + /** + * Performs and verifies the OCSP-Request + * + * @return the OCSPResp, when the request was successful, else a corresponding exception will be + * thrown. Never returns null. + * + * @throws IOException + * @throws OCSPException + * @throws RevokedCertificateException + */ + public OCSPResp getResponseOcsp() throws IOException, OCSPException, RevokedCertificateException + { + OCSPResp ocspResponse = performRequest(); + verifyOcspResponse(ocspResponse); + return ocspResponse; + } + + /** + * Get responder certificate. This is available after {@link #getResponseOcsp()} has been called. + * + * @return The certificate of the responder. + */ + public X509Certificate getOcspResponderCertificate() + { + return ocspResponderCertificate; + } + + /** + * Verifies the status and the response itself (including nonce), but not the signature. + * + * @param ocspResponse to be verified + * @throws OCSPException + * @throws RevokedCertificateException + * @throws IOException if the default security provider can't be instantiated + */ + private void verifyOcspResponse(OCSPResp ocspResponse) + throws OCSPException, RevokedCertificateException, IOException + { + verifyRespStatus(ocspResponse); + + BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResponse.getResponseObject(); + if (basicResponse != null) + { + ResponderID responderID = basicResponse.getResponderId().toASN1Primitive(); + // https://tools.ietf.org/html/rfc6960#section-4.2.2.3 + // The basic response type contains: + // (...) + // either the name of the responder or a hash of the responder's + // public key as the ResponderID + // (...) + // The responder MAY include certificates in the certs field of + // BasicOCSPResponse that help the OCSP client verify the responder's + // signature. + X500Name name = responderID.getName(); + if (name != null) + { + findResponderCertificateByName(basicResponse, name); + } + else + { + byte[] keyHash = responderID.getKeyHash(); + //TODO + // KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key + // -- (i.e., the SHA-1 hash of the value of the + // -- BIT STRING subjectPublicKey [excluding + // -- the tag, length, and number of unused + // -- bits] in the responder's certificate) + throw new UnsupportedOperationException("search by key hash is not implemented yet"); + + // how BC calculates the HeyHash: + // see CertificateID.createCertID() + // digCalc is a SHA1DigestCalculator + // SubjectPublicKeyInfo info = issuerCert.getSubjectPublicKeyInfo(); + // dgOut = digCalc.getOutputStream(); + // dgOut.write(info.getPublicKeyData().getBytes()); + // dgOut.close(); + // ASN1OctetString issuerKeyHash = new DEROctetString(digCalc.getDigest()); + } + + if (ocspResponderCertificate == null) + { + throw new OCSPException("OCSP: certificate for responder " + name + " not found"); + } + + try + { + SigUtils.checkResponderCertificateUsage(ocspResponderCertificate); + } + catch (CertificateParsingException ex) + { + // unlikely to happen because the certificate existed as an object + LOG.error(ex, ex); + } + checkOcspSignature(ocspResponderCertificate, basicResponse); + + boolean nonceChecked = checkNonce(basicResponse); + + SingleResp[] responses = basicResponse.getResponses(); + if (responses.length == 1) + { + SingleResp resp = responses[0]; + Object status = resp.getCertStatus(); + + if (!nonceChecked) + { + // https://tools.ietf.org/html/rfc5019 + // fall back to validating the OCSPResponse based on time + checkOcspResponseFresh(resp); + } + + if (status instanceof RevokedStatus) + { + RevokedStatus revokedStatus = (RevokedStatus) status; + if (revokedStatus.getRevocationTime().compareTo(signDate) <= 0) + { + throw new RevokedCertificateException( + "OCSP: Certificate is revoked since " + + revokedStatus.getRevocationTime(), + revokedStatus.getRevocationTime()); + } + LOG.info("The certificate was revoked after signing by OCSP " + ocspUrl + + " on " + revokedStatus.getRevocationTime()); + } + else if (status != CertificateStatus.GOOD) + { + throw new OCSPException("OCSP: Status of Cert is unknown"); + } + } + else + { + throw new OCSPException( + "OCSP: Received " + responses.length + " responses instead of 1!"); + } + } + } + + private void findResponderCertificateByName(BasicOCSPResp basicResponse, X500Name name) + { + X509CertificateHolder[] certHolders = basicResponse.getCerts(); + for (X509CertificateHolder certHolder : certHolders) + { + if (name.equals(certHolder.getSubject())) + { + try + { + ocspResponderCertificate = certificateConverter.getCertificate(certHolder); + } + catch (CertificateException ex) + { + // unlikely to happen because the certificate existed as an object + LOG.error(ex, ex); + } + break; + } + } + if (ocspResponderCertificate == null) + { + // DO NOT use the certificate found in additionalCerts first. One file had a + // responder certificate in the PDF itself with SHA1withRSA algorithm, but + // the responder delivered a different (newer, more secure) certificate + // with SHA256withRSA (tried with QV_RCA1_RCA3_CPCPS_V4_11.pdf) + // https://www.quovadisglobal.com/~/media/Files/Repository/QV_RCA1_RCA3_CPCPS_V4_11.ashx + for (X509Certificate cert : additionalCerts) + { + X500Name certSubjectName = new X500Name(cert.getSubjectX500Principal().getName()); + if (certSubjectName.equals(name)) + { + ocspResponderCertificate = cert; + break; + } + } + } + } + + private void checkOcspResponseFresh(SingleResp resp) throws OCSPException + { + // https://tools.ietf.org/html/rfc5019 + // Clients MUST check for the existence of the nextUpdate field and MUST + // ensure the current time, expressed in GMT time as described in + // Section 2.2.4, falls between the thisUpdate and nextUpdate times. If + // the nextUpdate field is absent, the client MUST reject the response. + + Date curDate = Calendar.getInstance().getTime(); + + Date thisUpdate = resp.getThisUpdate(); + if (thisUpdate == null) + { + throw new OCSPException("OCSP: thisUpdate field is missing in response (RFC 5019 2.2.4.)"); + } + Date nextUpdate = resp.getNextUpdate(); + if (nextUpdate == null) + { + throw new OCSPException("OCSP: nextUpdate field is missing in response (RFC 5019 2.2.4.)"); + } + if (curDate.compareTo(thisUpdate) < 0) + { + LOG.error(curDate + " < " + thisUpdate); + throw new OCSPException("OCSP: current date < thisUpdate field (RFC 5019 2.2.4.)"); + } + if (curDate.compareTo(nextUpdate) > 0) + { + LOG.error(curDate + " > " + nextUpdate); + throw new OCSPException("OCSP: current date > nextUpdate field (RFC 5019 2.2.4.)"); + } + LOG.info("OCSP response is fresh"); + } + + /** + * Checks whether the OCSP response is signed by the given certificate. + * + * @param certificate the certificate to check the signature + * @param basicResponse OCSP response containing the signature + * @throws OCSPException when the signature is invalid or could not be checked + * @throws IOException if the default security provider can't be instantiated + */ + private void checkOcspSignature(X509Certificate certificate, BasicOCSPResp basicResponse) + throws OCSPException, IOException + { + try + { + ContentVerifierProvider verifier = new JcaContentVerifierProviderBuilder() + .setProvider(SecurityProvider.getProvider()).build(certificate); + + if (!basicResponse.isSignatureValid(verifier)) + { + throw new OCSPException("OCSP-Signature is not valid!"); + } + } + catch (OperatorCreationException e) + { + throw new OCSPException("Error checking Ocsp-Signature", e); + } + } + + /** + * Checks if the nonce in the response matches. + * + * @param basicResponse Response to be checked + * @return true if the nonce is present and matches, false if nonce is missing. + * @throws OCSPException if the nonce is different + */ + private boolean checkNonce(BasicOCSPResp basicResponse) throws OCSPException + { + Extension nonceExt = basicResponse.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce); + if (nonceExt != null) + { + DEROctetString responseNonceString = (DEROctetString) nonceExt.getExtnValue(); + if (!responseNonceString.equals(encodedNonce)) + { + throw new OCSPException("Different nonce found in response!"); + } + else + { + LOG.info("Nonce is good"); + return true; + } + } + // https://tools.ietf.org/html/rfc5019 + // Clients that opt to include a nonce in the + // request SHOULD NOT reject a corresponding OCSPResponse solely on the + // basis of the nonexistent expected nonce, but MUST fall back to + // validating the OCSPResponse based on time. + return false; + } + + /** + * Performs the OCSP-Request, with given data. + * + * @return the OCSPResp, that has been fetched from the ocspUrl + * @throws IOException + * @throws OCSPException + */ + private OCSPResp performRequest() throws IOException, OCSPException + { + OCSPReq request = generateOCSPRequest(); + URL url = new URL(ocspUrl); + HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); + try + { + httpConnection.setRequestProperty("Content-Type", "application/ocsp-request"); + httpConnection.setRequestProperty("Accept", "application/ocsp-response"); + httpConnection.setDoOutput(true); + OutputStream out = httpConnection.getOutputStream(); + try + { + out.write(request.getEncoded()); + } + finally + { + IOUtils.closeQuietly(out); + } + + if (httpConnection.getResponseCode() != 200) + { + throw new IOException("OCSP: Could not access url, ResponseCode: " + + httpConnection.getResponseCode()); + } + // Get response + InputStream in = (InputStream) httpConnection.getContent(); + try + { + return new OCSPResp(in); + } + finally + { + IOUtils.closeQuietly(in); + } + } + finally + { + httpConnection.disconnect(); + } + } + + /** + * Helper method to verify response status. + * + * @param resp OCSP response + * @throws OCSPException if the response status is not ok + */ + public void verifyRespStatus(OCSPResp resp) throws OCSPException + { + String statusInfo = ""; + if (resp != null) + { + int status = resp.getStatus(); + switch (status) + { + case OCSPResponseStatus.INTERNAL_ERROR: + statusInfo = "INTERNAL_ERROR"; + LOG.error("An internal error occurred in the OCSP Server!"); + break; + case OCSPResponseStatus.MALFORMED_REQUEST: + // This happened when the "critical" flag was used for extensions + // on a responder known by the committer of this comment. + statusInfo = "MALFORMED_REQUEST"; + LOG.error("Your request did not fit the RFC 2560 syntax!"); + break; + case OCSPResponseStatus.SIG_REQUIRED: + statusInfo = "SIG_REQUIRED"; + LOG.error("Your request was not signed!"); + break; + case OCSPResponseStatus.TRY_LATER: + statusInfo = "TRY_LATER"; + LOG.error("The server was too busy to answer you!"); + break; + case OCSPResponseStatus.UNAUTHORIZED: + statusInfo = "UNAUTHORIZED"; + LOG.error("The server could not authenticate you!"); + break; + case OCSPResponseStatus.SUCCESSFUL: + break; + default: + statusInfo = "UNKNOWN"; + LOG.error("Unknown OCSPResponse status code! " + status); + } + } + if (resp == null || resp.getStatus() != OCSPResponseStatus.SUCCESSFUL) + { + throw new OCSPException("OCSP response unsuccessful, status: " + statusInfo); + } + } + + /** + * Generates an OCSP request and generates the CertificateID. + * + * @return OCSP request, ready to fetch data + * @throws OCSPException + * @throws IOException + */ + private OCSPReq generateOCSPRequest() throws OCSPException, IOException + { + Security.addProvider(SecurityProvider.getProvider()); + + // Generate the ID for the certificate we are looking for + CertificateID certId; + try + { + certId = new CertificateID(new SHA1DigestCalculator(), + new JcaX509CertificateHolder(issuerCertificate), + certificateToCheck.getSerialNumber()); + } + catch (CertificateEncodingException e) + { + throw new IOException("Error creating CertificateID with the Certificate encoding", e); + } + + // https://tools.ietf.org/html/rfc2560#section-4.1.2 + // Support for any specific extension is OPTIONAL. The critical flag + // SHOULD NOT be set for any of them. + + Extension responseExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_response, + false, new DLSequence(OCSPObjectIdentifiers.id_pkix_ocsp_basic).getEncoded()); + + Random rand = new Random(); + byte[] nonce = new byte[16]; + rand.nextBytes(nonce); + encodedNonce = new DEROctetString(new DEROctetString(nonce)); + Extension nonceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, + encodedNonce); + + OCSPReqBuilder builder = new OCSPReqBuilder(); + builder.setRequestExtensions( + new Extensions(new Extension[] { responseExtension, nonceExtension })); + builder.addRequest(certId); + return builder.build(); + } + + /** + * Class to create SHA-1 Digest, used for creation of CertificateID. + */ + private static class SHA1DigestCalculator implements DigestCalculator + { + private final ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + + @Override + public AlgorithmIdentifier getAlgorithmIdentifier() + { + return new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1); + } + + @Override + public OutputStream getOutputStream() + { + return bOut; + } + + @Override + public byte[] getDigest() + { + byte[] bytes = bOut.toByteArray(); + bOut.reset(); + + try + { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + return md.digest(bytes); + } + catch (NoSuchAlgorithmException e) + { + LOG.error("SHA-1 Algorithm not found", e); + return null; + } + } + } +} diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/RevokedCertificateException.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/RevokedCertificateException.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/RevokedCertificateException.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/RevokedCertificateException.java 2018-11-28 17:18:40.000000000 +0000 @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.examples.signature.cert; + +import java.util.Date; + +/** + * Exception to handle a revoked Certificate explicitly + * + * @author Alexis Suter + */ +public class RevokedCertificateException extends Exception +{ + private static final long serialVersionUID = 3543946618794126654L; + + private final Date revocationTime; + + public RevokedCertificateException(String message) + { + super(message); + this.revocationTime = null; + } + + public RevokedCertificateException(String message, Date revocationTime) + { + super(message); + this.revocationTime = revocationTime; + } + + public Date getRevocationTime() + { + return revocationTime; + } +} diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java 2018-11-28 17:18:40.000000000 +0000 @@ -27,12 +27,9 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; -import java.util.List; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; -import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSSignedData; @@ -42,7 +39,6 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; -import org.bouncycastle.util.Store; public abstract class CreateSignatureBase implements SignatureInterface { @@ -87,6 +83,8 @@ { // avoid expired certificate ((X509Certificate) cert).checkValidity(); + + SigUtils.checkCertificateUsage((X509Certificate) cert); } break; } @@ -130,14 +128,11 @@ // cannot be done private (interface) try { - List certList = new ArrayList(); - certList.addAll(Arrays.asList(certificateChain)); - Store certs = new JcaCertStore(certList); CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); - org.bouncycastle.asn1.x509.Certificate cert = org.bouncycastle.asn1.x509.Certificate.getInstance(certificateChain[0].getEncoded()); + X509Certificate cert = (X509Certificate) certificateChain[0]; ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey); - gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, new X509CertificateHolder(cert))); - gen.addCertificates(certs); + gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert)); + gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain))); CMSProcessableInputStream msg = new CMSProcessableInputStream(content); CMSSignedData signedData = gen.generate(msg, false); if (tsaUrl != null && tsaUrl.length() > 0) diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature2.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature2.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature2.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature2.java 2018-11-28 17:18:40.000000000 +0000 @@ -259,7 +259,7 @@ // PDDocument object anymore, with classic java file random access methods. // If you can't remember the offset value from ByteRange because your context has changed, // then open the file with PDFBox, find the field with findExistingSignature() or - // PODDocument.getLastSignatureDictionary() and get the ByteRange from there. + // PDDocument.getLastSignatureDictionary() and get the ByteRange from there. // Close the file and then write the signature as explained earlier in this comment. if (isLateExternalSigning()) { diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature.java 2018-11-28 17:18:40.000000000 +0000 @@ -291,7 +291,7 @@ // PDDocument object anymore, with classic java file random access methods. // If you can't remember the offset value from ByteRange because your context has changed, // then open the file with PDFBox, find the field with findExistingSignature() or - // PODDocument.getLastSignatureDictionary() and get the ByteRange from there. + // PDDocument.getLastSignatureDictionary() and get the ByteRange from there. // Close the file and then write the signature as explained earlier in this comment. if (isLateExternalSigning()) { diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/ShowSignature.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/ShowSignature.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/ShowSignature.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/ShowSignature.java 2018-11-28 17:18:40.000000000 +0000 @@ -20,55 +20,71 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.security.InvalidKeyException; +import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PublicKey; -import java.security.SignatureException; +import java.security.Security; import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateFactory; +import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; - import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSInputStream; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSObject; import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.cos.COSString; +import org.apache.pdfbox.examples.signature.cert.CertificateVerificationException; +import org.apache.pdfbox.examples.signature.cert.CertificateVerifier; import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.encryption.SecurityProvider; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.util.Hex; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.cms.Attribute; +import org.bouncycastle.asn1.cms.AttributeTable; +import org.bouncycastle.asn1.cms.CMSAttributes; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.Time; import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSProcessable; import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationVerifier; import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.tsp.TSPException; import org.bouncycastle.tsp.TimeStampToken; +import org.bouncycastle.util.Selector; import org.bouncycastle.util.Store; -import org.bouncycastle.util.StoreException; /** - * This will read a document from the filesystem, decrypt it and do something with the signature. + * This will get the signature(s) from the document, do some verifications and + * show the signature(s) and the certificates. This is a complex topic - the + * code here is an example and not a production-ready solution. * * @author Ben Litchfield */ public final class ShowSignature { private final SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss"); - + private ShowSignature() { } @@ -79,24 +95,22 @@ * @param args The command-line arguments. * * @throws IOException If there is an error reading the file. - * @throws CertificateException - * @throws java.security.NoSuchAlgorithmException - * @throws java.security.NoSuchProviderException * @throws org.bouncycastle.tsp.TSPException + * @throws java.security.GeneralSecurityException + * @throws org.apache.pdfbox.examples.signature.cert.CertificateVerificationException */ - public static void main(String[] args) throws IOException, CertificateException, - NoSuchAlgorithmException, - NoSuchProviderException, - TSPException + public static void main(String[] args) throws IOException, TSPException, GeneralSecurityException, + CertificateVerificationException { + // register BouncyCastle provider, needed for "exotic" algorithms + Security.addProvider(SecurityProvider.getProvider()); + ShowSignature show = new ShowSignature(); show.showSignature( args ); } - private void showSignature(String[] args) throws IOException, CertificateException, - NoSuchAlgorithmException, - NoSuchProviderException, - TSPException + private void showSignature(String[] args) throws IOException, TSPException, GeneralSecurityException, + CertificateVerificationException { if( args.length != 2 ) { @@ -168,8 +182,6 @@ subFilter.equals("ETSI.CAdES.detached")) { verifyPKCS7(buf, contents, sig); - - //TODO check certificate chain, revocation lists, timestamp... } else if (subFilter.equals("adbe.pkcs7.sha1")) { @@ -182,13 +194,12 @@ byte[] hash = MessageDigest.getInstance("SHA1").digest(buf); verifyPKCS7(hash, contents, sig); - - //TODO check certificate chain, revocation lists, timestamp... } else if (subFilter.equals("adbe.x509.rsa_sha1")) { // example: PDFBOX-2693.pdf COSString certString = (COSString) sigDict.getDictionaryObject(COSName.CERT); + //TODO this could also be an array. if (certString == null) { System.err.println("The /Cert certificate string is missing in the signature dictionary"); @@ -199,21 +210,52 @@ ByteArrayInputStream certStream = new ByteArrayInputStream(certData); Collection certs = factory.generateCertificates(certStream); System.out.println("certs=" + certs); - - //TODO verify signature + + X509Certificate cert = (X509Certificate) certs.iterator().next(); + + // to verify signature, see code at + // https://stackoverflow.com/questions/43383859/ + + try + { + if (sig.getSignDate() != null) + { + cert.checkValidity(sig.getSignDate().getTime()); + System.out.println("Certificate valid at signing time"); + } + else + { + System.err.println("Certificate cannot be verified without signing time"); + } + } + catch (CertificateExpiredException ex) + { + System.err.println("Certificate expired at signing time"); + } + catch (CertificateNotYetValidException ex) + { + System.err.println("Certificate not yet valid at signing time"); + } + if (CertificateVerifier.isSelfSigned(cert)) + { + System.err.println("Certificate is self-signed, LOL!"); + } + else + { + System.out.println("Certificate is not self-signed"); + + if (sig.getSignDate() != null) + { + verifyCertificateChain(new JcaCertStore(certs), + cert, + sig.getSignDate().getTime()); + } + } } else if (subFilter.equals("ETSI.RFC3161")) { - TimeStampToken timeStampToken = new TimeStampToken(new CMSSignedData(contents.getBytes())); - System.out.println("Time stamp gen time: " + timeStampToken.getTimeStampInfo().getGenTime()); - System.out.println("Time stamp tsa name: " + timeStampToken.getTimeStampInfo().getTsa().getName()); - - CertificateFactory factory = CertificateFactory.getInstance("X.509"); - ByteArrayInputStream certStream = new ByteArrayInputStream(contents.getBytes()); - Collection certs = factory.generateCertificates(certStream); - System.out.println("certs=" + certs); - - //TODO verify signature + // e.g. PDFBOX-1848, file_timestamped.pdf + verifyETSIdotRFC3161(buf, contents); } else { @@ -246,46 +288,140 @@ } } + private void verifyETSIdotRFC3161(byte[] buf, COSString contents) + throws CertificateException, CMSException, IOException, OperatorCreationException, + TSPException, NoSuchAlgorithmException, CertificateVerificationException + { + TimeStampToken timeStampToken = new TimeStampToken(new CMSSignedData(contents.getBytes())); + System.out.println("Time stamp gen time: " + timeStampToken.getTimeStampInfo().getGenTime()); + System.out.println("Time stamp tsa name: " + timeStampToken.getTimeStampInfo().getTsa().getName()); + + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream certStream = new ByteArrayInputStream(contents.getBytes()); + Collection certs = factory.generateCertificates(certStream); + System.out.println("certs=" + certs); + + String hashAlgorithm = timeStampToken.getTimeStampInfo().getMessageImprintAlgOID().getId(); + // compare the hash of the signed content with the hash in + // the timestamp + if (Arrays.equals(MessageDigest.getInstance(hashAlgorithm).digest(buf), + timeStampToken.getTimeStampInfo().getMessageImprintDigest())) + { + System.out.println("ETSI.RFC3161 timestamp signature verified"); + } + else + { + System.err.println("ETSI.RFC3161 timestamp signature verification failed"); + } + + X509Certificate certFromTimeStamp = (X509Certificate) certs.iterator().next(); + SigUtils.checkTimeStampCertificateUsage(certFromTimeStamp); + validateTimestampToken(timeStampToken); + verifyCertificateChain(timeStampToken.getCertificates(), + certFromTimeStamp, + timeStampToken.getTimeStampInfo().getGenTime()); + } + /** * Verify a PKCS7 signature. * * @param byteArray the byte sequence that has been signed * @param contents the /Contents field as a COSString * @param sig the PDF signature (the /V dictionary) - * @throws CertificateException * @throws CMSException - * @throws StoreException * @throws OperatorCreationException + * @throws IOException + * @throws GeneralSecurityException + * @throws TSPException */ private void verifyPKCS7(byte[] byteArray, COSString contents, PDSignature sig) - throws CMSException, CertificateException, StoreException, OperatorCreationException, - NoSuchAlgorithmException, NoSuchProviderException + throws CMSException, OperatorCreationException, + IOException, GeneralSecurityException, TSPException, CertificateVerificationException { // inspiration: // http://stackoverflow.com/a/26702631/535646 // http://stackoverflow.com/a/9261365/535646 CMSProcessable signedContent = new CMSProcessableByteArray(byteArray); CMSSignedData signedData = new CMSSignedData(signedContent, contents.getBytes()); + @SuppressWarnings("unchecked") Store certificatesStore = signedData.getCertificates(); Collection signers = signedData.getSignerInfos().getSigners(); SignerInformation signerInformation = signers.iterator().next(); - Collection matches = certificatesStore.getMatches(signerInformation.getSID()); + @SuppressWarnings("unchecked") + Collection matches = + certificatesStore.getMatches((Selector) signerInformation.getSID()); X509CertificateHolder certificateHolder = matches.iterator().next(); X509Certificate certFromSignedData = new JcaX509CertificateConverter().getCertificate(certificateHolder); System.out.println("certFromSignedData: " + certFromSignedData); - certFromSignedData.checkValidity(sig.getSignDate().getTime()); - if (isSelfSigned(certFromSignedData)) + SigUtils.checkCertificateUsage(certFromSignedData); + + // Embedded timestamp + TimeStampToken timeStampToken = extractTimeStampTokenFromSignerInformation(signerInformation); + if (timeStampToken != null) + { + // tested with QV_RCA1_RCA3_CPCPS_V4_11.pdf + // https://www.quovadisglobal.com/~/media/Files/Repository/QV_RCA1_RCA3_CPCPS_V4_11.ashx + // timeStampToken.getCertificates() only contained the local certificate and not + // the whole chain, so use the store of the main signature. + // (If this assumption is incorrect, then the code must be changed to merge + // both stores, or to pass a collection) + validateTimestampToken(timeStampToken); + X509CertificateHolder tstCertHolder = (X509CertificateHolder) timeStampToken.getCertificates().getMatches(null).iterator().next(); + X509Certificate certFromTimeStamp = new JcaX509CertificateConverter().getCertificate(tstCertHolder); + verifyCertificateChain(certificatesStore, + certFromTimeStamp, + timeStampToken.getTimeStampInfo().getGenTime()); + SigUtils.checkTimeStampCertificateUsage(certFromTimeStamp); + } + + try { - System.err.println("Certificate is self-signed, LOL!"); + if (sig.getSignDate() != null) + { + certFromSignedData.checkValidity(sig.getSignDate().getTime()); + System.out.println("Certificate valid at signing time"); + } + else + { + System.err.println("Certificate cannot be verified without signing time"); + } } - else + catch (CertificateExpiredException ex) { - System.out.println("Certificate is not self-signed"); - // todo rest of chain + System.err.println("Certificate expired at signing time"); + } + catch (CertificateNotYetValidException ex) + { + System.err.println("Certificate not yet valid at signing time"); } - if (signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(certFromSignedData))) + // usually not available + if (signerInformation.getSignedAttributes() != null) + { + // From SignedMailValidator.getSignatureTime() + Attribute signingTime = signerInformation.getSignedAttributes().get(CMSAttributes.signingTime); + if (signingTime != null) + { + Time timeInstance = Time.getInstance(signingTime.getAttrValues().getObjectAt(0)); + try + { + certFromSignedData.checkValidity(timeInstance.getDate()); + System.out.println("Certificate valid at signing time: " + timeInstance.getDate()); + } + catch (CertificateExpiredException ex) + { + System.err.println("Certificate expired at signing time"); + } + catch (CertificateNotYetValidException ex) + { + System.err.println("Certificate not yet valid at signing time"); + } + } + } + + if (signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder(). + setProvider(SecurityProvider.getProvider()).build(certFromSignedData))) { System.out.println("Signature verified"); } @@ -293,6 +429,75 @@ { System.out.println("Signature verification failed"); } + + if (CertificateVerifier.isSelfSigned(certFromSignedData)) + { + System.err.println("Certificate is self-signed, LOL!"); + } + else + { + System.out.println("Certificate is not self-signed"); + + if (sig.getSignDate() != null) + { + verifyCertificateChain(certificatesStore, certFromSignedData, sig.getSignDate().getTime()); + } + else + { + System.err.println("Certificate cannot be verified without signing time"); + } + } + } + + private TimeStampToken extractTimeStampTokenFromSignerInformation(SignerInformation signerInformation) + throws CMSException, IOException, TSPException + { + if (signerInformation.getUnsignedAttributes() == null) + { + return null; + } + AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); + // https://stackoverflow.com/questions/1647759/how-to-validate-if-a-signed-jar-contains-a-timestamp + Attribute attribute = unsignedAttributes.get( + PKCSObjectIdentifiers.id_aa_signatureTimeStampToken); + ASN1Object obj = (ASN1Object) attribute.getAttrValues().getObjectAt(0); + CMSSignedData signedTSTData = new CMSSignedData(obj.getEncoded()); + return new TimeStampToken(signedTSTData); + } + + private void verifyCertificateChain(Store certificatesStore, + X509Certificate certFromSignedData, Date signDate) + throws CertificateVerificationException, CertificateException + { + // Verify certificate chain (new since 11/2018) + // Please post bad PDF files that succeed and + // good PDF files that fail in + // https://issues.apache.org/jira/browse/PDFBOX-3017 + Collection certificateHolders = certificatesStore.getMatches(null); + Set additionalCerts = new HashSet(); + JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); + for (X509CertificateHolder certHolder : certificateHolders) + { + X509Certificate certificate = certificateConverter.getCertificate(certHolder); + if (!certificate.equals(certFromSignedData)) + { + additionalCerts.add(certificate); + } + } + CertificateVerifier.verifyCertificate(certFromSignedData, additionalCerts, true, signDate); + } + + private void validateTimestampToken(TimeStampToken timeStampToken) + throws IOException, CertificateException, TSPException, OperatorCreationException + { + // https://stackoverflow.com/questions/42114742/ + Collection tstMatches = + timeStampToken.getCertificates().getMatches(timeStampToken.getSID()); + X509CertificateHolder holder = tstMatches.iterator().next(); + X509Certificate tstCert = new JcaX509CertificateConverter().getCertificate(holder); + SignerInformationVerifier siv = new JcaSimpleSignerInfoVerifierBuilder().setProvider(SecurityProvider.getProvider()).build(tstCert); + timeStampToken.validate(siv); + System.out.println("TimeStampToken validated"); } /** @@ -354,31 +559,6 @@ } } - // https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.1/distribution/src/main/release/samples/sts_issue_operation/src/main/java/demo/sts/provider/cert/CertificateVerifier.java - - /** - * Checks whether given X.509 certificate is self-signed. - */ - private boolean isSelfSigned(X509Certificate cert) - throws CertificateException, NoSuchAlgorithmException, NoSuchProviderException - { - try - { - // Try to verify certificate signature with its own public key - PublicKey key = cert.getPublicKey(); - cert.verify(key); - return true; - } - catch (SignatureException sigEx) - { - return false; - } - catch (InvalidKeyException keyEx) - { - return false; - } - } - /** * This will print a usage message. */ diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/SigUtils.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/SigUtils.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/SigUtils.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/SigUtils.java 2018-11-28 17:18:40.000000000 +0000 @@ -16,12 +16,21 @@ package org.apache.pdfbox.examples.signature; +import java.io.IOException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.bouncycastle.asn1.x509.KeyPurposeId; /** * Utility class for the signature / timestamp examples. @@ -30,6 +39,8 @@ */ public class SigUtils { + private static final Log LOG = LogFactory.getLog(SigUtils.class); + private SigUtils() { } @@ -126,4 +137,103 @@ catalogDict.setNeedToBeUpdated(true); permsDict.setNeedToBeUpdated(true); } + + /** + * Log if the certificate is not valid for signature usage. Doing this + * anyway results in Adobe Reader failing to validate the PDF. + * + * @param x509Certificate + * @throws java.security.cert.CertificateParsingException + */ + public static void checkCertificateUsage(X509Certificate x509Certificate) + throws CertificateParsingException + { + // Check whether signer certificate is "valid for usage" + // https://stackoverflow.com/a/52765021/535646 + // https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSig/changes.html#id1 + boolean[] keyUsage = x509Certificate.getKeyUsage(); + if (keyUsage != null && !keyUsage[0] && !keyUsage[1]) + { + // (unclear what "signTransaction" is) + // https://tools.ietf.org/html/rfc5280#section-4.2.1.3 + LOG.error("Certificate key usage does not include " + + "digitalSignature nor nonRepudiation"); + } + List extendedKeyUsage = x509Certificate.getExtendedKeyUsage(); + if (extendedKeyUsage != null && + !extendedKeyUsage.contains(KeyPurposeId.id_kp_emailProtection.toString()) && + !extendedKeyUsage.contains(KeyPurposeId.id_kp_codeSigning.toString()) && + !extendedKeyUsage.contains(KeyPurposeId.anyExtendedKeyUsage.toString()) && + !extendedKeyUsage.contains("1.2.840.113583.1.1.5") && + // not mentioned in Adobe document, but tolerated in practice + !extendedKeyUsage.contains("1.3.6.1.4.1.311.10.3.12")) + { + LOG.error("Certificate extended key usage does not include " + + "emailProtection, nor codeSigning, nor anyExtendedKeyUsage, " + + "nor 'Adobe Authentic Documents Trust'"); + } + } + + /** + * Log if the certificate is not valid for timestamping. + * + * @param x509Certificate + * @throws java.security.cert.CertificateParsingException + */ + public static void checkTimeStampCertificateUsage(X509Certificate x509Certificate) + throws CertificateParsingException + { + List extendedKeyUsage = x509Certificate.getExtendedKeyUsage(); + // https://tools.ietf.org/html/rfc5280#section-4.2.1.12 + if (extendedKeyUsage != null && + !extendedKeyUsage.contains(KeyPurposeId.id_kp_timeStamping.toString())) + { + LOG.error("Certificate extended key usage does not include timeStamping"); + } + } + + /** + * Log if the certificate is not valid for responding. + * + * @param x509Certificate + * @throws java.security.cert.CertificateParsingException + */ + public static void checkResponderCertificateUsage(X509Certificate x509Certificate) + throws CertificateParsingException + { + List extendedKeyUsage = x509Certificate.getExtendedKeyUsage(); + // https://tools.ietf.org/html/rfc5280#section-4.2.1.12 + if (extendedKeyUsage != null && + !extendedKeyUsage.contains(KeyPurposeId.id_kp_OCSPSigning.toString())) + { + LOG.error("Certificate extended key usage does not include OCSP responding"); + } + } + + /** + * Gets the last relevant signature in the document, i.e. the one with the highest offset. + * + * @param document to get its last signature + * @return last signature or null when none found + * @throws IOException + */ + public static PDSignature getLastRelevantSignature(PDDocument document) throws IOException + { + SortedMap sortedMap = new TreeMap(); + for (PDSignature signature : document.getSignatureDictionaries()) + { + int sigOffset = signature.getByteRange()[1]; + sortedMap.put(sigOffset, signature); + } + if (sortedMap.size() > 0) + { + PDSignature lastSignature = sortedMap.get(sortedMap.lastKey()); + COSBase type = lastSignature.getCOSObject().getItem(COSName.TYPE); + if (type.equals(COSName.SIG) || type.equals(COSName.DOC_TIME_STAMP)) + { + return lastSignature; + } + } + return null; + } } diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/AddValidationInformation.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/AddValidationInformation.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/AddValidationInformation.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/AddValidationInformation.java 2018-11-28 17:18:40.000000000 +0000 @@ -24,9 +24,11 @@ import java.io.OutputStream; import java.math.BigInteger; import java.security.GeneralSecurityException; -import java.security.cert.CRLException; +import java.security.Security; import java.security.cert.CertificateEncodingException; +import java.security.cert.X509CRL; import java.security.cert.X509Certificate; +import java.util.Calendar; import java.util.HashSet; import java.util.Set; @@ -35,13 +37,20 @@ import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; -import org.apache.pdfbox.cos.COSInteger; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.cos.COSUpdateInfo; +import org.apache.pdfbox.examples.signature.SigUtils; +import org.apache.pdfbox.examples.signature.cert.CRLVerifier; +import org.apache.pdfbox.examples.signature.cert.CertificateVerificationException; +import org.apache.pdfbox.examples.signature.cert.OcspHelper; +import org.apache.pdfbox.examples.signature.cert.RevokedCertificateException; import org.apache.pdfbox.examples.signature.validation.CertInformationCollector.CertSignatureInformation; +import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.encryption.SecurityProvider; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.bouncycastle.cert.ocsp.BasicOCSPResp; import org.bouncycastle.cert.ocsp.OCSPException; import org.bouncycastle.cert.ocsp.OCSPResp; @@ -67,6 +76,7 @@ private COSArray certs; private PDDocument document; private final Set foundRevocationInformation = new HashSet(); + private Calendar signDate; /** * Signs the given PDF file. @@ -91,9 +101,9 @@ } /** - * Fetches certificate information from the last signature of the document and appends a DSS with the validation - * information to the document. - * + * Fetches certificate information from the last signature of the document and appends a DSS + * with the validation information to the document. + * * @param document containing the Signature * @param filename in file to extract signature * @param output where to write the changed document @@ -102,10 +112,15 @@ private void doValidation(String filename, OutputStream output) throws IOException { certInformationHelper = new CertInformationCollector(); - CertSignatureInformation certInfo; + CertSignatureInformation certInfo = null; try { - certInfo = certInformationHelper.getLastCertInfo(document, filename); + PDSignature signature = SigUtils.getLastRelevantSignature(document); + if (signature != null) + { + certInfo = certInformationHelper.getLastCertInfo(signature, filename); + signDate = signature.getSignDate(); + } } catch (CertificateProccessingException e) { @@ -190,14 +205,15 @@ /** * Fetches and adds revocation information based on the certInfo to the DSS. - * - * @param certInfo Certificate information from CertInformationHelper containing certificate chains. + * + * @param certInfo Certificate information from CertInformationHelper containing certificate + * chains. * @throws IOException */ private void addRevocationData(CertSignatureInformation certInfo) throws IOException { COSDictionary vri = new COSDictionary(); - vriBase.setItem(COSName.getPDFName(certInfo.getSignatureHash()), vri); + vriBase.setItem(certInfo.getSignatureHash(), vri); correspondingOCSPs = new COSArray(); correspondingCRLs = new COSArray(); @@ -206,11 +222,11 @@ if (correspondingOCSPs.size() > 0) { - vri.setItem(COSName.getPDFName("OCSP"), correspondingOCSPs); + vri.setItem("OCSP", correspondingOCSPs); } if (correspondingCRLs.size() > 0) { - vri.setItem(COSName.getPDFName("CRL"), correspondingCRLs); + vri.setItem("CRL", correspondingCRLs); } if (certInfo.getTsaCerts() != null) @@ -224,8 +240,9 @@ /** * Tries to get Revocation Data (first OCSP, else CRL) from the given Certificate Chain. - * - * @param certInfo from which to fetch revocation data. Will work recursively through its chains. + * + * @param certInfo from which to fetch revocation data. Will work recursively through its + * chains. * @throws IOException when failed to fetch an revocation data. */ private void addRevocationDataRecursive(CertSignatureInformation certInfo) throws IOException @@ -249,10 +266,14 @@ isRevocationInfoFound = true; } - if (!isRevocationInfoFound) + if (certInfo.getOcspUrl() == null && certInfo.getCrlUrl() == null) + { + LOG.info("No revocation information for cert " + certInfo.getCertificate().getSubjectX500Principal()); + } + else if (!isRevocationInfoFound) { throw new IOException("Could not fetch Revocation Info for Cert: " - + certInfo.getCertificate().getSubjectDN()); + + certInfo.getCertificate().getSubjectX500Principal()); } } @@ -269,7 +290,7 @@ /** * Tries to fetch and add OCSP Data to its containers. - * + * * @param certInfo the certificate info, for it to check OCSP data. * @return true when the OCSP data has successfully been fetched and added * @throws IOException when Certificate is revoked. @@ -304,9 +325,10 @@ /** * Tries to fetch and add CRL Data to its containers. - * + * * @param certInfo the certificate info, for it to check CRL data. - * @throws IOException when failed to fetch, because no validation data could be fetched for data. + * @throws IOException when failed to fetch, because no validation data could be fetched for + * data. */ private void fetchCrlData(CertSignatureInformation certInfo) throws IOException { @@ -314,7 +336,7 @@ { addCrlRevocationInfo(certInfo); } - catch (CRLException e) + catch (GeneralSecurityException e) { LOG.warn("Failed fetching CRL", e); throw new IOException(e); @@ -329,6 +351,11 @@ LOG.warn("Failed fetching CRL", e); throw new IOException(e); } + catch (CertificateVerificationException e) + { + LOG.warn("Failed fetching CRL", e); + throw new IOException(e); + } } /** @@ -343,12 +370,19 @@ private void addOcspData(CertSignatureInformation certInfo) throws IOException, OCSPException, CertificateProccessingException, RevokedCertificateException { - OcspHelper ocspHelper = new OcspHelper(certInfo.getCertificate(), - certInfo.getIssuerCertificate(), certInfo.getOcspUrl()); + OcspHelper ocspHelper = new OcspHelper( + certInfo.getCertificate(), + signDate.getTime(), + certInfo.getIssuerCertificate(), + new HashSet(certInformationHelper.getCertificatesMap().values()), + certInfo.getOcspUrl()); OCSPResp ocspResp = ocspHelper.getResponseOcsp(); BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResp.getResponseObject(); certInformationHelper.addAllCertsFromHolders(basicResponse.getCerts()); + + //TODO check revocation of responder + include in separate VRI (usually not needed). + // See comment by mkl in PDFBOX-3017 on 21.11.2018 byte[] ocspData = ocspResp.getEncoded(); @@ -365,16 +399,19 @@ * Fetches and adds CRL data to storage for the given Certificate. * * @param certInfo the certificate info, for it to check CRL data. - * @throws CRLException * @throws IOException * @throws RevokedCertificateException + * @throws GeneralSecurityException + * @throws CertificateVerificationException */ private void addCrlRevocationInfo(CertSignatureInformation certInfo) - throws CRLException, IOException, RevokedCertificateException + throws IOException, RevokedCertificateException, GeneralSecurityException, + CertificateVerificationException { - byte[] crlData = CrlHelper.performCrlRequestAndCheck(certInfo.getCrlUrl(), - certInfo.getCertificate()); - COSStream crlStream = writeDataToStream(crlData); + X509CRL crl = CRLVerifier.downloadCRLFromWeb(certInfo.getCrlUrl()); + crl.verify(certInfo.getIssuerCertificate().getPublicKey(), SecurityProvider.getProvider().getName()); + CRLVerifier.checkRevocation(crl, certInfo.getCertificate(), signDate.getTime(), certInfo.getCrlUrl()); + COSStream crlStream = writeDataToStream(crl.getEncoded()); crls.add(crlStream); if (correspondingCRLs != null) { @@ -384,16 +421,16 @@ } /** - * Adds all certs to the certs-array. Make sure, all certificates are inside the certificateStore of - * certInformationHelper - * + * Adds all certs to the certs-array. Make sure, all certificates are inside the + * certificateStore of certInformationHelper + * * @throws IOException */ private void addAllCertsToCertArray() throws IOException { try { - for (X509Certificate cert : certInformationHelper.getCertificateStore().values()) + for (X509Certificate cert : certInformationHelper.getCertificatesMap().values()) { COSStream stream = writeDataToStream(cert.getEncoded()); certs.add(stream); @@ -406,45 +443,48 @@ } /** - * Creates a FlateDecoded COSStream element with the given data. + * Creates a Flate encoded COSStream object with the given data. * - * @param data to write into the element - * @return COSStream Element, that can be added to the document + * @param data to write into the COSStream + * @return COSStream a COSStream object that can be added to the document * @throws IOException */ private COSStream writeDataToStream(byte[] data) throws IOException { COSStream stream = document.getDocument().createCOSStream(); - COSArray filters = new COSArray(); - filters.add(COSName.FLATE_DECODE); - - OutputStream os = stream.createOutputStream(filters); - os.write(data); - os.close(); - + OutputStream os = null; + try + { + os = stream.createOutputStream(COSName.FLATE_DECODE); + os.write(data); + } + finally + { + IOUtils.closeQuietly(os); + } return stream; } /** - * Adds Extensions to the document catalog. So that the use of DSS is identified. Described in PAdES Part 4, Chapter - * 4.4. - * + * Adds Extensions to the document catalog. So that the use of DSS is identified. Described in + * PAdES Part 4, Chapter 4.4. + * * @param catalog to add Extensions into */ private void addExtensions(PDDocumentCatalog catalog) { COSDictionary dssExtensions = new COSDictionary(); dssExtensions.setDirect(true); - catalog.getCOSObject().setItem(COSName.getPDFName("Extensions"), dssExtensions); + catalog.getCOSObject().setItem("Extensions", dssExtensions); COSDictionary adbeExtension = new COSDictionary(); adbeExtension.setDirect(true); - dssExtensions.setItem(COSName.getPDFName("ADBE"), adbeExtension); + dssExtensions.setItem("ADBE", adbeExtension); - adbeExtension.setItem(COSName.getPDFName("BaseVersion"), COSName.getPDFName("1.7")); - adbeExtension.setItem(COSName.getPDFName("ExtensionLevel"), COSInteger.get(5)); + adbeExtension.setName("BaseVersion", "1.7"); + adbeExtension.setInt("ExtensionLevel", 5); - catalog.getCOSObject().setItem(COSName.getPDFName("Version"), COSName.getPDFName("1.7")); + catalog.setVersion("1.7"); } public static void main(String[] args) throws IOException, GeneralSecurityException @@ -455,6 +495,9 @@ System.exit(1); } + // register BouncyCastle provider, needed for "exotic" algorithms + Security.addProvider(SecurityProvider.getProvider()); + // add ocspInformation AddValidationInformation addOcspInformation = new AddValidationInformation(); diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationCollector.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationCollector.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationCollector.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationCollector.java 2018-11-28 17:18:40.000000000 +0000 @@ -22,27 +22,31 @@ import java.io.InputStream; import java.math.BigInteger; import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.pdfbox.cos.COSBase; -import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.examples.signature.cert.CertificateVerifier; import org.apache.pdfbox.io.IOUtils; -import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.encryption.SecurityProvider; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.DERSet; import org.bouncycastle.asn1.cms.Attribute; import org.bouncycastle.asn1.cms.AttributeTable; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cms.CMSException; @@ -50,6 +54,7 @@ import org.bouncycastle.cms.SignerInformation; import org.bouncycastle.tsp.TSPException; import org.bouncycastle.tsp.TimeStampToken; +import org.bouncycastle.util.Selector; import org.bouncycastle.util.Store; /** @@ -64,78 +69,37 @@ { private static final Log LOG = LogFactory.getLog(CertInformationCollector.class); - // As described in https://tools.ietf.org/html/rfc3280.html#section-4.2.2.1 - private static final String ID_PE_AUTHORITYINFOACCESS = "1.3.6.1.5.5.7.1.1"; - - // As described in https://tools.ietf.org/html/rfc3280.html#section-4.2.1.14 - // Disable false Sonar warning for "Hardcoded IP Address ..." - @SuppressWarnings("squid:S1313") - private static final String ID_CE_CRLDISTRIBUTIONPOINTS = "2.5.29.31"; - private static final int MAX_CERTIFICATE_CHAIN_DEPTH = 5; - private final Map certificateStore = new HashMap(); + private final Map certificatesMap = new HashMap(); private final JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter(); private CertSignatureInformation rootCertInfo; /** - * Gets the Certificate Information of the last Signature. + * Gets the certificate information of a signature. * - * @param document to get the Signature from + * @param signature the signature of the document. * @param fileName of the document. * @return the CertSignatureInformation containing all certificate information * @throws CertificateProccessingException when there is an error processing the certificates * @throws IOException on a data processing error */ - public CertSignatureInformation getLastCertInfo(PDDocument document, String fileName) + public CertSignatureInformation getLastCertInfo(PDSignature signature, String fileName) throws CertificateProccessingException, IOException { - PDSignature signature = getLastRelevantSignature(document); - if (signature != null) + FileInputStream documentInput = null; + try { - FileInputStream documentInput = null; - try - { - documentInput = new FileInputStream(fileName); - byte[] docBytes = IOUtils.toByteArray(documentInput); - byte[] signatureContent = signature.getContents(docBytes); - return getCertInfo(signatureContent); - } - finally - { - IOUtils.closeQuietly(document); - } + documentInput = new FileInputStream(fileName); + byte[] signatureContent = signature.getContents(documentInput); + return getCertInfo(signatureContent); } - return null; - } - - /** - * Gets the last relevant signature in the document - * - * @param document to get its last Signature - * @return last signature or null when none found - * @throws IOException - */ - private PDSignature getLastRelevantSignature(PDDocument document) throws IOException - { - SortedMap sortedMap = new TreeMap(); - for (PDSignature signature : document.getSignatureDictionaries()) + finally { - int sigOffset = signature.getByteRange()[1]; - sortedMap.put(sigOffset, signature); + IOUtils.closeQuietly(documentInput); } - if (sortedMap.size() > 0) - { - PDSignature lastSignature = sortedMap.get(sortedMap.lastKey()); - COSBase type = lastSignature.getCOSObject().getItem(COSName.TYPE); - if (type.equals(COSName.SIG) || type.equals(COSName.DOC_TIME_STAMP)) - { - return lastSignature; - } - } - return null; } /** @@ -225,10 +189,10 @@ * not yet practicable. * * @param certificatesStore To get the certificate information from. Certificates will be saved - * in the certificateStore. - * @param signedData to get SignerInformation off + * in certificatesMap. + * @param signedData data from which to get the SignerInformation * @param certInfo where to add certificate information - * @return Signer Information of the processed certificate Store for further usage. + * @return Signer Information of the processed certificatesStore for further usage. * @throws IOException on data-processing error * @throws CertificateProccessingException on a specific error with a certificate */ @@ -239,8 +203,9 @@ Collection signers = signedData.getSignerInfos().getSigners(); SignerInformation signerInformation = signers.iterator().next(); + @SuppressWarnings("unchecked") Collection matches = certificatesStore - .getMatches(signerInformation.getSID()); + .getMatches((Selector) signerInformation.getSID()); X509Certificate certificate = getCertFromHolder(matches.iterator().next()); @@ -266,7 +231,8 @@ certInfo.certificate = certificate; // Certificate Authority Information Access - byte[] authorityExtensionValue = certificate.getExtensionValue(ID_PE_AUTHORITYINFOACCESS); + // As described in https://tools.ietf.org/html/rfc3280.html#section-4.2.2.1 + byte[] authorityExtensionValue = certificate.getExtensionValue(Extension.authorityInfoAccess.getId()); if (authorityExtensionValue != null) { CertInformationHelper.getAuthorityInfoExtensionValue(authorityExtensionValue, certInfo); @@ -277,37 +243,66 @@ getAlternativeIssuerCertificate(certInfo, maxDepth); } - byte[] crlExtensionValue = certificate.getExtensionValue(ID_CE_CRLDISTRIBUTIONPOINTS); + // As described in https://tools.ietf.org/html/rfc3280.html#section-4.2.1.14 + byte[] crlExtensionValue = certificate.getExtensionValue(Extension.cRLDistributionPoints.getId()); if (crlExtensionValue != null) { certInfo.crlUrl = CertInformationHelper.getCrlUrlFromExtensionValue(crlExtensionValue); } - if (CertInformationHelper.isSelfSigned(certificate)) + try { - certInfo.isSelfSigned = true; + certInfo.isSelfSigned = CertificateVerifier.isSelfSigned(certificate); + } + catch (GeneralSecurityException ex) + { + throw new CertificateProccessingException(ex); } if (maxDepth <= 0 || certInfo.isSelfSigned) { return; } - for (X509Certificate issuer : certificateStore.values()) + for (X509Certificate issuer : certificatesMap.values()) { - if (CertInformationHelper.verify(certificate, issuer.getPublicKey())) + if (certificate.getIssuerX500Principal().equals(issuer.getSubjectX500Principal())) { - LOG.info("Found the right Issuer Cert! for Cert: " + certificate.getSubjectDN() - + "\n" + issuer.getSubjectDN()); + try + { + certificate.verify(issuer.getPublicKey(), SecurityProvider.getProvider().getName()); + } + catch (CertificateException ex) + { + throw new CertificateProccessingException(ex); + } + catch (NoSuchAlgorithmException ex) + { + throw new CertificateProccessingException(ex); + } + catch (InvalidKeyException ex) + { + throw new CertificateProccessingException(ex); + } + catch (SignatureException ex) + { + throw new CertificateProccessingException(ex); + } + catch (NoSuchProviderException ex) + { + throw new CertificateProccessingException(ex); + } + LOG.info("Found the right Issuer Cert! for Cert: " + certificate.getSubjectX500Principal() + + "\n" + issuer.getSubjectX500Principal()); certInfo.issuerCertificate = issuer; certInfo.certChain = new CertSignatureInformation(); - traverseChain(issuer, certInfo.certChain, --maxDepth); + traverseChain(issuer, certInfo.certChain, maxDepth - 1); break; } } if (certInfo.issuerCertificate == null) { throw new IOException( - "No Issuer Certificate found for Cert: " + certificate.getSubjectDN()); + "No Issuer Certificate found for Cert: " + certificate.getSubjectX500Principal()); } } @@ -324,7 +319,7 @@ private void getAlternativeIssuerCertificate(CertSignatureInformation certInfo, int maxDepth) throws CertificateProccessingException { - System.out.println("Get Certificate from: " + certInfo.issuerUrl); + LOG.info("Get alternative issuer certificate from: " + certInfo.issuerUrl); try { URL certUrl = new URL(certInfo.issuerUrl); @@ -332,7 +327,7 @@ InputStream in = certUrl.openStream(); X509Certificate altIssuerCert = (X509Certificate) certFactory.generateCertificate(in); - addCertToCertStore(altIssuerCert); + addCertToCertificatesMap(altIssuerCert); certInfo.alternativeCertChain = new CertSignatureInformation(); traverseChain(altIssuerCert, certInfo.alternativeCertChain, maxDepth - 1); @@ -340,30 +335,30 @@ } catch (IOException e) { - LOG.error("Error getting additional Certificate from " + certInfo.issuerUrl, e); + LOG.error("Error getting alternative issuer certificate from " + certInfo.issuerUrl, e); } catch (CertificateException e) { - LOG.error("Error getting additional Certificate from " + certInfo.issuerUrl, e); + LOG.error("Error getting alternative issuer certificate from " + certInfo.issuerUrl, e); } } /** - * Adds the given Certificate to the certificateStore, if not yet containing. + * Adds the given Certificate to the certificatesMap, if not yet containing. * - * @param certificate to add to the certificateStore + * @param certificate to add to the certificatesMap */ - private void addCertToCertStore(X509Certificate certificate) + private void addCertToCertificatesMap(X509Certificate certificate) { - if (!certificateStore.containsKey(certificate.getSerialNumber())) + if (!certificatesMap.containsKey(certificate.getSerialNumber())) { - certificateStore.put(certificate.getSerialNumber(), certificate); + certificatesMap.put(certificate.getSerialNumber(), certificate); } } /** - * Gets the X509Certificate out of the X509CertificateHolder - * + * Gets the X509Certificate out of the X509CertificateHolder and add it to certificatesMap. + * * @param certificateHolder to get the certificate from * @return a X509Certificate or null when there was an Error with the Certificate * @throws CertificateProccessingException on failed conversion from X509CertificateHolder to X509Certificate @@ -371,12 +366,13 @@ private X509Certificate getCertFromHolder(X509CertificateHolder certificateHolder) throws CertificateProccessingException { - if (!certificateStore.containsKey(certificateHolder.getSerialNumber())) + //TODO getCertFromHolder violates "do one thing" rule (adds to the map and returns a certificate) + if (!certificatesMap.containsKey(certificateHolder.getSerialNumber())) { try { X509Certificate certificate = certConverter.getCertificate(certificateHolder); - certificateStore.put(certificate.getSerialNumber(), certificate); + certificatesMap.put(certificate.getSerialNumber(), certificate); return certificate; } catch (CertificateException e) @@ -387,13 +383,12 @@ } else { - return certificateStore.get(certificateHolder.getSerialNumber()); + return certificatesMap.get(certificateHolder.getSerialNumber()); } } /** - * Adds multiple Certificates out of a Collection of X509CertificateHolder into the - * Certificate-Store. + * Adds multiple Certificates out of a Collection of X509CertificateHolder into certificatesMap. * * @param certHolders Collection of X509CertificateHolder */ @@ -414,7 +409,7 @@ /** * Gets a list of X509Certificate out of an array of X509CertificateHolder. The certificates - * will be added to the certificateStore. + * will be added to certificatesMap. * * @param certHolders Array of X509CertificateHolder * @throws CertificateProccessingException when one of the Certificates could not be parsed. @@ -422,20 +417,42 @@ public void addAllCertsFromHolders(X509CertificateHolder[] certHolders) throws CertificateProccessingException { - for (X509CertificateHolder certHolder : certHolders) + addAllCerts(Arrays.asList(certHolders)); + } + + /** + * Traverse the OCSP certificate. + * + * @param certHolder + * @return + * @throws CertificateProccessingException + */ + CertSignatureInformation getOCSPCertInfo(X509CertificateHolder certHolder) throws CertificateProccessingException + { + try + { + CertSignatureInformation certSignatureInformation = new CertSignatureInformation(); + traverseChain(certConverter.getCertificate(certHolder), certSignatureInformation, MAX_CERTIFICATE_CHAIN_DEPTH); + return certSignatureInformation; + } + catch (CertificateException ex) + { + throw new CertificateProccessingException(ex); + } + catch (IOException ex) { - getCertFromHolder(certHolder); + throw new CertificateProccessingException(ex); } } /** - * Get the certificate store of all processed certificates until now. + * Get the map of all processed certificates until now. * * @return a map of serial numbers to certificates. */ - public Map getCertificateStore() + public Map getCertificatesMap() { - return certificateStore; + return certificatesMap; } /** diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationHelper.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationHelper.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationHelper.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationHelper.java 2018-11-28 17:18:40.000000000 +0000 @@ -17,14 +17,8 @@ package org.apache.pdfbox.examples.signature.validation; import java.io.IOException; -import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PublicKey; -import java.security.SignatureException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; import java.util.Enumeration; import org.apache.commons.logging.Log; @@ -38,7 +32,7 @@ import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.X509ObjectIdentifiers; -import org.bouncycastle.x509.extension.X509ExtensionUtil; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; public class CertInformationHelper { @@ -69,73 +63,6 @@ } /** - * Checks whether the given certificate is self-signed (root). - * - * @param cert to be checked - * @return true when it is a self-signed certificate - * @throws CertificateProccessingException containing the cause, on multiple exception with the given data - */ - public static boolean isSelfSigned(X509Certificate cert) throws CertificateProccessingException - { - return verify(cert, cert.getPublicKey()); - } - - /** - * Verifies whether the certificate is signed by the given public key. Can be done to check - * signature chain. Idea and code are from Julius Musseau at: - * https://stackoverflow.com/a/10822177/2497581 - * - * @param cert Child certificate to check - * @param key Fathers public key to check - * @return true when the certificate is signed by the public key - * @throws CertificateProccessingException containing the cause, on multiple exception with the - * given data - */ - public static boolean verify(X509Certificate cert, PublicKey key) - throws CertificateProccessingException - { - try - { - String sigAlg = cert.getSigAlgName(); - String keyAlg = key.getAlgorithm(); - sigAlg = sigAlg != null ? sigAlg.trim().toUpperCase() : ""; - keyAlg = keyAlg != null ? keyAlg.trim().toUpperCase() : ""; - if (keyAlg.length() >= 2 && sigAlg.endsWith(keyAlg)) - { - try - { - cert.verify(key); - return true; - } - catch (SignatureException se) - { - return false; - } - } - else - { - return false; - } - } - catch (InvalidKeyException e) - { - throw new CertificateProccessingException(e); - } - catch (CertificateException e) - { - throw new CertificateProccessingException(e); - } - catch (NoSuchAlgorithmException e) - { - throw new CertificateProccessingException(e); - } - catch (NoSuchProviderException e) - { - throw new CertificateProccessingException(e); - } - } - - /** * Extracts authority information access extension values from the given data. The Data * structure has to be implemented as described in RFC 2459, 4.2.2.1. * @@ -146,7 +73,7 @@ protected static void getAuthorityInfoExtensionValue(byte[] extensionValue, CertSignatureInformation certInfo) throws IOException { - ASN1Sequence asn1Seq = (ASN1Sequence) X509ExtensionUtil.fromExtensionValue(extensionValue); + ASN1Sequence asn1Seq = (ASN1Sequence) JcaX509ExtensionUtils.parseExtensionValue(extensionValue); Enumeration objects = asn1Seq.getObjects(); while (objects.hasMoreElements()) { @@ -171,8 +98,8 @@ } /** - * Gets the first CRL Url from given extension value. Structure has to be build as in 4.2.1.14 - * CRL Distribution Points of RFC 2459. + * Gets the first CRL URL from given extension value. Structure has to be + * built as in 4.2.1.14 CRL Distribution Points of RFC 2459. * * @param extensionValue to get the extension value from * @return first CRL- URL or null @@ -180,7 +107,7 @@ */ protected static String getCrlUrlFromExtensionValue(byte[] extensionValue) throws IOException { - ASN1Sequence asn1Seq = (ASN1Sequence) X509ExtensionUtil.fromExtensionValue(extensionValue); + ASN1Sequence asn1Seq = (ASN1Sequence) JcaX509ExtensionUtils.parseExtensionValue(extensionValue); Enumeration objects = asn1Seq.getObjects(); while (objects.hasMoreElements()) diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CrlHelper.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CrlHelper.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CrlHelper.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CrlHelper.java 1970-01-01 00:00:00.000000000 +0000 @@ -1,77 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.pdfbox.examples.signature.validation; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.security.cert.CRLException; -import java.security.cert.X509CRL; -import java.security.cert.X509Certificate; - -import org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory; - -/** - * Helper class to get CRL (Certificate revocation list) from given crlUrl and check if Certificate - * has been revoked. - * - * @author Alexis Suter - */ -public final class CrlHelper -{ - private CrlHelper() - { - } - - /** - * Performs the CRL-Request and checks if the given certificate has been revoked. - * - * @param crlUrl to get the CRL from - * @param cert to be checked if it is inside the CRL - * @return CRL-Response; might be very big depending on the issuer. - * @throws CRLException if an Error occurred getting the CRL, or parsing it. - * @throws RevokedCertificateException - */ - public static byte[] performCrlRequestAndCheck(String crlUrl, X509Certificate cert) - throws CRLException, RevokedCertificateException - { - try - { - URL url = new URL(crlUrl); - - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - if (con.getResponseCode() != 200) - { - throw new IOException("Unsuccessful CRL request. Status: " + con.getResponseCode() - + " Url: " + crlUrl); - } - - CertificateFactory certFac = new CertificateFactory(); - X509CRL crl = (X509CRL) certFac.engineGenerateCRL(con.getInputStream()); - if (crl.isRevoked(cert)) - { - throw new RevokedCertificateException("The Certificate was found on the CRL and is revoked!"); - } - return crl.getEncoded(); - } - catch (IOException e) - { - throw new CRLException(e); - } - } - -} diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/OcspHelper.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/OcspHelper.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/OcspHelper.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/OcspHelper.java 1970-01-01 00:00:00.000000000 +0000 @@ -1,353 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.pdfbox.examples.signature.validation; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.Security; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.util.Random; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.pdfbox.util.Hex; -import org.bouncycastle.asn1.DEROctetString; -import org.bouncycastle.asn1.DLSequence; -import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; -import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; -import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; -import org.bouncycastle.asn1.x509.AlgorithmIdentifier; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.Extensions; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; -import org.bouncycastle.cert.jcajce.JcaX509ContentVerifierProviderBuilder; -import org.bouncycastle.cert.ocsp.BasicOCSPResp; -import org.bouncycastle.cert.ocsp.CertificateID; -import org.bouncycastle.cert.ocsp.CertificateStatus; -import org.bouncycastle.cert.ocsp.OCSPException; -import org.bouncycastle.cert.ocsp.OCSPReq; -import org.bouncycastle.cert.ocsp.OCSPReqBuilder; -import org.bouncycastle.cert.ocsp.OCSPResp; -import org.bouncycastle.cert.ocsp.RevokedStatus; -import org.bouncycastle.cert.ocsp.SingleResp; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentVerifierProvider; -import org.bouncycastle.operator.DigestCalculator; -import org.bouncycastle.operator.OperatorCreationException; - -/** - * Helper Class for OCSP-Operations with bouncy castle. - * - * @author Alexis Suter - */ -public class OcspHelper -{ - private static final Log LOG = LogFactory.getLog(OcspHelper.class); - - private final X509Certificate issuerCertificate; - private final X509Certificate certificateToCheck; - private final String ocspUrl; - private DEROctetString encodedNonce; - - /** - * @param checkCertificate Certificate to be OCSP-Checked - * @param issuerCertificate Certificate of the issuer - * @param ocspUrl where to fetch for OCSP - */ - public OcspHelper(X509Certificate checkCertificate, X509Certificate issuerCertificate, - String ocspUrl) - { - this.certificateToCheck = checkCertificate; - this.issuerCertificate = issuerCertificate; - this.ocspUrl = ocspUrl; - } - - /** - * Performs and verifies the OCSP-Request - * - * @return the OCSPResp, when the request was successful, else a corresponding exception will be - * thrown. - * @throws IOException - * @throws OCSPException - * @throws RevokedCertificateException - */ - public OCSPResp getResponseOcsp() throws IOException, OCSPException, RevokedCertificateException - { - OCSPResp ocspResponse = performRequest(); - verifyOcspResponse(ocspResponse); - return ocspResponse; - } - - /** - * Verifies the status and the response itself (including nonce), but not the signature. - * - * @param ocspResponse to be verified - * @throws OCSPException - * @throws RevokedCertificateException - */ - private void verifyOcspResponse(OCSPResp ocspResponse) - throws OCSPException, RevokedCertificateException - { - verifyRespStatus(ocspResponse); - - BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResponse.getResponseObject(); - if (basicResponse != null) - { - checkOcspSignature(basicResponse.getCerts()[0], basicResponse); - - checkNonce(basicResponse); - - SingleResp[] responses = basicResponse.getResponses(); - if (responses.length == 1) - { - SingleResp resp = responses[0]; - Object status = resp.getCertStatus(); - - if (status instanceof RevokedStatus) - { - throw new RevokedCertificateException("OCSP: Certificate is revoked."); - } - else if (status != CertificateStatus.GOOD) - { - throw new OCSPException("OCSP: Status of Cert is unknown"); - } - } - else - { - throw new OCSPException( - "OCSP: Recieved " + responses.length + " responses instead of 1!"); - } - } - } - - /** - * Checks whether the OCSP response is signed by the given certificate. - * - * @param certificate the certificate to check the signature - * @param basicResponse OCSP response containing the signature - * @throws OCSPException when the signature is invalid or could not be checked - */ - private void checkOcspSignature(X509CertificateHolder certificate, BasicOCSPResp basicResponse) - throws OCSPException - { - try - { - ContentVerifierProvider verifier = new JcaX509ContentVerifierProviderBuilder() - .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(certificate); - - if (!basicResponse.isSignatureValid(verifier)) - { - throw new OCSPException("OCSP-Signature is not valid!"); - } - } - catch (OperatorCreationException e) - { - throw new OCSPException("Error checking Ocsp-Signature", e); - } - } - - /** - * Checks if the nonce in the response is correct - * - * @param basicResponse Response to be checked - * @throws OCSPException if nonce is wrong or inexistent - */ - private void checkNonce(BasicOCSPResp basicResponse) throws OCSPException - { - Extension nonceExt = basicResponse.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce); - if (nonceExt != null) - { - DEROctetString responseNonceString = (DEROctetString) nonceExt.getExtnValue(); - if (!responseNonceString.equals(encodedNonce)) - { - throw new OCSPException("Invalid Nonce found in response!"); - } - } - else if (encodedNonce != null) - { - throw new OCSPException("Nonce not found in response!"); - } - } - - /** - * Performs the OCSP-Request, with given data. - * - * @return the OCSPResp, that has been fetched from the ocspUrl - * @throws IOException - * @throws OCSPException - */ - private OCSPResp performRequest() throws IOException, OCSPException - { - OCSPReq request = generateOCSPRequest(); - URL url = new URL(ocspUrl); - HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); - httpConnection.setRequestProperty("Content-Type", "application/ocsp-request"); - httpConnection.setRequestProperty("Accept", "application/ocsp-response"); - httpConnection.setDoOutput(true); - OutputStream out = httpConnection.getOutputStream(); - out.write(request.getEncoded()); - out.close(); - - if (httpConnection.getResponseCode() != 200) - { - throw new IOException("OCSP: Could not access url, ResponseCode: " - + httpConnection.getResponseCode()); - } - // Get Response - InputStream in = (InputStream) httpConnection.getContent(); - return new OCSPResp(in); - } - - /** - * Helper method to verify response status. - * - * @param resp OCSP response - * @throws OCSPException if the response status is not ok - */ - public void verifyRespStatus(OCSPResp resp) throws OCSPException - { - String statusInfo = ""; - if (resp != null) - { - int status = resp.getStatus(); - switch (status) - { - case OCSPResponseStatus.INTERNAL_ERROR: - statusInfo = "INTERNAL_ERROR"; - System.err.println("An internal error occurred in the OCSP Server!"); - break; - case OCSPResponseStatus.MALFORMED_REQUEST: - statusInfo = "MALFORMED_REQUEST"; - System.err.println("Your request did not fit the RFC 2560 syntax!"); - break; - case OCSPResponseStatus.SIG_REQUIRED: - statusInfo = "SIG_REQUIRED"; - System.err.println("Your request was not signed!"); - break; - case OCSPResponseStatus.TRY_LATER: - statusInfo = "TRY_LATER"; - System.err.println("The server was too busy to answer you!"); - break; - case OCSPResponseStatus.UNAUTHORIZED: - statusInfo = "UNAUTHORIZED"; - System.err.println("The server could not authenticate you!"); - break; - case OCSPResponseStatus.SUCCESSFUL: - break; - default: - statusInfo = "UNKNOWN"; - System.err.println("Unknown OCSPResponse status code! " + status); - } - } - if (resp == null || resp.getStatus() != OCSPResponseStatus.SUCCESSFUL) - { - throw new OCSPException(statusInfo + "OCSP response unsuccessful! "); - } - } - - /** - * Generates an OCSP request and generates the CertificateID. - * - * @return OCSP request, ready to fetch data - * @throws OCSPException - * @throws IOException - */ - private OCSPReq generateOCSPRequest() throws OCSPException, IOException - { - Security.addProvider(new BouncyCastleProvider()); - - // Generate the ID for the certificate we are looking for - CertificateID certId; - try - { - certId = new CertificateID(new SHA1DigestCalculator(), - new JcaX509CertificateHolder(issuerCertificate), - certificateToCheck.getSerialNumber()); - } - catch (CertificateEncodingException e) - { - throw new IOException("Error creating CertificateID with the Certificate encoding", e); - } - - OCSPReqBuilder builder = new OCSPReqBuilder(); - - Extension responseExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_response, - true, new DLSequence(OCSPObjectIdentifiers.id_pkix_ocsp_basic).getEncoded()); - - Random rand = new Random(); - byte[] nonce = new byte[16]; - rand.nextBytes(nonce); - encodedNonce = new DEROctetString(new DEROctetString(nonce)); - Extension nonceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, true, - encodedNonce); - - builder.setRequestExtensions( - new Extensions(new Extension[] { responseExtension, nonceExtension })); - - builder.addRequest(certId); - - System.out.println("Nonce: " + Hex.getString(nonceExtension.getExtnValue().getEncoded())); - - return builder.build(); - } - - /** - * Class to create SHA-1 Digest, used for creation of CertificateID. - */ - private static class SHA1DigestCalculator implements DigestCalculator - { - private final ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - - @Override - public AlgorithmIdentifier getAlgorithmIdentifier() - { - return new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1); - } - - @Override - public OutputStream getOutputStream() - { - return bOut; - } - - @Override - public byte[] getDigest() - { - byte[] bytes = bOut.toByteArray(); - bOut.reset(); - - try - { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - return md.digest(bytes); - } - catch (NoSuchAlgorithmException e) - { - LOG.error("SHA-1 Algorithm not found", e); - return null; - } - } - } -} diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/RevokedCertificateException.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/RevokedCertificateException.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/RevokedCertificateException.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/RevokedCertificateException.java 1970-01-01 00:00:00.000000000 +0000 @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.pdfbox.examples.signature.validation; - -/** - * Exception to handle a revoked Certificate explicitly - * - * @author Alexis Suter - */ -public class RevokedCertificateException extends Exception -{ - private static final long serialVersionUID = 3543946618794126654L; - - public RevokedCertificateException(String message) - { - super(message); - } -} diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/util/ExtractTextSimple.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/util/ExtractTextSimple.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/util/ExtractTextSimple.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/util/ExtractTextSimple.java 2018-11-28 17:18:40.000000000 +0000 @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.examples.util; + +import java.io.File; +import java.io.IOException; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.encryption.AccessPermission; +import org.apache.pdfbox.text.PDFTextStripper; + +/** + * This is a simple text extraction example to get started. For more advance usage, see the + * ExtractTextByArea and the DrawPrintTextLocations examples in this subproject, as well as the + * ExtractText tool in the tools subproject. + * + * @author Tilman Hausherr + */ +public class ExtractTextSimple +{ + private ExtractTextSimple() + { + // example class should not be instantiated + } + + /** + * This will print the documents text page by page. + * + * @param args The command line arguments. + * + * @throws IOException If there is an error parsing or extracting the document. + */ + public static void main(String[] args) throws IOException + { + if (args.length != 1) + { + usage(); + } + + PDDocument document = PDDocument.load(new File(args[0])); + AccessPermission ap = document.getCurrentAccessPermission(); + if (!ap.canExtractContent()) + { + throw new IOException("You do not have permission to extract text"); + } + + PDFTextStripper stripper = new PDFTextStripper(); + + // This example uses sorting, but in some cases it is more useful to switch it off, + // e.g. in some files with columns where the PDF content stream respects the + // column order. + stripper.setSortByPosition(true); + + for (int p = 1; p <= document.getNumberOfPages(); ++p) + { + // Set the page interval to extract. If you don't, then all pages would be extracted. + stripper.setStartPage(p); + stripper.setEndPage(p); + + // let the magic happen + String text = stripper.getText(document); + + // do some nice output with a header + String pageStr = String.format("page %d:", p); + System.out.println(pageStr); + for (int i = 0; i < pageStr.length(); ++i) + { + System.out.print("-"); + } + System.out.println(); + System.out.println(text.trim()); + System.out.println(); + + // If the extracted text is empty or gibberish, please try extracting text + // with Adobe Reader first before asking for help. Also read the FAQ + // on the website: + // https://pdfbox.apache.org/2.0/faq.html#text-extraction + } + document.close(); + } + + /** + * This will print the usage for this document. + */ + private static void usage() + { + System.err.println("Usage: java " + ExtractTextSimple.class.getName() + " "); + System.exit(-1); + } +} diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/util/PDFHighlighter.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/util/PDFHighlighter.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/util/PDFHighlighter.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/util/PDFHighlighter.java 2018-11-28 17:18:40.000000000 +0000 @@ -54,7 +54,6 @@ */ public PDFHighlighter() throws IOException { - super(); super.setLineSeparator( "" ); super.setWordSeparator( "" ); super.setShouldSeparateByBeads( false ); diff -Nru libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/util/RemoveAllText.java libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/util/RemoveAllText.java --- libpdfbox2-java-2.0.9/examples/src/main/java/org/apache/pdfbox/examples/util/RemoveAllText.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/main/java/org/apache/pdfbox/examples/util/RemoveAllText.java 2018-11-28 17:18:40.000000000 +0000 @@ -130,12 +130,23 @@ if (token instanceof Operator) { Operator op = (Operator) token; - if ("TJ".equals(op.getName()) || "Tj".equals(op.getName()) || - "'".equals(op.getName()) || "\"".equals(op.getName())) + if ("TJ".equals(op.getName()) || + "Tj".equals(op.getName()) || + "'".equals(op.getName())) { - // remove the one argument to this operator + // remove the argument to this operator newTokens.remove(newTokens.size() - 1); - + + token = parser.parseNextToken(); + continue; + } + else if ("\"".equals(op.getName())) + { + // remove the 3 arguments to this operator + newTokens.remove(newTokens.size() - 1); + newTokens.remove(newTokens.size() - 1); + newTokens.remove(newTokens.size() - 1); + token = parser.parseNextToken(); continue; } Binary files /tmp/tmpAhqVVB/umSeuAuTn3/libpdfbox2-java-2.0.9/examples/src/main/resources/org/apache/pdfbox/resources/ttf/Lohit-Bengali.ttf and /tmp/tmpAhqVVB/Y0XihoDyIA/libpdfbox2-java-2.0.13/examples/src/main/resources/org/apache/pdfbox/resources/ttf/Lohit-Bengali.ttf differ diff -Nru libpdfbox2-java-2.0.9/examples/src/test/java/org/apache/pdfbox/examples/pdmodel/TestCreateSignature.java libpdfbox2-java-2.0.13/examples/src/test/java/org/apache/pdfbox/examples/pdmodel/TestCreateSignature.java --- libpdfbox2-java-2.0.9/examples/src/test/java/org/apache/pdfbox/examples/pdmodel/TestCreateSignature.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/test/java/org/apache/pdfbox/examples/pdmodel/TestCreateSignature.java 2018-11-28 17:18:40.000000000 +0000 @@ -34,7 +34,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import javax.xml.bind.DatatypeConverter; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSString; @@ -46,6 +45,7 @@ import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.apache.pdfbox.util.Hex; import org.apache.wink.client.MockHttpServer; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; @@ -56,6 +56,7 @@ import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.tsp.TSPValidationException; +import org.bouncycastle.util.Selector; import org.bouncycastle.util.Store; import org.junit.Assert; import org.junit.BeforeClass; @@ -123,7 +124,7 @@ final String fileName = getOutputFileName("signed{0}.pdf"); signing.signDetached(new File(inDir + "sign_me.pdf"), new File(outDir + fileName)); - checkSignature(new File(outDir + fileName)); + checkSignature(new File(inDir, "sign_me.pdf"), new File(outDir, fileName)); } /** @@ -209,7 +210,7 @@ signing.signPDF(new File(inPath), destFile, null); fis.close(); - checkSignature(destFile); + checkSignature(new File(inPath), destFile); } /** @@ -251,7 +252,7 @@ signing1.setExternalSigning(false); signing1.signDetached(new File(filename), new File(filenameSigned1)); - checkSignature(new File(filenameSigned1)); + checkSignature(new File(filename), new File(filenameSigned1)); PDDocument doc1 = PDDocument.load(new File(filenameSigned1)); List signatureDictionaries = doc1.getSignatureDictionaries(); @@ -267,7 +268,7 @@ signing2.signPDF(new File(filenameSigned1), new File(filenameSigned2), null, "Signature1"); fis.close(); - checkSignature(new File(filenameSigned2)); + checkSignature(new File(filenameSigned1), new File(filenameSigned2)); PDDocument doc2 = PDDocument.load(new File(filenameSigned2)); signatureDictionaries = doc2.getSignatureDictionaries(); @@ -281,10 +282,18 @@ } // This check fails with a file created with the code before PDFBOX-3011 was solved. - private void checkSignature(File file) + private void checkSignature(File origFile, File signedFile) throws IOException, CMSException, OperatorCreationException, GeneralSecurityException { - PDDocument document = PDDocument.load(file); + PDDocument document = PDDocument.load(origFile); + // get string representation of pages COSObject + String origPageKey = document.getDocumentCatalog().getCOSObject().getItem(COSName.PAGES).toString(); + document.close(); + + document = PDDocument.load(signedFile); + // PDFBOX-4261: check that object number stays the same + Assert.assertEquals(origPageKey, document.getDocumentCatalog().getCOSObject().getItem(COSName.PAGES).toString()); + List signatureDictionaries = document.getSignatureDictionaries(); if (signatureDictionaries.isEmpty()) { @@ -294,7 +303,7 @@ { COSString contents = (COSString) sig.getCOSObject().getDictionaryObject(COSName.CONTENTS); - FileInputStream fis = new FileInputStream(file); + FileInputStream fis = new FileInputStream(signedFile); byte[] buf = sig.getSignedContent(fis); fis.close(); @@ -305,7 +314,7 @@ Store certificatesStore = signedData.getCertificates(); Collection signers = signedData.getSignerInfos().getSigners(); SignerInformation signerInformation = signers.iterator().next(); - Collection matches = certificatesStore.getMatches(signerInformation.getSID()); + Collection matches = certificatesStore.getMatches((Selector) signerInformation.getSID()); X509CertificateHolder certificateHolder = (X509CertificateHolder) matches.iterator().next(); X509Certificate certFromSignedData = new JcaX509CertificateConverter().getCertificate(certificateHolder); @@ -324,7 +333,7 @@ private String calculateDigestString(InputStream inputStream) throws NoSuchAlgorithmException, IOException { MessageDigest md = MessageDigest.getInstance("SHA-256"); - return DatatypeConverter.printHexBinary(md.digest(IOUtils.toByteArray(inputStream))); + return Hex.getString(md.digest(IOUtils.toByteArray(inputStream))); } /** diff -Nru libpdfbox2-java-2.0.9/examples/src/test/java/org/apache/wink/client/MockHttpServer.java libpdfbox2-java-2.0.13/examples/src/test/java/org/apache/wink/client/MockHttpServer.java --- libpdfbox2-java-2.0.9/examples/src/test/java/org/apache/wink/client/MockHttpServer.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/examples/src/test/java/org/apache/wink/client/MockHttpServer.java 2018-11-28 17:18:40.000000000 +0000 @@ -0,0 +1,548 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + *******************************************************************************/ + +package org.apache.wink.client; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.BindException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.net.ServerSocketFactory; +import javax.net.ssl.SSLServerSocketFactory; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +/** + * Copied from + * http://svn.apache.org/repos/asf/wink/trunk/wink-component-test-support/src/main/java/org/apache/wink/client/MockHttpServer.java + * on 28.7.2018. + */ +public class MockHttpServer extends Thread { + + public static class MockHttpServerResponse { + + // mock response data + private int mockResponseCode = 200; + private final Map mockResponseHeaders = new HashMap(); + private byte[] mockResponseContent = "received message".getBytes(); + private String mockResponseContentType = "text/plain;charset=utf-8"; + private boolean mockResponseContentEchoRequest; + + public void setMockResponseHeaders(Map headers) { + mockResponseHeaders.clear(); + mockResponseHeaders.putAll(headers); + } + + public void setMockResponseHeader(String name, String value) { + mockResponseHeaders.put(name, value); + } + + public Map getMockResponseHeaders() { + return mockResponseHeaders; + } + + public void setMockResponseCode(int responseCode) { + this.mockResponseCode = responseCode; + } + + public int getMockResponseCode() { + return mockResponseCode; + } + + public void setMockResponseContent(String content) { + mockResponseContent = content.getBytes(); + } + + public void setMockResponseContent(byte[] content) { + mockResponseContent = content; + } + + public byte[] getMockResponseContent() { + return mockResponseContent; + } + + public void setMockResponseContentType(String type) { + mockResponseContentType = type; + } + + public String getMockResponseContentType() { + return mockResponseContentType; + } + + public void setMockResponseContentEchoRequest(boolean echo) { + mockResponseContentEchoRequest = echo; + } + + public boolean getMockResponseContentEchoRequest() { + return mockResponseContentEchoRequest; + } + } + + private Thread serverThread = null; + private ServerSocket serverSocket = null; + private boolean serverStarted = false; + private ServerSocketFactory serverSocketFactory = null; + private int serverPort; + private int readTimeOut = 5000; // 5 + // seconds + private int delayResponseTime = 0; + private static byte[] NEW_LINE = "\r\n".getBytes(); + // request data + private String requestMethod = null; + private String requestUrl = null; + private Map> requestHeaders = + new HashMap>(); + private ByteArrayOutputStream requestContent = new ByteArrayOutputStream(); + private List mockHttpServerResponses = + new ArrayList(); + private int responseCounter = 0; + + public MockHttpServer(int serverPort) { + this(serverPort, false); + } + + public MockHttpServer(int serverPort, boolean ssl) { + mockHttpServerResponses.add(new MockHttpServerResponse()); // set a + // default + // response + this.serverPort = serverPort; + try { + serverSocketFactory = ServerSocketFactory.getDefault(); + if (ssl) { + serverSocketFactory = SSLServerSocketFactory.getDefault(); + } + while (serverSocket == null) { + try { + serverSocket = serverSocketFactory.createServerSocket(++this.serverPort); + } catch (BindException e) { + + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public synchronized void startServer() { + if (serverStarted) + return; + + // start the server thread + start(); + serverStarted = true; + + // wait for the server thread to start + waitForServerToStart(); + } + + private synchronized void waitForServerToStart() { + try { + wait(5000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private synchronized void waitForServerToStop() { + try { + wait(5000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void run() { + serverThread = Thread.currentThread(); + executeLoop(); + } + + private void executeLoop() { + serverStarted(); + try { + while (true) { + Socket socket = serverSocket.accept(); + HttpProcessor processor = new HttpProcessor(socket); + processor.run(); + } + } catch (IOException e) { + if (e instanceof SocketException) { + if (!("Socket closed".equalsIgnoreCase(e.getMessage()) || "Socket is closed" + .equalsIgnoreCase(e.getMessage()))) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } else { + e.printStackTrace(); + throw new RuntimeException(e); + } + } finally { + // notify that the server was stopped + serverStopped(); + } + } + + private synchronized void serverStarted() { + // notify the waiting thread that the thread started + notifyAll(); + } + + private synchronized void serverStopped() { + // notify the waiting thread that the thread started + notifyAll(); + } + + public synchronized void stopServer() { + if (!serverStarted) + return; + + try { + serverStarted = false; + // the server may be sleeping somewhere... + serverThread.interrupt(); + // close the server socket + serverSocket.close(); + // wait for the server to stop + waitForServerToStop(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private class HttpProcessor { + + private Socket socket; + + public HttpProcessor(Socket socket) throws SocketException { + // set the read timeout (5 seconds by default) + socket.setSoTimeout(readTimeOut); + socket.setKeepAlive(false); + this.socket = socket; + } + + public void run() { + try { + processRequest(socket); + processResponse(socket); + } catch (IOException e) { + if (e instanceof SocketException) { + if (!("socket closed".equalsIgnoreCase(e.getMessage()))) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } else { + e.printStackTrace(); + throw new RuntimeException(e); + } + } finally { + try { + socket.shutdownOutput(); + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private void processRequest(Socket socket) throws IOException { + requestContent.reset(); + BufferedInputStream is = new BufferedInputStream(socket.getInputStream()); + String requestMethodHeader = new String(readLine(is)); + processRequestMethod(requestMethodHeader); + processRequestHeaders(is); + processRequestContent(is); + } + + private void processRequestMethod(String requestMethodHeader) { + String[] parts = requestMethodHeader.split(" "); + if (parts.length < 2) { + throw new RuntimeException("illegal http request"); + } + requestMethod = parts[0]; + requestUrl = parts[1]; + } + + private void processRequestHeaders(InputStream is) throws IOException { + requestHeaders.clear(); + byte[] line; + while ((line = readLine(is)) != null) { + String lineStr = new String(line); + // if there are no more headers + if ("".equals(lineStr.trim())) { + break; + } + addRequestHeader(lineStr); + } + } + + private void processRequestContent(InputStream is) throws NumberFormatException, + IOException { + if (!("PUT".equals(requestMethod) || "POST".equals(requestMethod))) { + return; + } + + List transferEncodingValues = requestHeaders.get("Transfer-Encoding"); + String transferEncoding = + (transferEncodingValues == null || transferEncodingValues.isEmpty()) ? null + : transferEncodingValues.get(0); + if ("chunked".equals(transferEncoding)) { + processChunkedContent(is); + } else { + processRegularContent(is); + } + + if (mockHttpServerResponses.get(responseCounter).getMockResponseContentEchoRequest()) { + mockHttpServerResponses.get(responseCounter).setMockResponseContent(requestContent + .toByteArray()); + } + + } + + private void processRegularContent(InputStream is) throws IOException { + List contentLengthValues = requestHeaders.get("Content-Length"); + String contentLength = + (contentLengthValues == null || contentLengthValues.isEmpty()) ? null + : contentLengthValues.get(0); + if (contentLength == null) { + return; + } + int contentLen = Integer.parseInt(contentLength); + byte[] bytes = new byte[contentLen]; + is.read(bytes); + requestContent.write(bytes); + } + + private void processChunkedContent(InputStream is) throws IOException { + requestContent.write("".getBytes()); + byte[] chunk; + byte[] line = null; + boolean lastChunk = false; + // we should exit this loop only after we get to the end of stream + while (!lastChunk && (line = readLine(is)) != null) { + + String lineStr = new String(line); + // a chunk is identified as: + // 1) not an empty line + // 2) not 0. 0 means that there are no more chunks + if ("0".equals(lineStr)) { + lastChunk = true; + } + + if (!lastChunk) { + // get the length of the current chunk (it is in hexadecimal + // form) + int chunkLen = Integer.parseInt(lineStr, 16); + + // get the chunk + chunk = getChunk(is, chunkLen); + + // consume the newline after the chunk that separates + // between + // the chunk content and the next chunk size + readLine(is); + + requestContent.write(chunk); + } + } + + // do one last read to consume the empty line after the last chunk + if (lastChunk) { + readLine(is); + } + } + + private byte[] readLine(InputStream is) throws IOException { + int n; + ByteArrayOutputStream tmpOs = new ByteArrayOutputStream(); + while ((n = is.read()) != -1) { + if (n == '\r') { + n = is.read(); + if (n == '\n') { + return tmpOs.toByteArray(); + } else { + tmpOs.write('\r'); + if (n != -1) { + tmpOs.write(n); + } else { + return tmpOs.toByteArray(); + } + } + } else if (n == '\n') { + return tmpOs.toByteArray(); + } else { + tmpOs.write(n); + } + } + return tmpOs.toByteArray(); + } + + private byte[] getChunk(InputStream is, int len) throws IOException { + ByteArrayOutputStream chunk = new ByteArrayOutputStream(); + int read; + int totalRead = 0; + byte[] bytes = new byte[512]; + // read len bytes as the chunk + while (totalRead < len) { + read = is.read(bytes, 0, Math.min(bytes.length, len - totalRead)); + chunk.write(bytes, 0, read); + totalRead += read; + } + return chunk.toByteArray(); + } + + private void addRequestHeader(String line) { + String[] parts = line.split(": "); + List values = requestHeaders.get(parts[0]); + if (values == null) { + values = new ArrayList(); + requestHeaders.put(parts[0], values); + } + values.add(parts[1]); + } + + private void processResponse(Socket socket) throws IOException { + // if delaying the response failed (because it was interrupted) + // then don't send the response + if (!delayResponse()) + return; + + OutputStream sos = socket.getOutputStream(); + BufferedOutputStream os = new BufferedOutputStream(sos); + String reason = ""; + Status statusCode = + Response.Status.fromStatusCode(mockHttpServerResponses.get(responseCounter) + .getMockResponseCode()); + if (statusCode != null) { + reason = statusCode.toString(); + } + os.write(("HTTP/1.1 " + mockHttpServerResponses.get(responseCounter) + .getMockResponseCode() + + " " + reason).getBytes()); + os.write(NEW_LINE); + processResponseHeaders(os); + processResponseContent(os); + os.flush(); + responseCounter++; + } + + // return: + // true - delay was successful + // false - delay was unsuccessful + private boolean delayResponse() { + // delay the response by delayResponseTime milliseconds + if (delayResponseTime > 0) { + try { + Thread.sleep(delayResponseTime); + return true; + } catch (InterruptedException e) { + return false; + } + } + return true; + } + + private void processResponseContent(OutputStream os) throws IOException { + if (mockHttpServerResponses.get(responseCounter).getMockResponseContent() == null) { + return; + } + + os.write(mockHttpServerResponses.get(responseCounter).getMockResponseContent()); + } + + private void processResponseHeaders(OutputStream os) throws IOException { + addServerResponseHeaders(); + for (String header : mockHttpServerResponses.get(responseCounter) + .getMockResponseHeaders().keySet()) { + os.write((header + ": " + mockHttpServerResponses.get(responseCounter) + .getMockResponseHeaders().get(header)).getBytes()); + os.write(NEW_LINE); + } + os.write(NEW_LINE); + } + + private void addServerResponseHeaders() { + Map mockResponseHeaders = + mockHttpServerResponses.get(responseCounter).getMockResponseHeaders(); + mockResponseHeaders.put("Content-Type", mockHttpServerResponses.get(responseCounter) + .getMockResponseContentType()); + mockResponseHeaders.put("Content-Length", mockHttpServerResponses.get(responseCounter) + .getMockResponseContent().length + ""); + mockResponseHeaders.put("Server", "Mock HTTP Server v1.0"); + mockResponseHeaders.put("Connection", "closed"); + } + } + + public void setReadTimeout(int milliseconds) { + readTimeOut = milliseconds; + } + + public void setDelayResponse(int milliseconds) { + delayResponseTime = milliseconds; + } + + public String getRequestContentAsString() { + return requestContent.toString(); + } + + public byte[] getRequestContent() { + return requestContent.toByteArray(); + } + + public Map> getRequestHeaders() { + return requestHeaders; + } + + public String getRequestMethod() { + return requestMethod; + } + + public String getRequestUrl() { + return requestUrl; + } + + public void setMockHttpServerResponses(MockHttpServerResponse... responses) { + mockHttpServerResponses.clear(); + mockHttpServerResponses.addAll(Arrays.asList(responses)); + } + + public List getMockHttpServerResponses() { + return mockHttpServerResponses; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + public int getServerPort() { + return serverPort; + } +} diff -Nru libpdfbox2-java-2.0.9/fontbox/pom.xml libpdfbox2-java-2.0.13/fontbox/pom.xml --- libpdfbox2-java-2.0.9/fontbox/pom.xml 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/pom.xml 2018-11-28 17:18:34.000000000 +0000 @@ -21,7 +21,7 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 ../parent/pom.xml @@ -70,7 +70,6 @@ com.googlecode.maven-download-plugin download-maven-plugin - 1.3.0 PDFBOX-4038 diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/appended-resources/META-INF/LICENSE libpdfbox2-java-2.0.13/fontbox/src/main/appended-resources/META-INF/LICENSE --- libpdfbox2-java-2.0.9/fontbox/src/main/appended-resources/META-INF/LICENSE 2018-03-20 16:19:46.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/appended-resources/META-INF/LICENSE 2018-11-28 17:18:34.000000000 +0000 @@ -30,3 +30,100 @@ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Lohit-Bengali font (https://pagure.io/lohit): + + Copyright 2011-13 Lohit Fonts Project contributors + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. + This license is copied below, and is also available with a FAQ at: + http://scripts.sil.org/OFL + + + ----------------------------------------------------------- + SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + ----------------------------------------------------------- + + PREAMBLE + The goals of the Open Font License (OFL) are to stimulate worldwide + development of collaborative font projects, to support the font creation + efforts of academic and linguistic communities, and to provide a free and + open framework in which fonts may be shared and improved in partnership + with others. + + The OFL allows the licensed fonts to be used, studied, modified and + redistributed freely as long as they are not sold by themselves. The + fonts, including any derivative works, can be bundled, embedded, + redistributed and/or sold with any software provided that any reserved + names are not used by derivative works. The fonts and derivatives, + however, cannot be released under any other type of license. The + requirement for fonts to remain under this license does not apply + to any document created using the fonts or their derivatives. + + DEFINITIONS + "Font Software" refers to the set of files released by the Copyright + Holder(s) under this license and clearly marked as such. This may + include source files, build scripts and documentation. + + "Reserved Font Name" refers to any names specified as such after the + copyright statement(s). + + "Original Version" refers to the collection of Font Software components as + distributed by the Copyright Holder(s). + + "Modified Version" refers to any derivative made by adding to, deleting, + or substituting -- in part or in whole -- any of the components of the + Original Version, by changing formats or by porting the Font Software to a + new environment. + + "Author" refers to any designer, engineer, programmer, technical + writer or other person who contributed to the Font Software. + + PERMISSION & CONDITIONS + Permission is hereby granted, free of charge, to any person obtaining + a copy of the Font Software, to use, study, copy, merge, embed, modify, + redistribute, and sell modified and unmodified copies of the Font + Software, subject to the following conditions: + + 1) Neither the Font Software nor any of its individual components, + in Original or Modified Versions, may be sold by itself. + + 2) Original or Modified Versions of the Font Software may be bundled, + redistributed and/or sold with any software, provided that each copy + contains the above copyright notice and this license. These can be + included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or + binary files as long as those fields can be easily viewed by the user. + + 3) No Modified Version of the Font Software may use the Reserved Font + Name(s) unless explicit written permission is granted by the corresponding + Copyright Holder. This restriction only applies to the primary font name as + presented to the users. + + 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + Software shall not be used to promote, endorse or advertise any + Modified Version, except to acknowledge the contribution(s) of the + Copyright Holder(s) and the Author(s) or with their explicit written + permission. + + 5) The Font Software, modified or unmodified, in part or in whole, + must be distributed entirely under this license, and must not be + distributed under any other license. The requirement for fonts to + remain under this license does not apply to any document created + using the Font Software. + + TERMINATION + This license becomes null and void if any of the above conditions are + not met. + + DISCLAIMER + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT + OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE + COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL + DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM + OTHER DEALINGS IN THE FONT SOFTWARE. diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/afm/AFMParser.java libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/afm/AFMParser.java --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/afm/AFMParser.java 2018-03-20 16:19:46.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/afm/AFMParser.java 2018-11-28 17:18:32.000000000 +0000 @@ -951,9 +951,11 @@ buf.append( (char)nextByte ); //now read the data - while( !isEOL(nextByte = input.read()) ) + nextByte = input.read(); + while (nextByte != -1 && !isEOL(nextByte)) { - buf.append( (char)nextByte ); + buf.append((char) nextByte); + nextByte = input.read(); } return buf.toString(); } @@ -978,9 +980,11 @@ buf.append( (char)nextByte ); //now read the data - while( !isWhitespace(nextByte = input.read()) ) + nextByte = input.read(); + while (nextByte != -1 && !isWhitespace(nextByte)) { - buf.append( (char)nextByte ); + buf.append((char) nextByte); + nextByte = input.read(); } return buf.toString(); } diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/afm/package.html libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/afm/package.html --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/afm/package.html 2018-03-20 16:19:46.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/afm/package.html 2018-11-28 17:18:34.000000000 +0000 @@ -21,7 +21,7 @@ This package holds classes used to parse AFM(Adobe Font Metrics) files. -
+
More information about AFM files can be found at http://partners.adobe.com/asn/developer/type/ diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/cff/CFFParser.java libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/cff/CFFParser.java --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/cff/CFFParser.java 2018-03-20 16:19:46.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/cff/CFFParser.java 2018-11-28 17:18:34.000000000 +0000 @@ -24,6 +24,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.fontbox.util.Charsets; @@ -33,6 +35,11 @@ */ public class CFFParser { + /** + * Log instance. + */ + private static final Log LOG = LogFactory.getLog(CFFParser.class); + private static final String TAG_OTTO = "OTTO"; private static final String TAG_TTCF = "ttcf"; private static final String TAG_TTFONLY = "\u0000\u0001\u0000\u0000"; @@ -336,6 +343,7 @@ StringBuilder sb = new StringBuilder(); boolean done = false; boolean exponentMissing = false; + boolean hasExponent = false; while (!done) { int b = input.readUnsignedByte(); @@ -361,12 +369,24 @@ sb.append("."); break; case 0xb: + if (hasExponent) + { + LOG.warn("duplicate 'E' ignored after " + sb); + break; + } sb.append("E"); exponentMissing = true; + hasExponent = true; break; case 0xc: + if (hasExponent) + { + LOG.warn("duplicate 'E-' ignored after " + sb); + break; + } sb.append("E-"); exponentMissing = true; + hasExponent = true; break; case 0xd: break; diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java 2018-03-20 16:19:46.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java 2018-11-28 17:18:34.000000000 +0000 @@ -22,6 +22,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; /** * This class represents a CMap file. @@ -30,6 +32,8 @@ */ public class CMap { + private static final Log LOG = LogFactory.getLog(CMap.class); + private int wmode = 0; private String cmapName = null; private String cmapVersion = null; @@ -120,7 +124,13 @@ bytes[byteCount] = (byte)in.read(); } } - throw new IOException("CMap is invalid"); + String seq = ""; + for (int i = 0; i < maxCodeLength; ++i) + { + seq += String.format("0x%02X (%04o) ", bytes[i], bytes[i]); + } + LOG.warn("Invalid character code sequence " + seq + "in CMap " + cmapName); + return 0; } /** diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/ttf/CmapSubtable.java libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/ttf/CmapSubtable.java --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/ttf/CmapSubtable.java 2018-03-20 16:19:46.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/ttf/CmapSubtable.java 2018-11-28 17:18:34.000000000 +0000 @@ -45,7 +45,7 @@ private long subTableOffset; private int[] glyphIdToCharacterCode; private final Map> glyphIdToCharacterCodeMultiple = new HashMap>(); - private Map characterCodeToGlyphId; + private Map characterCodeToGlyphId= new HashMap(); /** * This will read the required data from the stream. @@ -513,6 +513,10 @@ if (p > 0) { p = (p + idDelta) % 65536; + if (p < 0) + { + p += 65536; + } } if (p >= numGlyphs) diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/ttf/CmapTable.java libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/ttf/CmapTable.java --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/ttf/CmapTable.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/ttf/CmapTable.java 2018-11-28 17:18:34.000000000 +0000 @@ -68,8 +68,10 @@ * @param data The stream to read the data from. * @throws IOException If there is an error reading the data. */ + @Override public void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { + @SuppressWarnings({"unused", "squid:S1854", "squid:S1481"}) int version = data.readUnsignedShort(); int numberOfTables = data.readUnsignedShort(); cmaps = new CmapSubtable[numberOfTables]; diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java 2018-03-20 16:19:46.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java 2018-11-28 17:18:34.000000000 +0000 @@ -80,10 +80,14 @@ /** * Returns all glyphs. This method can be very slow. + * + * @throws IOException If there is an error reading the data. */ public GlyphData[] getGlyphs() throws IOException { - synchronized (font) + // PDFBOX-4219: synchronize on data because it is accessed by several threads + // when PDFBox is accessing a standard 14 font for the first time + synchronized (data) { // the glyph offsets long[] offsets = loca.getOffsets(); @@ -157,7 +161,9 @@ return glyphs[gid]; } - synchronized (font) + // PDFBOX-4219: synchronize on data because it is accessed by several threads + // when PDFBox is accessing a standard 14 font for the first time + synchronized (data) { // read a single glyph long[] offsets = loca.getOffsets(); diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/ttf/RAFDataStream.java libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/ttf/RAFDataStream.java --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/ttf/RAFDataStream.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/ttf/RAFDataStream.java 2018-11-28 17:18:34.000000000 +0000 @@ -96,8 +96,11 @@ @Override public void close() throws IOException { - raf.close(); - raf = null; + if (raf != null) + { + raf.close(); + raf = null; + } } /** diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java 2018-03-20 16:19:46.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java 2018-11-28 17:18:34.000000000 +0000 @@ -343,12 +343,17 @@ */ void readTable(TTFTable table) throws IOException { - // save current position - long currentPosition = data.getCurrentPosition(); - data.seek(table.getOffset()); - table.read(this, data); - // restore current position - data.seek(currentPosition); + // PDFBOX-4219: synchronize on data because it is accessed by several threads + // when PDFBox is accessing a standard 14 font for the first time + synchronized (data) + { + // save current position + long currentPosition = data.getCurrentPosition(); + data.seek(table.getOffset()); + table.read(this, data); + // restore current position + data.seek(currentPosition); + } } /** diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/util/autodetect/FontFileFinder.java libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/util/autodetect/FontFileFinder.java --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/util/autodetect/FontFileFinder.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/util/autodetect/FontFileFinder.java 2018-11-28 17:18:34.000000000 +0000 @@ -48,16 +48,17 @@ { return new WindowsFontDirFinder(); } + else if (osName.startsWith("Mac")) + { + return new MacFontDirFinder(); + } + else if (osName.startsWith("OS/400")) + { + return new OS400FontDirFinder(); + } else { - if (osName.startsWith("Mac")) - { - return new MacFontDirFinder(); - } - else - { - return new UnixFontDirFinder(); - } + return new UnixFontDirFinder(); } } diff -Nru libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/util/autodetect/OS400FontDirFinder.java libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/util/autodetect/OS400FontDirFinder.java --- libpdfbox2-java-2.0.9/fontbox/src/main/java/org/apache/fontbox/util/autodetect/OS400FontDirFinder.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/main/java/org/apache/fontbox/util/autodetect/OS400FontDirFinder.java 2018-11-28 17:18:34.000000000 +0000 @@ -0,0 +1,31 @@ +/* + * Copyright 2018 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.fontbox.util.autodetect; + +/** + * Font finder for OS/400 systems. + */ +public class OS400FontDirFinder extends NativeFontDirFinder +{ + @Override + protected String[] getSearchableDirectories() + { + return new String[] { System.getProperty("user.home") + "/.fonts", // user + "/QIBM/ProdData/OS400/Fonts" + }; + } +} diff -Nru libpdfbox2-java-2.0.9/fontbox/src/test/java/org/apache/fontbox/afm/AFMParserTest.java libpdfbox2-java-2.0.13/fontbox/src/test/java/org/apache/fontbox/afm/AFMParserTest.java --- libpdfbox2-java-2.0.9/fontbox/src/test/java/org/apache/fontbox/afm/AFMParserTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/test/java/org/apache/fontbox/afm/AFMParserTest.java 2018-11-28 17:18:32.000000000 +0000 @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.fontbox.afm; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import org.apache.fontbox.util.Charsets; +import org.junit.Assert; +import org.junit.Test; + +/** + * + * @author Tilman Hausherr + */ +public class AFMParserTest +{ + @Test + public void testEof() throws IOException + { + try + { + new AFMParser(new ByteArrayInputStream("huhu".getBytes(Charsets.US_ASCII))).parse(); + } + catch (IOException ex) + { + Assert.assertEquals("Error: The AFM file should start with StartFontMetrics and not 'huhu'", ex.getMessage()); + } + } +} diff -Nru libpdfbox2-java-2.0.9/fontbox/src/test/java/org/apache/fontbox/ttf/RAFDataStreamTest.java libpdfbox2-java-2.0.13/fontbox/src/test/java/org/apache/fontbox/ttf/RAFDataStreamTest.java --- libpdfbox2-java-2.0.9/fontbox/src/test/java/org/apache/fontbox/ttf/RAFDataStreamTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/fontbox/src/test/java/org/apache/fontbox/ttf/RAFDataStreamTest.java 2018-11-28 17:18:32.000000000 +0000 @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.fontbox.ttf; + +import java.io.IOException; +import org.junit.Test; + +/** + * + * @author Tilman Hausherr + */ +public class RAFDataStreamTest +{ + /** + * Test of PDFBOX-4242: make sure that the Closeable.close() contract is fulfilled. + * + * @throws IOException + */ + @Test + public void testDoubleClose() throws IOException + { + RAFDataStream raf = new RAFDataStream("src/test/resources/ttf/LiberationSans-Regular.ttf", "r"); + raf.close(); + raf.close(); + } +} Binary files /tmp/tmpAhqVVB/umSeuAuTn3/libpdfbox2-java-2.0.9/fontbox/src/test/resources/ttf/Lohit-Bengali.ttf and /tmp/tmpAhqVVB/Y0XihoDyIA/libpdfbox2-java-2.0.13/fontbox/src/test/resources/ttf/Lohit-Bengali.ttf differ diff -Nru libpdfbox2-java-2.0.9/parent/pom.xml libpdfbox2-java-2.0.13/parent/pom.xml --- libpdfbox2-java-2.0.9/parent/pom.xml 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/parent/pom.xml 2018-11-28 17:18:40.000000000 +0000 @@ -23,13 +23,17 @@ org.apache apache - 16 + + + 19 + org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 pom PDFBox parent @@ -39,7 +43,7 @@ The Apache Software Foundation http://pdfbox.apache.org - + jira https://issues.apache.org/jira/browse/PDFBOX @@ -48,11 +52,9 @@ UTF-8 UTF-8 + + 1.60 - - - 3.0.0 - @@ -70,46 +72,41 @@ commons-io commons-io - 2.4 + 2.6 test org.bouncycastle bcprov-jdk15on - 1.54 + ${bouncycastle.version} org.bouncycastle bcmail-jdk15on - 1.54 + ${bouncycastle.version} org.bouncycastle bcpkix-jdk15on - 1.54 - - - log4j - log4j - 1.2.17 + ${bouncycastle.version} org.apache.pdfbox jbig2-imageio - 3.0.0 + 3.0.2 test - com.github.jai-imageio jai-imageio-core - 1.3.1 + 1.4.0 test - +
com.github.jai-imageio jai-imageio-jpeg2000 @@ -123,11 +120,41 @@ jdk9 + + + [9,10] + --add-modules java.activation --add-modules java.xml.bind + + + jdk11 + + [11,) + + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + javax.activation + activation + 1.1.1 + + + + + pedantic @@ -146,7 +173,7 @@ org.owasp dependency-check-maven - 3.1.1 + 3.3.3 true @@ -166,6 +193,23 @@ + maven-enforcer-plugin + + + + enforce + + + + + 3.0.0 + + + + + + + maven-compiler-plugin true @@ -179,10 +223,11 @@ 1.6 - http://download.oracle.com/javase/1.6.0/docs/api/ + https://docs.oracle.com/javase/6/docs/api/ UTF-8 true + en @@ -221,7 +266,7 @@ org.codehaus.mojo animal-sniffer-maven-plugin - 1.14 + 1.17 check-java-version @@ -238,14 +283,24 @@ - + + + com.googlecode.maven-download-plugin + download-maven-plugin + 1.3.0 + + + com.googlecode.maven-download-plugin + + maven-download-plugin + 1.1.0 + org.apache.rat apache-rat-plugin - 0.11 release.properties @@ -258,6 +313,11 @@ 2.5.4 + + + maven-surefire-plugin + 2.22.1 + @@ -428,8 +488,8 @@ - scm:svn:http://svn.apache.org/repos/asf/maven/pom/tags/2.0.9/pdfbox-parent - scm:svn:https://svn.apache.org/repos/asf/maven/pom/tags/2.0.9/pdfbox-parent - http://svn.apache.org/viewvc/maven/pom/tags/2.0.9/pdfbox-parent + scm:svn:http://svn.apache.org/repos/asf/maven/pom/tags/2.0.13/pdfbox-parent + scm:svn:https://svn.apache.org/repos/asf/maven/pom/tags/2.0.13/pdfbox-parent + http://svn.apache.org/viewvc/maven/pom/tags/2.0.13/pdfbox-parent diff -Nru libpdfbox2-java-2.0.9/pdfbox/pom.xml libpdfbox2-java-2.0.13/pdfbox/pom.xml --- libpdfbox2-java-2.0.9/pdfbox/pom.xml 2018-03-20 16:19:52.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/pom.xml 2018-11-28 17:18:38.000000000 +0000 @@ -23,7 +23,7 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 ../parent/pom.xml @@ -184,7 +184,6 @@ com.googlecode.maven-download-plugin download-maven-plugin - 1.3.0 PDFBOX-3208 @@ -393,6 +392,92 @@ 8eafe21ffa6f3d7d0a50e9f4e5bcdeb727e804b552d74e65b709e778c9ed4605e5aa63743be285f0bc17ad162768583fec4196e1d1146d98f8703359247f22d0 + + PDFBOX-4197 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12919726/sample.pdf + ${project.build.directory}/pdfs + PDFBOX-4197.pdf + 6fefc869dff9db8cd539db177d35beeacc62304173245742eaee8882dab330860a31cbbd4c4ec6cc724603cc453afc07ec61361fbc1e80a47f44b04ccfbaf40d + + + + PDFBOX-4184 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12949710/032163.jpg + + ${project.build.directory}/imgs + PDFBOX-4184-032163.jpg + 35241c979d3808ca9d2641b5ec5e40637132b313f75070faca8b8f6d00ddce394070414236db3993f1092fe3bc16995750d528b6d803a7851423c14c308ccdde + + + + PDFBOX-4184-2 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12929821/16bit.png + + ${project.build.directory}/imgs + PDFBOX-4184-16bit.png + 45f148913590ea1a94c3ac17080969b74e579fe51967a5bf535caa3f7104ea81ee222b99deb8ee528b0a53640f97d87cf668633a1bdd61a62092246df1807471 + + + + PDFBOX-4308 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12938094/Quelldatei.pdf + ${project.build.directory}/pdfs + PDFBOX-4308.pdf + 566346239d51f10b2ccfc435620e8f3b0281e91286983cb86660060a8d48777998eab46dfda93d35024e7e4b50b7ab6654f9a1002524163d228a5e41a80a1221 + + + + PDFBOX-4338 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12943502/ArrayIndexOutOfBoundsException%20COSParser + ${project.build.directory}/pdfs + PDFBOX-4338.pdf + 130fa4b49345410b203613f3e67263f483f9a9797bef22322647655bb55cc55bcb1d1e0eb03c27f6f2855b3823675b27e8899d8eeb880d27a74fad5f60f23b47 + + + + PDFBOX-4339 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12943503/NullPointerException%20COSParser + ${project.build.directory}/pdfs + PDFBOX-4339.pdf + 2e48aeae83ef6fc4c5f95aafdfe8c76dd8d2dcf3516701c70ffeb14f06ba246a17c21f2dadf8fa48bccef5b72daffdd30ed7c9aa7f5183ddf889968caa2ded6a + + diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java 2018-11-28 17:18:38.000000000 +0000 @@ -118,7 +118,7 @@ } /** - * Initialises the stream engine for the given page. + * Initializes the stream engine for the given page. */ private void initPage(PDPage page) { @@ -136,7 +136,7 @@ } /** - * This will initialise and process the contents of the stream. + * This will initialize and process the contents of the stream. * * @param page the page to process * @throws IOException if there is an error accessing the stream @@ -1008,7 +1008,8 @@ } /** - * @return the stream' resources. + * @return the stream' resources. This is mainly to be used by the {@link OperatorProcessor} + * classes. */ public PDResources getResources() { diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/cos/COSArray.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/cos/COSArray.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/cos/COSArray.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/cos/COSArray.java 2018-11-28 17:18:36.000000000 +0000 @@ -534,9 +534,11 @@ public float[] toFloatArray() { float[] retval = new float[size()]; - for( int i=0; i filters; private final COSDictionary parameters; - // todo: this is an in-memory buffer, should use scratch file (if any) instead - private ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - + private final ScratchFile scratchFile; + private RandomAccess buffer; + /** * Creates a new COSOutputStream writes to an encoded COS stream. * * @param filters Filters to apply. * @param parameters Filter parameters. * @param output Encoded stream. - * @param scratchFile Scratch file to use, or null. + * @param scratchFile Scratch file to use. + * + * @throws IOException If there was an error creating a temporary buffer */ COSOutputStream(List filters, COSDictionary parameters, OutputStream output, - ScratchFile scratchFile) + ScratchFile scratchFile) throws IOException { super(output); this.filters = filters; this.parameters = parameters; + this.scratchFile = scratchFile; + + if (filters.isEmpty()) + { + this.buffer = null; + } + else + { + this.buffer = scratchFile.createBuffer(); + } } @Override public void write(byte[] b) throws IOException { - buffer.write(b); + if (buffer != null) + { + buffer.write(b); + } + else + { + super.write(b); + } } @Override public void write(byte[] b, int off, int len) throws IOException { - buffer.write(b, off, len); + if (buffer != null) + { + buffer.write(b, off, len); + } + else + { + super.write(b, off, len); + } } @Override public void write(int b) throws IOException { - buffer.write(b); + if (buffer != null) + { + buffer.write(b); + } + else + { + super.write(b); + } } @Override public void flush() throws IOException { } - + @Override public void close() throws IOException { - if (buffer == null) - { - return; + try { + if (buffer != null) + { + try + { + // apply filters in reverse order + for (int i = filters.size() - 1; i >= 0; i--) + { + InputStream unfilteredIn = new RandomAccessInputStream(buffer); + try + { + if (i == 0) + { + /* + * The last filter to run can encode directly to the enclosed output + * stream. + */ + filters.get(i).encode(unfilteredIn, out, parameters, i); + } + else + { + RandomAccess filteredBuffer = scratchFile.createBuffer(); + try + { + OutputStream filteredOut = new RandomAccessOutputStream(filteredBuffer); + try + { + filters.get(i).encode(unfilteredIn, filteredOut, parameters, i); + } + finally + { + filteredOut.close(); + } + + RandomAccess tmpSwap = filteredBuffer; + filteredBuffer = buffer; + buffer = tmpSwap; + } + finally + { + filteredBuffer.close(); + } + } + } + finally + { + unfilteredIn.close(); + } + } + } + finally + { + buffer.close(); + buffer = null; + } + } } - // apply filters in reverse order - for (int i = filters.size() - 1; i >= 0; i--) + finally { - // todo: this is an in-memory buffer, should use scratch file (if any) instead - ByteArrayInputStream input = new ByteArrayInputStream(buffer.toByteArray()); - buffer = new ByteArrayOutputStream(); - filters.get(i).encode(input, buffer, parameters, i); - } - // flush the entire stream - out.write(buffer.toByteArray()); - super.close(); - buffer = null; + super.close(); + } } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/cos/package.html libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/cos/package.html --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/cos/package.html 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/cos/package.html 2018-11-28 17:18:36.000000000 +0000 @@ -21,7 +21,7 @@ These are the low level objects that make up a PDF document. -

+

See the PDF Reference 1.4. diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/ASCIIHexFilter.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/ASCIIHexFilter.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/ASCIIHexFilter.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/ASCIIHexFilter.java 2018-11-28 17:18:38.000000000 +0000 @@ -45,7 +45,22 @@ /* 70 */ 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 80 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 90 */ -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, - /* 100 */ 13, 14, 15 + /* 100 */ 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, + /* 110 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 120 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 130 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 140 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 150 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 160 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 170 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 180 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 190 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 200 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 210 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 220 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 230 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 240 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 250 */ -1, -1, -1, -1, -1, -1 }; @Override diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxDecoderStream.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxDecoderStream.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxDecoderStream.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxDecoderStream.java 2018-11-28 17:18:38.000000000 +0000 @@ -258,7 +258,7 @@ private void decodeRowType4() throws IOException { if(optionByteAligned) { - bufferPos = -1; // Skip remaining bits and fetch the next byte at row start + resetBuffer(); } eof: while (true) { // read till next EOL code @@ -287,7 +287,7 @@ private void decodeRowType6() throws IOException { if(optionByteAligned) { - bufferPos = -1; // Skip remaining bits and fetch the next byte at row start + resetBuffer(); } decode2D(); } @@ -383,17 +383,7 @@ } private void resetBuffer() throws IOException { - for (int i = 0; i < decodedRow.length; i++) { - decodedRow[i] = 0; - } - - while (true) { - if (bufferPos == -1) { - return; - } - - readBit(); - } + bufferPos = -1; } int buffer = -1; diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/Filter.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/Filter.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/Filter.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/Filter.java 2018-11-28 17:18:38.000000000 +0000 @@ -123,7 +123,11 @@ COSArray array = (COSArray)obj; if (index < array.size()) { - return (COSDictionary)array.getObject(index); + COSBase objAtIndex = array.getObject(index); + if (objAtIndex instanceof COSDictionary) + { + return (COSDictionary)array.getObject(index); + } } } else if (obj != null && !(filter instanceof COSArray || obj instanceof COSArray)) @@ -161,4 +165,20 @@ return reader; } + /** + * @return the ZIP compression level configured for PDFBox + */ + public static int getCompressionLevel() + { + int compressionLevel = Deflater.DEFAULT_COMPRESSION; + try + { + compressionLevel = Integer.parseInt(System.getProperty(Filter.SYSPROP_DEFLATELEVEL, "-1")); + } + catch (NumberFormatException ex) + { + LOG.warn(ex.getMessage(), ex); + } + return Math.max(-1, Math.min(Deflater.BEST_COMPRESSION, compressionLevel)); + } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/FlateFilter.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/FlateFilter.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/FlateFilter.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/FlateFilter.java 2018-11-28 17:18:38.000000000 +0000 @@ -16,8 +16,6 @@ */ package org.apache.pdfbox.filter; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -28,7 +26,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.pdfbox.cos.COSDictionary; -import org.apache.pdfbox.cos.COSName; /** * Decompresses data encoded using the zlib/deflate compression method, @@ -46,34 +43,12 @@ public DecodeResult decode(InputStream encoded, OutputStream decoded, COSDictionary parameters, int index) throws IOException { - int predictor = -1; - final COSDictionary decodeParams = getDecodeParams(parameters, index); - if (decodeParams != null) - { - predictor = decodeParams.getInt(COSName.PREDICTOR); - } try { - if (predictor > 1) - { - int colors = Math.min(decodeParams.getInt(COSName.COLORS, 1), 32); - int bitsPerPixel = decodeParams.getInt(COSName.BITS_PER_COMPONENT, 8); - int columns = decodeParams.getInt(COSName.COLUMNS, 1); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - decompress(encoded, baos); - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - Predictor.decodePredictor(predictor, colors, bitsPerPixel, columns, bais, decoded); - decoded.flush(); - baos.reset(); - bais.reset(); - } - else - { - decompress(encoded, decoded); - } - } + decompress(encoded, Predictor.wrapPredictor(decoded, decodeParams)); + } catch (DataFormatException e) { // if the stream is corrupt a DataFormatException may occur @@ -98,7 +73,7 @@ // use nowrap mode to bypass zlib-header and checksum to avoid a DataFormatException Inflater inflater = new Inflater(true); inflater.setInput(buf,0,read); - byte[] res = new byte[1024]; + byte[] res = new byte[1024]; boolean dataWritten = false; while (true) { @@ -143,16 +118,7 @@ protected void encode(InputStream input, OutputStream encoded, COSDictionary parameters) throws IOException { - int compressionLevel = Deflater.DEFAULT_COMPRESSION; - try - { - compressionLevel = Integer.parseInt(System.getProperty(Filter.SYSPROP_DEFLATELEVEL, "-1")); - } - catch (NumberFormatException ex) - { - LOG.warn(ex.getMessage(), ex); - } - compressionLevel = Math.max(-1, Math.min(Deflater.BEST_COMPRESSION, compressionLevel)); + int compressionLevel = getCompressionLevel(); Deflater deflater = new Deflater(compressionLevel); DeflaterOutputStream out = new DeflaterOutputStream(encoded, deflater); int amountRead; @@ -167,5 +133,6 @@ } out.close(); encoded.flush(); + deflater.end(); } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/JPXFilter.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/JPXFilter.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/JPXFilter.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/JPXFilter.java 2018-11-28 17:18:38.000000000 +0000 @@ -16,11 +16,14 @@ */ package org.apache.pdfbox.filter; +import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferUShort; -import java.awt.image.WritableRaster; +import java.awt.image.IndexColorModel; +import java.awt.image.MultiPixelPackedSampleModel; +import java.awt.image.Raster; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -49,6 +52,9 @@ */ public final class JPXFilter extends Filter { + /** + * {@inheritDoc} + */ @Override public DecodeResult decode(InputStream encoded, OutputStream decoded, COSDictionary parameters, int index, DecodeOptions options) throws IOException @@ -57,7 +63,7 @@ result.getParameters().addAll(parameters); BufferedImage image = readJPX(encoded, options, result); - WritableRaster raster = image.getRaster(); + Raster raster = image.getRaster(); switch (raster.getDataBuffer().getDataType()) { case DataBuffer.TYPE_BYTE: @@ -74,6 +80,23 @@ } return result; + case DataBuffer.TYPE_INT: + // not yet used (as of October 2018) but works as fallback + // if we decide to convert to BufferedImage.TYPE_INT_RGB + int[] ar = new int[raster.getNumBands()]; + for (int y = 0; y < image.getHeight(); ++y) + { + for (int x = 0; x < image.getWidth(); ++x) + { + raster.getPixel(x, y, ar); + for (int i = 0; i < ar.length; ++i) + { + decoded.write(ar[i]); + } + } + } + return result; + default: throw new IOException("Data type " + raster.getDataBuffer().getDataType() + " not implemented"); } @@ -136,7 +159,21 @@ // extract embedded color space if (!parameters.containsKey(COSName.COLORSPACE)) { - result.setColorSpace(new PDJPXColorSpace(image.getColorModel().getColorSpace())); + if (image.getSampleModel() instanceof MultiPixelPackedSampleModel && + image.getColorModel().getPixelSize() == 1 && + image.getRaster().getNumBands() == 1 && + image.getColorModel() instanceof IndexColorModel) + { + // PDFBOX-4326: + // force CS_GRAY colorspace because colorspace in IndexColorModel + // has 3 colors despite that there is only 1 color per pixel + // in raster + result.setColorSpace(new PDJPXColorSpace(ColorSpace.getInstance(ColorSpace.CS_GRAY))); + } + else + { + result.setColorSpace(new PDJPXColorSpace(image.getColorModel().getColorSpace())); + } } return image; @@ -151,6 +188,9 @@ } } + /** + * {@inheritDoc} + */ @Override protected void encode(InputStream input, OutputStream encoded, COSDictionary parameters) throws IOException diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/LZWFilter.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/LZWFilter.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/LZWFilter.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/LZWFilter.java 2018-11-28 17:18:38.000000000 +0000 @@ -15,8 +15,6 @@ */ package org.apache.pdfbox.filter; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -67,37 +65,15 @@ public DecodeResult decode(InputStream encoded, OutputStream decoded, COSDictionary parameters, int index) throws IOException { - int predictor = -1; - int earlyChange = 1; - COSDictionary decodeParams = getDecodeParams(parameters, index); - if (decodeParams != null) - { - predictor = decodeParams.getInt(COSName.PREDICTOR); - earlyChange = decodeParams.getInt(COSName.EARLY_CHANGE, 1); - if (earlyChange != 0 && earlyChange != 1) - { - earlyChange = 1; - } - } - if (predictor > 1) - { - @SuppressWarnings("null") - int colors = Math.min(decodeParams.getInt(COSName.COLORS, 1), 32); - int bitsPerPixel = decodeParams.getInt(COSName.BITS_PER_COMPONENT, 8); - int columns = decodeParams.getInt(COSName.COLUMNS, 1); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - doLZWDecode(encoded, baos, earlyChange); - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - Predictor.decodePredictor(predictor, colors, bitsPerPixel, columns, bais, decoded); - decoded.flush(); - baos.reset(); - bais.reset(); - } - else + int earlyChange = decodeParams.getInt(COSName.EARLY_CHANGE, 1); + + if (earlyChange != 0 && earlyChange != 1) { - doLZWDecode(encoded, decoded, earlyChange); + earlyChange = 1; } + + doLZWDecode(encoded, Predictor.wrapPredictor(decoded, decodeParams), earlyChange); return new DecodeResult(parameters); } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/Predictor.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/Predictor.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/filter/Predictor.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/filter/Predictor.java 2018-11-28 17:18:38.000000000 +0000 @@ -15,10 +15,14 @@ */ package org.apache.pdfbox.filter; +import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Arrays; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.io.IOUtils; /** @@ -31,6 +35,173 @@ private Predictor() { } + + /** + * Decodes a single line of data in-place. + * @param predictor Predictor value for the current line + * @param colors Number of color components, from decode parameters. + * @param bitsPerComponent Number of bits per components, from decode parameters. + * @param columns Number samples in a row, from decode parameters. + * @param actline Current (active) line to decode. Data will be decoded in-place, + * i.e. - the contents of this buffer will be modified. + * @param lastline The previous decoded line. When decoding the first line, this + * parameter should be an empty byte array of the same length as + * actline. + */ + static void decodePredictorRow(int predictor, int colors, int bitsPerComponent, int columns, byte[] actline, byte[] lastline) + { + if (predictor == 1) + { + // no prediction + return; + } + final int bitsPerPixel = colors * bitsPerComponent; + final int bytesPerPixel = (bitsPerPixel + 7) / 8; + final int rowlength = actline.length; + switch (predictor) + { + case 2: + // PRED TIFF SUB + if (bitsPerComponent == 8) + { + // for 8 bits per component it is the same algorithm as PRED SUB of PNG format + for (int p = bytesPerPixel; p < rowlength; p++) + { + int sub = actline[p] & 0xff; + int left = actline[p - bytesPerPixel] & 0xff; + actline[p] = (byte) (sub + left); + } + break; + } + if (bitsPerComponent == 16) + { + for (int p = bytesPerPixel; p < rowlength; p += 2) + { + int sub = ((actline[p] & 0xff) << 8) + (actline[p + 1] & 0xff); + int left = (((actline[p - bytesPerPixel] & 0xff) << 8) + + (actline[p - bytesPerPixel + 1] & 0xff)); + actline[p] = (byte) (((sub + left) >> 8) & 0xff); + actline[p + 1] = (byte) ((sub + left) & 0xff); + } + break; + } + if (bitsPerComponent == 1 && colors == 1) + { + // bytesPerPixel cannot be used: + // "A row shall occupy a whole number of bytes, rounded up if necessary. + // Samples and their components shall be packed into bytes + // from high-order to low-order bits." + for (int p = 0; p < rowlength; p++) + { + for (int bit = 7; bit >= 0; --bit) + { + int sub = (actline[p] >> bit) & 1; + if (p == 0 && bit == 7) + { + continue; + } + int left; + if (bit == 7) + { + // use bit #0 from previous byte + left = actline[p - 1] & 1; + } + else + { + // use "previous" bit + left = (actline[p] >> (bit + 1)) & 1; + } + if (((sub + left) & 1) == 0) + { + // reset bit + actline[p] = (byte) (actline[p] & ~(1 << bit)); + } + else + { + // set bit + actline[p] = (byte) (actline[p] | (1 << bit)); + } + } + } + break; + } + // everything else, i.e. bpc 2 and 4, but has been tested for bpc 1 and 8 too + int elements = columns * colors; + for (int p = colors; p < elements; ++p) + { + int bytePosSub = p * bitsPerComponent / 8; + int bitPosSub = 8 - p * bitsPerComponent % 8 - bitsPerComponent; + int bytePosLeft = (p - colors) * bitsPerComponent / 8; + int bitPosLeft = 8 - (p - colors) * bitsPerComponent % 8 - bitsPerComponent; + + int sub = getBitSeq(actline[bytePosSub], bitPosSub, bitsPerComponent); + int left = getBitSeq(actline[bytePosLeft], bitPosLeft, bitsPerComponent); + actline[bytePosSub] = (byte) calcSetBitSeq(actline[bytePosSub], bitPosSub, bitsPerComponent, sub + left); + } + break; + case 10: + // PRED NONE + // do nothing + break; + case 11: + // PRED SUB + for (int p = bytesPerPixel; p < rowlength; p++) + { + int sub = actline[p]; + int left = actline[p - bytesPerPixel]; + actline[p] = (byte) (sub + left); + } + break; + case 12: + // PRED UP + for (int p = 0; p < rowlength; p++) + { + int up = actline[p] & 0xff; + int prior = lastline[p] & 0xff; + actline[p] = (byte) ((up + prior) & 0xff); + } + break; + case 13: + // PRED AVG + for (int p = 0; p < rowlength; p++) + { + int avg = actline[p] & 0xff; + int left = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0; + int up = lastline[p] & 0xff; + actline[p] = (byte) ((avg + (left + up) / 2) & 0xff); + } + break; + case 14: + // PRED PAETH + for (int p = 0; p < rowlength; p++) + { + int paeth = actline[p] & 0xff; + int a = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0;// left + int b = lastline[p] & 0xff;// upper + int c = p - bytesPerPixel >= 0 ? lastline[p - bytesPerPixel] & 0xff : 0;// upperleft + int value = a + b - c; + int absa = Math.abs(value - a); + int absb = Math.abs(value - b); + int absc = Math.abs(value - c); + + if (absa <= absb && absa <= absc) + { + actline[p] = (byte) ((paeth + a) & 0xff); + } + else if (absb <= absc) + { + actline[p] = (byte) ((paeth + b) & 0xff); + } + else + { + actline[p] = (byte) ((paeth + c) & 0xff); + } + } + break; + default: + break; + } + } static void decodePredictor(int predictor, int colors, int bitsPerComponent, int columns, InputStream in, OutputStream out) throws IOException @@ -43,9 +214,7 @@ else { // calculate sizes - final int bitsPerPixel = colors * bitsPerComponent; - final int bytesPerPixel = (bitsPerPixel + 7) / 8; - final int rowlength = (columns * bitsPerPixel + 7) / 8; + final int rowlength = calculateRowLength(colors, bitsPerComponent, columns); byte[] actline = new byte[rowlength]; byte[] lastline = new byte[rowlength]; @@ -74,155 +243,18 @@ offset += i; } - // do prediction as specified in PNG-Specification 1.2 - switch (linepredictor) - { - case 2: - // PRED TIFF SUB - if (bitsPerComponent == 8) - { - // for 8 bits per component it is the same algorithm as PRED SUB of PNG format - for (int p = bytesPerPixel; p < rowlength; p++) - { - int sub = actline[p] & 0xff; - int left = actline[p - bytesPerPixel] & 0xff; - actline[p] = (byte) (sub + left); - } - break; - } - if (bitsPerComponent == 16) - { - for (int p = bytesPerPixel; p < rowlength; p += 2) - { - int sub = ((actline[p] & 0xff) << 8) + (actline[p + 1] & 0xff); - int left = (((actline[p - bytesPerPixel] & 0xff) << 8) - + (actline[p - bytesPerPixel + 1] & 0xff)); - actline[p] = (byte) (((sub + left) >> 8) & 0xff); - actline[p + 1] = (byte) ((sub + left) & 0xff); - } - break; - } - if (bitsPerComponent == 1 && colors == 1) - { - // bytesPerPixel cannot be used: - // "A row shall occupy a whole number of bytes, rounded up if necessary. - // Samples and their components shall be packed into bytes - // from high-order to low-order bits." - for (int p = 0; p < rowlength; p++) - { - for (int bit = 7; bit >= 0; --bit) - { - int sub = (actline[p] >> bit) & 1; - if (p == 0 && bit == 7) - { - continue; - } - int left; - if (bit == 7) - { - // use bit #0 from previous byte - left = actline[p - 1] & 1; - } - else - { - // use "previous" bit - left = (actline[p] >> (bit + 1)) & 1; - } - if (((sub + left) & 1) == 0) - { - // reset bit - actline[p] = (byte) (actline[p] & ~(1 << bit)); - } - else - { - // set bit - actline[p] = (byte) (actline[p] | (1 << bit)); - } - } - } - break; - } - // everything else, i.e. bpc 2 and 4, but has been tested for bpc 1 and 8 too - int elements = columns * colors; - for (int p = colors; p < elements; ++p) - { - int bytePosSub = p * bitsPerComponent / 8; - int bitPosSub = 8 - p * bitsPerComponent % 8 - bitsPerComponent; - int bytePosLeft = (p - colors) * bitsPerComponent / 8; - int bitPosLeft = 8 - (p - colors) * bitsPerComponent % 8 - bitsPerComponent; - - int sub = getBitSeq(actline[bytePosSub], bitPosSub, bitsPerComponent); - int left = getBitSeq(actline[bytePosLeft], bitPosLeft, bitsPerComponent); - actline[bytePosSub] = (byte) calcSetBitSeq(actline[bytePosSub], bitPosSub, bitsPerComponent, sub + left); - } - break; - case 10: - // PRED NONE - // do nothing - break; - case 11: - // PRED SUB - for (int p = bytesPerPixel; p < rowlength; p++) - { - int sub = actline[p]; - int left = actline[p - bytesPerPixel]; - actline[p] = (byte) (sub + left); - } - break; - case 12: - // PRED UP - for (int p = 0; p < rowlength; p++) - { - int up = actline[p] & 0xff; - int prior = lastline[p] & 0xff; - actline[p] = (byte) ((up + prior) & 0xff); - } - break; - case 13: - // PRED AVG - for (int p = 0; p < rowlength; p++) - { - int avg = actline[p] & 0xff; - int left = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0; - int up = lastline[p] & 0xff; - actline[p] = (byte) ((avg + (left + up) / 2) & 0xff); - } - break; - case 14: - // PRED PAETH - for (int p = 0; p < rowlength; p++) - { - int paeth = actline[p] & 0xff; - int a = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0;// left - int b = lastline[p] & 0xff;// upper - int c = p - bytesPerPixel >= 0 ? lastline[p - bytesPerPixel] & 0xff : 0;// upperleft - int value = a + b - c; - int absa = Math.abs(value - a); - int absb = Math.abs(value - b); - int absc = Math.abs(value - c); - - if (absa <= absb && absa <= absc) - { - actline[p] = (byte) ((paeth + a) & 0xff); - } - else if (absb <= absc) - { - actline[p] = (byte) ((paeth + b) & 0xff); - } - else - { - actline[p] = (byte) ((paeth + c) & 0xff); - } - } - break; - default: - break; - } + decodePredictorRow(linepredictor, colors, bitsPerComponent, columns, actline, lastline); System.arraycopy(actline, 0, lastline, 0, rowlength); out.write(actline); } } } + + static int calculateRowLength(int colors, int bitsPerComponent, int columns) + { + final int bitsPerPixel = colors * bitsPerComponent; + return (columns * bitsPerPixel + 7) / 8; + } // get value from bit interval from a byte static int getBitSeq(int by, int startBit, int bitSize) @@ -240,4 +272,144 @@ return (by & mask) | (truncatedVal << startBit); } + /** + * Wraps and OutputStream in a predictor decoding stream as necessary. + * If no predictor is specified by the parameters, the original stream is returned as is. + * + * @param out The stream to which decoded data should be written + * @param decodeParams Decode parameters for the stream + * @return An OutputStream is returned, which will write decoded data + * into the given stream. If no predictor is specified, the original stream is returned. + */ + static OutputStream wrapPredictor(OutputStream out, COSDictionary decodeParams) + { + int predictor = decodeParams.getInt(COSName.PREDICTOR); + if (predictor > 1) + { + int colors = Math.min(decodeParams.getInt(COSName.COLORS, 1), 32); + int bitsPerPixel = decodeParams.getInt(COSName.BITS_PER_COMPONENT, 8); + int columns = decodeParams.getInt(COSName.COLUMNS, 1); + + return new PredictorOutputStream(out, predictor, colors, bitsPerPixel, columns); + } + else + { + return out; + } + } + + /** + * Output stream that implements predictor decoding. Data is buffered until a complete + * row is available, which is then decoded and written to the underlying stream. + * The previous row is retained for decoding the next row. + */ + private static final class PredictorOutputStream extends FilterOutputStream + { + // current predictor type + private int predictor; + // image decode parameters + private final int colors; + private final int bitsPerComponent; + private final int columns; + private final int rowLength; + // PNG predictor (predictor>=10) means every row has a (potentially different) + // predictor value + private final boolean predictorPerRow; + + // data buffers + private byte[] currentRow, lastRow; + // amount of data in the current row + private int currentRowData = 0; + // was the per-row predictor value read for the current row being processed + private boolean predictorRead = false; + + PredictorOutputStream(OutputStream out, int predictor, int colors, int bitsPerComponent, int columns) + { + super(out); + this.predictor = predictor; + this.colors = colors; + this.bitsPerComponent = bitsPerComponent; + this.columns = columns; + this.rowLength = calculateRowLength(colors, bitsPerComponent, columns); + this.predictorPerRow = predictor >= 10; + currentRow = new byte[rowLength]; + lastRow = new byte[rowLength]; + } + + @Override + public void write(byte[] bytes) throws IOException + { + write(bytes, 0, bytes.length); + } + + @Override + public void write(byte[] bytes, int off, int len) throws IOException + { + int currentOffset = off; + int maxOffset = currentOffset + len; + while (currentOffset < maxOffset) + { + if (predictorPerRow && currentRowData == 0 && !predictorRead) + { + // PNG predictor; each row starts with predictor type (0, 1, 2, 3, 4) + // read per line predictor, add 10 to tread value 0 as 10, 1 as 11, ... + predictor = bytes[currentOffset] + 10; + currentOffset++; + predictorRead = true; + } + else + { + int toRead = Math.min(rowLength - currentRowData, maxOffset - currentOffset); + System.arraycopy(bytes, currentOffset, currentRow, currentRowData, toRead); + currentRowData += toRead; + currentOffset += toRead; + + // current row is filled, decode it, write it to underlying stream, + // and reset the state. + if (currentRowData == currentRow.length) + { + decodeAndWriteRow(); + } + } + } + } + + private void decodeAndWriteRow() throws IOException + { + decodePredictorRow(predictor, colors, bitsPerComponent, columns, currentRow, lastRow); + out.write(currentRow); + flipRows(); + } + + /** + * Flips the row buffers (to avoid copying), and resets the current-row index + * and predictorRead flag + */ + private void flipRows() + { + byte[] temp = lastRow; + lastRow = currentRow; + currentRow = temp; + currentRowData = 0; + predictorRead = false; + } + + @Override + public void flush() throws IOException + { + // The last row is allowed to be incomplete, and should be completed with zeros. + if (currentRowData > 0) + { + Arrays.fill(currentRow, currentRowData, rowLength, (byte)0); + decodeAndWriteRow(); + } + super.flush(); + } + + @Override + public void write(int i) throws IOException + { + throw new UnsupportedOperationException("Not supported"); + } + } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/io/IOUtils.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/io/IOUtils.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/io/IOUtils.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/io/IOUtils.java 2018-11-28 17:18:38.000000000 +0000 @@ -15,7 +15,7 @@ * limitations under the License. */ -/* $Id: IOUtils.java 1666014 2015-03-11 21:08:39Z tilman $ */ +/* $Id: IOUtils.java 1834594 2018-06-28 10:29:51Z msahyoun $ */ package org.apache.pdfbox.io; @@ -25,6 +25,8 @@ import java.io.InputStream; import java.io.OutputStream; +import org.apache.commons.logging.Log; + /** * This class contains various I/O-related methods. */ @@ -115,4 +117,32 @@ // ignore } } + + /** + * Try to close an IO resource and log and return if there was an exception. + * + *

An exception is only returned if the IOException passed in is null. + * + * @param closeable to be closed + * @param logger the logger to be used so that logging appears under that log instance + * @param resourceName the name to appear in the log output + * @param initialException to be closed + * @return the IOException is there was any but only if initialException is null + */ + public static IOException closeAndLogException(Closeable closeable, Log logger, String resourceName, IOException initialException) + { + try + { + closeable.close(); + } + catch (IOException ioe) + { + logger.warn("Error closing " + resourceName, ioe); + if (initialException == null) + { + return ioe; + } + } + return initialException; + } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/multipdf/LayerUtility.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/multipdf/LayerUtility.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/multipdf/LayerUtility.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/multipdf/LayerUtility.java 2018-11-28 17:18:36.000000000 +0000 @@ -45,9 +45,9 @@ import org.apache.pdfbox.util.Matrix; /** - * This class allows to import pages as Form XObjects into a PDF file and use them to create - * layers (optional content groups). - * + * This class allows to import pages as Form XObjects into a document and use them to create layers + * (optional content groups). It should used only on loaded documents, not on generated documents + * because these can contain unfinished parts, e.g. font subsetting information. */ public class LayerUtility { diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/multipdf/Overlay.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/multipdf/Overlay.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/multipdf/Overlay.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/multipdf/Overlay.java 2018-11-28 17:18:36.000000000 +0000 @@ -17,6 +17,7 @@ package org.apache.pdfbox.multipdf; import java.awt.geom.AffineTransform; +import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -24,8 +25,10 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; @@ -45,7 +48,7 @@ * Based on code contributed by Balazs Jerk. * */ -public class Overlay +public class Overlay implements Closeable { /** * Possible location of the overlayed pages: foreground or background. @@ -61,7 +64,7 @@ private LayoutPage oddPageOverlayPage; private LayoutPage evenPageOverlayPage; - private final Map specificPageOverlay = new HashMap(); + private final Set openDocuments = new HashSet(); private Map specificPageOverlayPage = new HashMap(); private Position position = Position.BACKGROUND; @@ -87,21 +90,22 @@ private String evenPageOverlayFilename = null; private PDDocument evenPageOverlay = null; - private int numberOfOverlayPages = 0; private boolean useAllOverlayPages = false; /** - * This will add overlays to a documents. - * - * @param specificPageOverlayFile map of overlay files for specific pages - * - * @return the resulting pdf, which has to be saved and closed be the caller - * + * This will add overlays to a document. + * + * @param specificPageOverlayFile map of overlay files for specific pages. The page numbers are + * 1-based. + * + * @return The modified input PDF document, which has to be saved and closed by the caller. If + * the input document was passed by {@link #setInputPDF(PDDocument) setInputPDF(PDDocument)} + * then it is that object that is returned. + * * @throws IOException if something went wrong */ - public PDDocument overlay(Map specificPageOverlayFile) - throws IOException + public PDDocument overlay(Map specificPageOverlayFile) throws IOException { Map loadedDocuments = new HashMap(); Map layouts = new HashMap(); @@ -115,7 +119,7 @@ loadedDocuments.put(e.getValue(), doc); layouts.put(doc, getLayoutPage(doc)); } - specificPageOverlay.put(e.getKey(), doc); + openDocuments.add(doc); specificPageOverlayPage.put(e.getKey(), layouts.get(doc)); } processPages(inputPDFDocument); @@ -123,10 +127,38 @@ } /** - * Close all input pdfs which were used for the overlay. - * + * This will add overlays documents to a document. + * + * @param specificPageOverlayDocuments map of overlay documents for specific pages. The page + * numbers are 1-based. + * + * @return The modified input PDF document, which has to be saved and closed by the caller. If + * the input document was passed by {@link #setInputPDF(PDDocument) setInputPDF(PDDocument)} + * then it is that object that is returned. + * * @throws IOException if something went wrong */ + public PDDocument overlayDocuments(Map specificPageOverlayDocuments) throws IOException + { + loadPDFs(); + for (Map.Entry e : specificPageOverlayDocuments.entrySet()) + { + PDDocument doc = e.getValue(); + if (doc != null) + { + specificPageOverlayPage.put(e.getKey(), getLayoutPage(doc)); + } + } + processPages(inputPDFDocument); + return inputPDFDocument; + } + + /** + * Close all input documents which were used for the overlay and opened by this class. + * + * @throws IOException if something went wrong + */ + @Override public void close() throws IOException { if (defaultOverlay != null) @@ -153,15 +185,12 @@ { evenPageOverlay.close(); } - if (specificPageOverlay != null) + for (PDDocument doc : openDocuments) { - for (Map.Entry e : specificPageOverlay.entrySet()) - { - e.getValue().close(); - } - specificPageOverlay.clear(); - specificPageOverlayPage.clear(); + doc.close(); } + openDocuments.clear(); + specificPageOverlayPage.clear(); } private void loadPDFs() throws IOException @@ -260,7 +289,7 @@ { resources = new PDResources(); } - return new LayoutPage(page.getMediaBox(), createContentStream(contents), + return new LayoutPage(page.getMediaBox(), createCombinedContentStream(contents), resources.getCOSObject()); } @@ -277,13 +306,13 @@ { resources = new PDResources(); } - layoutPages.put(i,new LayoutPage(page.getMediaBox(), createContentStream(contents), + layoutPages.put(i, new LayoutPage(page.getMediaBox(), createCombinedContentStream(contents), resources.getCOSObject())); } return layoutPages; } - private COSStream createContentStream(COSBase contents) throws IOException + private COSStream createCombinedContentStream(COSBase contents) throws IOException { List contentStreams = createContentStreamList(contents); // concatenate streams @@ -300,10 +329,15 @@ return concatStream; } + // get the content streams as a list private List createContentStreamList(COSBase contents) throws IOException { List contentStreams = new ArrayList(); - if (contents instanceof COSStream) + if (contents == null) + { + return contentStreams; + } + else if (contents instanceof COSStream) { contentStreams.add((COSStream) contents); } @@ -320,45 +354,56 @@ } else { - throw new IOException("Contents are unknown type:" + contents.getClass().getName()); + throw new IOException("Unknown content type: " + contents.getClass().getName()); } return contentStreams; } private void processPages(PDDocument document) throws IOException { - int pageCount = 0; + int pageCounter = 0; for (PDPage page : document.getPages()) { + pageCounter++; COSDictionary pageDictionary = page.getCOSObject(); - COSBase contents = pageDictionary.getDictionaryObject(COSName.CONTENTS); - COSArray contentArray = new COSArray(); + COSBase originalContent = pageDictionary.getDictionaryObject(COSName.CONTENTS); + COSArray newContentArray = new COSArray(); + LayoutPage layoutPage = getLayoutPage(pageCounter, document.getNumberOfPages()); + if (layoutPage == null) + { + continue; + } switch (position) { - case FOREGROUND: - // save state - contentArray.add(createStream("q\n")); - addOriginalContent(contents, contentArray); - // restore state - contentArray.add(createStream("Q\n")); - // overlay content - overlayPage(contentArray, page, pageCount + 1, document.getNumberOfPages()); - break; - case BACKGROUND: - // overlay content - overlayPage(contentArray, page, pageCount + 1, document.getNumberOfPages()); - addOriginalContent(contents, contentArray); - break; - default: - throw new IOException("Unknown type of position:" + position); + case FOREGROUND: + // save state + newContentArray.add(createStream("q\n")); + addOriginalContent(originalContent, newContentArray); + // restore state + newContentArray.add(createStream("Q\n")); + // overlay content last + overlayPage(page, layoutPage, newContentArray); + break; + case BACKGROUND: + // overlay content first + overlayPage(page, layoutPage, newContentArray); + + addOriginalContent(originalContent, newContentArray); + break; + default: + throw new IOException("Unknown type of position:" + position); } - pageDictionary.setItem(COSName.CONTENTS, contentArray); - pageCount++; + pageDictionary.setItem(COSName.CONTENTS, newContentArray); } } private void addOriginalContent(COSBase contents, COSArray contentArray) throws IOException { + if (contents == null) + { + return; + } + if (contents instanceof COSStream) { contentArray.add(contents); @@ -369,13 +414,26 @@ } else { - throw new IOException("Unknown content type:" + contents.getClass().getName()); + throw new IOException("Unknown content type: " + contents.getClass().getName()); } } - private void overlayPage(COSArray array, PDPage page, int pageNumber, int numberOfPages) + private void overlayPage(PDPage page, LayoutPage layoutPage, COSArray array) throws IOException { + PDResources resources = page.getResources(); + if (resources == null) + { + resources = new PDResources(); + page.setResources(resources); + } + COSName xObjectId = createOverlayXObject(page, layoutPage, + layoutPage.overlayContentStream); + array.add(createOverlayStream(page, layoutPage, xObjectId)); + } + + private LayoutPage getLayoutPage(int pageNumber, int numberOfPages) + { LayoutPage layoutPage = null; if (!useAllOverlayPages && specificPageOverlayPage.containsKey(pageNumber)) { @@ -406,18 +464,7 @@ int usePageNum = (pageNumber -1 ) % numberOfOverlayPages; layoutPage = specificPageOverlayPage.get(usePageNum); } - if (layoutPage != null) - { - PDResources resources = page.getResources(); - if (resources == null) - { - resources = new PDResources(); - page.setResources(resources); - } - COSName xObjectId = createOverlayXObject(page, layoutPage, - layoutPage.overlayContentStream); - array.add(createOverlayStream(page, layoutPage, xObjectId)); - } + return layoutPage; } private COSName createOverlayXObject(PDPage page, LayoutPage layoutPage, COSStream contentStream) @@ -435,20 +482,41 @@ throws IOException { // create a new content stream that executes the XObject content - PDRectangle pageMediaBox = page.getMediaBox(); - float hShift = (pageMediaBox.getWidth() - layoutPage.overlayMediaBox.getWidth()) / 2.0f; - float vShift = (pageMediaBox.getHeight() - layoutPage.overlayMediaBox.getHeight()) / 2.0f; StringBuilder overlayStream = new StringBuilder(); - overlayStream.append("q\nq 1 0 0 1 "); - overlayStream.append(float2String(hShift)); - overlayStream.append(" "); - overlayStream.append(float2String(vShift) ); - overlayStream.append(" cm /"); + overlayStream.append("q\nq\n"); + AffineTransform at = calculateAffineTransform(page, layoutPage.overlayMediaBox); + double[] flatmatrix = new double[6]; + at.getMatrix(flatmatrix); + for (double v : flatmatrix) + { + overlayStream.append(float2String((float) v)); + overlayStream.append(" "); + } + overlayStream.append(" cm\n/"); overlayStream.append(xObjectId.getName()); overlayStream.append(" Do Q\nQ\n"); return createStream(overlayStream.toString()); } + /** + * Calculate the transform to be used when positioning the overlay. The default implementation + * centers on the destination. Override this method to do your own, e.g. move to a corner, or + * rotate. + * + * @param page The page that will get the overlay. + * @param overlayMediaBox The overlay media box. + * @return The affine transform to be used. + */ + protected AffineTransform calculateAffineTransform(PDPage page, PDRectangle overlayMediaBox) + { + AffineTransform at = new AffineTransform(); + PDRectangle pageMediaBox = page.getMediaBox(); + float hShift = (pageMediaBox.getWidth() - overlayMediaBox.getWidth()) / 2.0f; + float vShift = (pageMediaBox.getHeight() - overlayMediaBox.getHeight()) / 2.0f; + at.translate(hShift, vShift); + return at; + } + private String float2String(float floatValue) { // use a BigDecimal as intermediate state to avoid @@ -488,8 +556,10 @@ /** * Sets the file to be overlayed. - * - * @param inputFile the file to be overlayed + * + * @param inputFile the file to be overlayed. The {@link PDDocument} object gathered from + * opening this file will be returned by + * {@link #overlay(java.util.Map) overlay(Map<Integer, String>)}. */ public void setInputFile(String inputFile) { @@ -498,8 +568,9 @@ /** * Sets the PDF to be overlayed. - * - * @param inputPDF the PDF to be overlayed + * + * @param inputPDF the PDF to be overlayed. This will be the object that is returned by + * {@link #overlay(java.util.Map) overlay(Map<Integer, String>)}. */ public void setInputPDF(PDDocument inputPDF) { diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/multipdf/PDFCloneUtility.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/multipdf/PDFCloneUtility.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/multipdf/PDFCloneUtility.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/multipdf/PDFCloneUtility.java 2018-11-28 17:18:36.000000000 +0000 @@ -34,11 +34,13 @@ /** * Utility class used to clone PDF objects. It keeps track of objects it has already cloned. - * + * Although this class is public, it is for PDFBox internal use and should not be used outside, + * except by very experienced users. The "public" modifier will be removed in 3.0. The class should + * not be used on documents that are being generated because these can contain unfinished parts, + * e.g. font subsetting information. */ public class PDFCloneUtility { - private final PDDocument destination; private final Map clonedVersion = new HashMap(); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/multipdf/PDFMergerUtility.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/multipdf/PDFMergerUtility.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/multipdf/PDFMergerUtility.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/multipdf/PDFMergerUtility.java 2018-11-28 17:18:36.000000000 +0000 @@ -31,13 +31,18 @@ import java.util.Map; import java.util.Set; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSInteger; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSNumber; +import org.apache.pdfbox.cos.COSObject; import org.apache.pdfbox.cos.COSStream; +import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; @@ -71,21 +76,47 @@ */ public class PDFMergerUtility { - private final List sources; - private final List fileInputStreams; + /** + * Log instance. + */ + private static final Log LOG = LogFactory.getLog(PDFMergerUtility.class); + + private final List sources; private String destinationFileName; private OutputStream destinationStream; private boolean ignoreAcroFormErrors = false; private PDDocumentInformation destinationDocumentInformation = null; private PDMetadata destinationMetadata = null; + private DocumentMergeMode documentMergeMode = DocumentMergeMode.PDFBOX_LEGACY_MODE; + + /** + * The mode to use when merging documents: + * + *
    + *
  • {@link DocumentMergeMode#OPTIMIZE_RESOURCES_MODE} Optimizes resource handling such as + * closing documents early. Not all document elements are merged compared to + * the PDFBOX_LEGACY_MODE. Currently supported are: + *
      + *
    • Page content and resources + *
    + *
  • {@link DocumentMergeMode#PDFBOX_LEGACY_MODE} Keeps all files open until the + * merge has been completed. This is currently necessary to merge documents + * containing a Structure Tree.
    This is the standard mode for PDFBox 2.0. + *
+ */ + public enum DocumentMergeMode + { + OPTIMIZE_RESOURCES_MODE, + PDFBOX_LEGACY_MODE + } + /** * Instantiate a new PDFMergerUtility. */ public PDFMergerUtility() { - sources = new ArrayList(); - fileInputStreams = new ArrayList(); + sources = new ArrayList(); } /** @@ -193,9 +224,7 @@ */ public void addSource(File source) throws FileNotFoundException { - FileInputStream stream = new FileInputStream(source); - sources.add(stream); - fileInputStreams.add(stream); + sources.add(source); } /** @@ -242,11 +271,100 @@ */ public void mergeDocuments(MemoryUsageSetting memUsageSetting) throws IOException { + if (documentMergeMode == DocumentMergeMode.PDFBOX_LEGACY_MODE) + { + legacyMergeDocuments(memUsageSetting); + } + else if (documentMergeMode == DocumentMergeMode.OPTIMIZE_RESOURCES_MODE) + { + optimizedMergeDocuments(memUsageSetting, sources); + } + } + + private void optimizedMergeDocuments(MemoryUsageSetting memUsageSetting, + List sourceDocuments) throws IOException + { + PDDocument destination = null; + try + { + destination = new PDDocument(memUsageSetting); + PDFCloneUtility cloner = new PDFCloneUtility(destination); + + for (Object sourceObject : sources) + { + PDDocument sourceDoc = null; + try + { + if (sourceObject instanceof File) + { + sourceDoc = PDDocument.load((File) sourceObject, memUsageSetting); + } + else + { + sourceDoc = PDDocument.load((InputStream) sourceObject, memUsageSetting); + } + + for (PDPage page : sourceDoc.getPages()) + { + PDPage newPage = new PDPage((COSDictionary) cloner.cloneForNewDocument(page.getCOSObject())); + newPage.setCropBox(page.getCropBox()); + newPage.setMediaBox(page.getMediaBox()); + newPage.setRotation(page.getRotation()); + PDResources resources = page.getResources(); + if (resources != null) + { + // this is smart enough to just create references for resources that are used on multiple pages + newPage.setResources(new PDResources((COSDictionary) cloner.cloneForNewDocument(resources))); + } + else + { + newPage.setResources(new PDResources()); + } + destination.addPage(newPage); + } + } + finally + { + IOUtils.closeQuietly(sourceDoc); + } + } + + if (destinationStream == null) + { + destination.save(destinationFileName); + } + else + { + destination.save(destinationStream); + } + } + finally + { + IOUtils.closeQuietly(destination); + } + } + + /** + * Merge the list of source documents, saving the result in the destination + * file. + * + * @param memUsageSetting defines how memory is used for buffering PDF streams; + * in case of null unrestricted main memory is used + * + * @throws IOException If there is an error saving the document. + */ + private void legacyMergeDocuments(MemoryUsageSetting memUsageSetting) throws IOException + { PDDocument destination = null; - InputStream sourceFile; - PDDocument source; if (sources != null && sources.size() > 0) { + // Make sure that: + // - first Exception is kept + // - destination is closed + // - all PDDocuments are closed + // - all FileInputStreams are closed + // - there's a way to see which errors occured + List tobeclosed = new ArrayList(); try @@ -256,9 +374,18 @@ MemoryUsageSetting.setupMainMemoryOnly(); destination = new PDDocument(partitionedMemSetting); - for (InputStream sourceInputStream : sources) + for (Object sourceObject : sources) { - PDDocument sourceDoc = PDDocument.load(sourceInputStream, partitionedMemSetting); + PDDocument sourceDoc = null; + if (sourceObject instanceof File) + { + sourceDoc = PDDocument.load((File) sourceObject, partitionedMemSetting); + } + else + { + sourceDoc = PDDocument.load((InputStream) sourceObject, + partitionedMemSetting); + } tobeclosed.add(sourceDoc); appendDocument(destination, sourceDoc); } @@ -286,15 +413,12 @@ { if (destination != null) { - destination.close(); + IOUtils.closeAndLogException(destination, LOG, "PDDocument", null); } + for (PDDocument doc : tobeclosed) { - doc.close(); - } - for (FileInputStream stream : fileInputStreams) - { - stream.close(); + IOUtils.closeAndLogException(doc, LOG, "PDDocument", null); } } } @@ -345,7 +469,16 @@ if (destCatalog.getOpenAction() == null) { // PDFBOX-3972: get local dest page index, it must be reassigned after the page cloning - PDDestinationOrAction openAction = srcCatalog.getOpenAction(); + PDDestinationOrAction openAction = null; + try + { + openAction = srcCatalog.getOpenAction(); + } + catch (IOException ex) + { + // PDFBOX-4223 + LOG.error("Invalid OpenAction ignored", ex); + } PDDestination openActionDestination = null; if (openAction instanceof PDActionGoTo) { @@ -366,7 +499,7 @@ } } - destCatalog.setOpenAction(srcCatalog.getOpenAction()); + destCatalog.setOpenAction(openAction); } PDFCloneUtility cloner = new PDFCloneUtility(destination); @@ -499,9 +632,21 @@ COSArray srcNums = (COSArray) srcLabels.getDictionaryObject(COSName.NUMS); if (srcNums != null) { + int startSize = destNums.size(); for (int i = 0; i < srcNums.size(); i += 2) { - COSNumber labelIndex = (COSNumber) srcNums.getObject(i); + COSBase base = srcNums.getObject(i); + if (!(base instanceof COSNumber)) + { + LOG.error("page labels ignored, index " + i + " should be a number, but is " + base); + // remove what we added + while (destNums.size() > startSize) + { + destNums.remove(startSize); + } + break; + } + COSNumber labelIndex = (COSNumber) base; long labelIndexValue = labelIndex.intValue(); destNums.add(COSInteger.get(labelIndexValue + destPageCount)); destNums.add(cloner.cloneForNewDocument(srcNums.getObject(i + 1))); @@ -513,10 +658,18 @@ COSStream srcMetadata = (COSStream) srcCatalog.getCOSObject().getDictionaryObject(COSName.METADATA); if (destMetadata == null && srcMetadata != null) { - PDStream newStream = new PDStream(destination, srcMetadata.createInputStream(), (COSName) null); - mergeInto(srcMetadata, newStream.getCOSObject(), - new HashSet(Arrays.asList(COSName.FILTER, COSName.LENGTH))); - destCatalog.getCOSObject().setItem(COSName.METADATA, newStream); + try + { + PDStream newStream = new PDStream(destination, srcMetadata.createInputStream(), (COSName) null); + mergeInto(srcMetadata, newStream.getCOSObject(), + new HashSet(Arrays.asList(COSName.FILTER, COSName.LENGTH))); + destCatalog.getCOSObject().setItem(COSName.METADATA, newStream); + } + catch (IOException ex) + { + // PDFBOX-4227 cleartext XMP stream with /Flate + LOG.error("Metadata skipped because it could not be read", ex); + } } COSDictionary destOCP = (COSDictionary) destCatalog.getCOSObject().getDictionaryObject(COSName.OCPROPERTIES); @@ -599,6 +752,8 @@ } if (mergeStructTree) { + // add the value of the destination ParentTreeNextKey to every source element + // StructParent(s) value so that these don't overlap with the existing values updateStructParentEntries(newPage, destParentTreeNextKey); objMapping.put(page.getCOSObject(), newPage.getCOSObject()); List oldAnnots = page.getAnnotations(); @@ -631,7 +786,7 @@ } if (mergeStructTree) { - updatePageReferences(srcNumbersArray, objMapping); + updatePageReferences(cloner, srcNumbersArray, objMapping); for (int i = 0; i < srcNumbersArray.size() / 2; i++) { destNumbersArray.add(COSInteger.get(destParentTreeNextKey + i)); @@ -781,7 +936,9 @@ * @param parentTreeEntry * @param objMapping mapping between old and new references */ - private void updatePageReferences(COSDictionary parentTreeEntry, Map objMapping) + private void updatePageReferences(PDFCloneUtility cloner, + COSDictionary parentTreeEntry, Map objMapping) + throws IOException { COSBase page = parentTreeEntry.getDictionaryObject(COSName.PG); if (page instanceof COSDictionary && objMapping.containsKey(page)) @@ -789,33 +946,56 @@ parentTreeEntry.setItem(COSName.PG, objMapping.get(page)); } COSBase obj = parentTreeEntry.getDictionaryObject(COSName.OBJ); - if (obj instanceof COSDictionary && objMapping.containsKey(obj)) + if (obj instanceof COSDictionary) { - parentTreeEntry.setItem(COSName.OBJ, objMapping.get(obj)); + if (objMapping.containsKey(obj)) + { + parentTreeEntry.setItem(COSName.OBJ, objMapping.get(obj)); + } + else + { + // PDFBOX-3999: clone objects that are not in mapping to make sure that + // these don't remain attached to the source document + COSBase item = parentTreeEntry.getItem(COSName.OBJ); + if (item instanceof COSObject) + { + LOG.debug("clone potential orphan object in structure tree: " + item + + ", type: " + ((COSDictionary) obj).getNameAsString(COSName.TYPE)); + } + else + { + // don't display because of stack overflow + LOG.debug("clone potential orphan object in structure tree, type: " + + ((COSDictionary) obj).getNameAsString(COSName.TYPE)); + } + parentTreeEntry.setItem(COSName.OBJ, cloner.cloneForNewDocument(obj)); + } } COSBase kSubEntry = parentTreeEntry.getDictionaryObject(COSName.K); if (kSubEntry instanceof COSArray) { - updatePageReferences((COSArray) kSubEntry, objMapping); + updatePageReferences(cloner, (COSArray) kSubEntry, objMapping); } else if (kSubEntry instanceof COSDictionary) { - updatePageReferences((COSDictionary) kSubEntry, objMapping); + updatePageReferences(cloner, (COSDictionary) kSubEntry, objMapping); } } - private void updatePageReferences(COSArray parentTreeEntry, Map objMapping) + private void updatePageReferences(PDFCloneUtility cloner, + COSArray parentTreeEntry, Map objMapping) + throws IOException { for (int i = 0; i < parentTreeEntry.size(); i++) { COSBase subEntry = parentTreeEntry.getObject(i); if (subEntry instanceof COSArray) { - updatePageReferences((COSArray) subEntry, objMapping); + updatePageReferences(cloner, (COSArray) subEntry, objMapping); } else if (subEntry instanceof COSDictionary) { - updatePageReferences((COSDictionary) subEntry, objMapping); + updatePageReferences(cloner, (COSDictionary) subEntry, objMapping); } } } @@ -873,9 +1053,10 @@ } /** - * This will add all of the dictionarys keys/values to this dictionary, but only if they are not - * in an exclusion list and if they don't already exist. If a key already exists in this - * dictionary then nothing is changed. + * This will add all of the dictionaries keys/values to this dictionary, but + * only if they are not in an exclusion list and if they don't already + * exist. If a key already exists in this dictionary then nothing is + * changed. * * @param src The source dictionary to get the keys/values from. * @param dst The destination dictionary to merge the keys/values into. diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/BaseParser.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/BaseParser.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/BaseParser.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/BaseParser.java 2018-11-28 17:18:38.000000000 +0000 @@ -621,6 +621,7 @@ */ protected COSArray parseCOSArray() throws IOException { + long startPosition = seqSource.getPosition(); readExpectedChar('['); COSArray po = new COSArray(); COSBase pbo; @@ -632,10 +633,10 @@ if( pbo instanceof COSObject ) { // We have to check if the expected values are there or not PDFBOX-385 - if (po.get(po.size()-1) instanceof COSInteger) + if (po.size() > 0 && po.get(po.size() - 1) instanceof COSInteger) { COSInteger genNumber = (COSInteger)po.remove( po.size() -1 ); - if (po.get(po.size()-1) instanceof COSInteger) + if (po.size() > 0 && po.get(po.size() - 1) instanceof COSInteger) { COSInteger number = (COSInteger)po.remove( po.size() -1 ); COSObjectKey key = new COSObjectKey(number.longValue(), genNumber.intValue()); @@ -659,7 +660,8 @@ else { //it could be a bad object in the array which is just skipped - LOG.warn("Corrupt object reference at offset " + seqSource.getPosition()); + LOG.warn("Corrupt object reference at offset " + + seqSource.getPosition() + ", start offset: " + startPosition); // This could also be an "endobj" or "endstream" which means we can assume that // the array has ended. diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/COSParser.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/COSParser.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/COSParser.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/COSParser.java 2018-11-28 17:18:38.000000000 +0000 @@ -17,7 +17,9 @@ package org.apache.pdfbox.pdfparser; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.security.KeyStore; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -45,10 +47,16 @@ import org.apache.pdfbox.cos.COSObject; import org.apache.pdfbox.cos.COSObjectKey; import org.apache.pdfbox.cos.COSStream; +import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.io.RandomAccessRead; import org.apache.pdfbox.pdfparser.XrefTrailerResolver.XRefType; +import org.apache.pdfbox.pdmodel.encryption.AccessPermission; +import org.apache.pdfbox.pdmodel.encryption.DecryptionMaterial; +import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException; +import org.apache.pdfbox.pdmodel.encryption.PDEncryption; +import org.apache.pdfbox.pdmodel.encryption.PublicKeyDecryptionMaterial; import org.apache.pdfbox.pdmodel.encryption.SecurityHandler; - +import org.apache.pdfbox.pdmodel.encryption.StandardDecryptionMaterial; import static org.apache.pdfbox.util.Charsets.ISO_8859_1; @@ -85,7 +93,12 @@ private final byte[] strmBuf = new byte[ STRMBUFLEN ]; protected final RandomAccessRead source; - + + private AccessPermission accessPermission; + private InputStream keyStoreInputStream = null; + private String password = ""; + private String keyAlias = null; + /** * Only parse the PDF file minimally allowing access to basic information. */ @@ -144,6 +157,7 @@ private Long lastEOFMarker = null; private List bfSearchXRefTablesOffsets = null; private List bfSearchXRefStreamsOffsets = null; + private PDEncryption encryption = null; /** * The security handler. @@ -179,6 +193,25 @@ } /** + * Constructor for encrypted pdfs. + * + * @param source input representing the pdf. + * @param password password to be used for decryption. + * @param keyStore key store to be used for decryption when using public key security + * @param keyAlias alias to be used for decryption when using public key security + * + */ + public COSParser(RandomAccessRead source, String password, InputStream keyStore, + String keyAlias) + { + super(new RandomAccessSource(source)); + this.source = source; + this.password = password; + this.keyAlias = keyAlias; + keyStoreInputStream = keyStore; + } + + /** * Sets how many trailing bytes of PDF file are searched for EOF marker and 'startxref' marker. If not set we use * default value {@link #DEFAULT_TRAIL_BYTECOUNT}. * @@ -245,6 +278,15 @@ { trailer = rebuildTrailer(); } + else + { + // prepare decryption if necessary + prepareDecryption(); + if (bfSearchCOSObjectKeyOffsets != null && !bfSearchCOSObjectKeyOffsets.isEmpty()) + { + bfSearchForObjStreams(); + } + } return trailer; } @@ -281,10 +323,9 @@ { // xref table and trailer // use existing parser to parse xref table - parseXrefTable(prev); - if (!parseTrailer()) + if (!parseXrefTable(prev) || !parseTrailer()) { - throw new IOException("Expected trailer object at position: " + throw new IOException("Expected trailer object at offset " + source.getPosition()); } COSDictionary trailer = xrefTrailerResolver.getCurrentTrailer(); @@ -386,7 +427,12 @@ private long parseXrefObjStream(long objByteOffset, boolean isStandalone) throws IOException { // ---- parse indirect object head - readObjectNumber(); + long objectNumber = readObjectNumber(); + + // remember the highest XRef object number to avoid it being reused in incremental saving + long currentHighestXRefObjectNumber = document.getHighestXRefObjectNumber(); + document.setHighestXRefObjectNumber(Math.max(currentHighestXRefObjectNumber, objectNumber)); + readGenerationNumber(); readExpectedString(OBJ_MARKER, true); @@ -553,7 +599,9 @@ /** * Adds newObject to toBeParsedList if it is not an COSObject or we didn't - * add this COSObject already (checked via addedObjects). + * add this COSObject already (checked via addedObjects). Simple objects are + * not added because nothing is done with them when toBeParsedList is + * processed. */ private void addNewToList(final Queue toBeParsedList, final COSBase newObject, final Set addedObjects) @@ -565,8 +613,12 @@ { return; } + toBeParsedList.add(newObject); + } + else if (newObject instanceof COSDictionary || newObject instanceof COSArray) + { + toBeParsedList.add(newObject); } - toBeParsedList.add(newObject); } /** @@ -1575,7 +1627,6 @@ bfSearchCOSObjectKeyOffsets.put(new COSObjectKey(lastObjectId, lastGenID), lastObjOffset); } - bfSearchForObjStreams(); // reestablish origin position source.seek(originOffset); } @@ -1685,29 +1736,23 @@ skipSpaces(); COSDictionary trailerDict = parseCOSDictionary(); StringBuilder trailerKeys = new StringBuilder(); - if (trailerDict.containsKey(COSName.ROOT)) + COSObject rootObj = trailerDict.getCOSObject(COSName.ROOT); + if (rootObj != null) { - COSBase rootObj = trailerDict.getItem(COSName.ROOT); - if (rootObj instanceof COSObject) - { - long objNumber = ((COSObject) rootObj).getObjectNumber(); - int genNumber = ((COSObject) rootObj).getGenerationNumber(); - trailerKeys.append(objNumber).append(" "); - trailerKeys.append(genNumber).append(" "); - rootFound = true; - } - } - if (trailerDict.containsKey(COSName.INFO)) - { - COSBase infoObj = trailerDict.getItem(COSName.INFO); - if (infoObj instanceof COSObject) - { - long objNumber = ((COSObject) infoObj).getObjectNumber(); - int genNumber = ((COSObject) infoObj).getGenerationNumber(); - trailerKeys.append(objNumber).append(" "); - trailerKeys.append(genNumber).append(" "); - infoFound = true; - } + long objNumber = rootObj.getObjectNumber(); + int genNumber = rootObj.getGenerationNumber(); + trailerKeys.append(objNumber).append(" "); + trailerKeys.append(genNumber).append(" "); + rootFound = true; + } + COSObject infoObj = trailerDict.getCOSObject(COSName.INFO); + if (infoObj != null) + { + long objNumber = infoObj.getObjectNumber(); + int genNumber = infoObj.getGenerationNumber(); + trailerKeys.append(objNumber).append(" "); + trailerKeys.append(genNumber).append(" "); + infoFound = true; } if (rootFound && infoFound) { @@ -1936,7 +1981,7 @@ { source.seek(offset); long stmObjNumber = readObjectNumber(); - readGenerationNumber(); + int stmGenNumber = readGenerationNumber(); readExpectedString(OBJ_MARKER, true); int nrOfObjects = 0; byte[] numbersBytes = null; @@ -1953,6 +1998,10 @@ continue; } stream = parseCOSStream(dict); + if (securityHandler != null) + { + securityHandler.decryptStream(stream, stmObjNumber, stmGenNumber); + } is = stream.createInputStream(); numbersBytes = new byte[offsetFirstStream]; is.read(numbersBytes); @@ -1976,7 +2025,7 @@ } int start = 0; // skip spaces - while (numbersBytes[start] == 32) + while (start < numbersBytes.length && numbersBytes[start] == 32) { start++; } @@ -1990,14 +2039,23 @@ "Skipped corrupt stream: (" + stmObjNumber + " 0 at offset " + offset); continue; } + Map xrefOffset = xrefTrailerResolver.getXrefTable(); for (int i = 0; i < nrOfObjects; i++) { - long objNumber = Long.parseLong(numbers[i * 2]); - COSObjectKey objKey = new COSObjectKey(objNumber, 0); - Long existingOffset = bfSearchCOSObjectKeyOffsets.get(objKey); - if (existingOffset == null || offset > existingOffset) + try { - bfSearchCOSObjectKeyOffsets.put(objKey, -stmObjNumber); + long objNumber = Long.parseLong(numbers[i * 2]); + COSObjectKey objKey = new COSObjectKey(objNumber, 0); + Long existingOffset = bfSearchCOSObjectKeyOffsets.get(objKey); + if (existingOffset == null || offset > existingOffset) + { + bfSearchCOSObjectKeyOffsets.put(objKey, -stmObjNumber); + xrefOffset.put(objKey, -stmObjNumber); + } + } + catch (NumberFormatException exception) + { + LOG.debug("Skipped corrupt object key in stream: " + stmObjNumber); } } } @@ -2145,36 +2203,57 @@ xrefTrailerResolver.setStartxref(0); trailer = xrefTrailerResolver.getTrailer(); getDocument().setTrailer(trailer); + boolean searchForObjStreamsDone = false; if (!bfSearchForTrailer(trailer)) { // search for the different parts of the trailer dictionary - for (Entry entry : bfSearchCOSObjectKeyOffsets.entrySet()) + if (!searchForTrailerItems(trailer)) { - COSDictionary dictionary = retrieveCOSDictionary(entry.getKey(), - entry.getValue()); - if (dictionary == null) - { - continue; - } - // document catalog - if (isCatalog(dictionary)) - { - trailer.setItem(COSName.ROOT, document.getObjectFromPool(entry.getKey())); - } - // info dictionary - else if (isInfo(dictionary)) - { - trailer.setItem(COSName.INFO, document.getObjectFromPool(entry.getKey())); - } - // encryption dictionary, if existing, is lost - // We can't run "Algorithm 2" from PDF specification because of missing ID + // root entry wasn't found, maybe it is part of an object stream + bfSearchForObjStreams(); + searchForObjStreamsDone = true; + // search again for the root entry + searchForTrailerItems(trailer); } } + // prepare decryption if necessary + prepareDecryption(); + if (!searchForObjStreamsDone) + { + bfSearchForObjStreams(); + } } trailerWasRebuild = true; return trailer; } + private boolean searchForTrailerItems(COSDictionary trailer) throws IOException + { + boolean rootFound = false; + for (Entry entry : bfSearchCOSObjectKeyOffsets.entrySet()) + { + COSDictionary dictionary = retrieveCOSDictionary(entry.getKey(), entry.getValue()); + if (dictionary == null) + { + continue; + } + // document catalog + if (isCatalog(dictionary)) + { + trailer.setItem(COSName.ROOT, document.getObjectFromPool(entry.getKey())); + rootFound = true; + } + // info dictionary + else if (isInfo(dictionary)) + { + trailer.setItem(COSName.INFO, document.getObjectFromPool(entry.getKey())); + } + // encryption dictionary, if existing, is lost + // We can't run "Algorithm 2" from PDF specification because of missing ID + } + return rootFound; + } + private COSDictionary retrieveCOSDictionary(COSObject object) throws IOException { COSObjectKey key = new COSObjectKey((COSObject) object); @@ -2239,12 +2318,12 @@ COSBase pages = root.getDictionaryObject(COSName.PAGES); if (pages instanceof COSDictionary) { - checkPagesDictionary((COSDictionary) pages); + checkPagesDictionary((COSDictionary) pages, new HashSet()); } } } - private int checkPagesDictionary(COSDictionary pagesDict) + private int checkPagesDictionary(COSDictionary pagesDict, Set set) { // check for kids COSBase kids = pagesDict.getDictionaryObject(COSName.KIDS); @@ -2255,10 +2334,15 @@ List kidsList = kidsArray.toList(); for (COSBase kid : kidsList) { + if (!(kid instanceof COSObject) || set.contains((COSObject) kid)) + { + kidsArray.remove(kid); + continue; + } COSObject kidObject = (COSObject) kid; COSBase kidBaseobject = kidObject.getObject(); // object wasn't dereferenced -> remove it - if (kidBaseobject.equals(COSNull.NULL)) + if (kidBaseobject == null || kidBaseobject.equals(COSNull.NULL)) { LOG.warn("Removed null object " + kid + " from pages dictionary"); kidsArray.remove(kid); @@ -2270,7 +2354,8 @@ if (COSName.PAGES.equals(type)) { // process nested pages dictionaries - numberOfPages += checkPagesDictionary(kidDictionary); + set.add(kidObject); + numberOfPages += checkPagesDictionary(kidDictionary, set); } else if (COSName.PAGE.equals(type)) { @@ -2412,7 +2497,7 @@ if (source.getPosition() == trailerOffset) { // warn only the first time - LOG.warn("Expected trailer object at position " + trailerOffset + LOG.warn("Expected trailer object at offset " + trailerOffset + ", keep trying"); } readLine(); @@ -2603,12 +2688,31 @@ if (splitString.length != 2) { LOG.warn("Unexpected XRefTable Entry: " + currentLine); - break; + return false; } // first obj id - long currObjID = Long.parseLong(splitString[0]); + long currObjID = 0; + try + { + currObjID = Long.parseLong(splitString[0]); + } + catch (NumberFormatException exception) + { + LOG.warn("XRefTable: invalid ID for the first object: " + currentLine); + return false; + } + // the number of objects in the xref table - int count = Integer.parseInt(splitString[1]); + int count = 0; + try + { + count = Integer.parseInt(splitString[1]); + } + catch (NumberFormatException exception) + { + LOG.warn("XRefTable: invalid number of objects: " + currentLine); + return false; + } skipSpaces(); for(int i = 0; i < count; i++) @@ -2683,9 +2787,8 @@ } /** - * This will get the document that was parsed. parse() must be called before this is called. - * When you are done with this document you must call close() on it to release - * resources. + * This will get the document that was parsed. The document must be parsed before this is called. When you are done + * with this document you must call close() on it to release resources. * * @return The document that was parsed. * @@ -2695,18 +2798,51 @@ { if( document == null ) { - throw new IOException( "You must call parse() before calling getDocument()" ); + throw new IOException("You must parse the document first before calling getDocument()"); } return document; } /** + * This will get the encryption dictionary. The document must be parsed before this is called. + * + * @return The encryption dictionary of the document that was parsed. + * + * @throws IOException If there is an error getting the document. + */ + public PDEncryption getEncryption() throws IOException + { + if (document == null) + { + throw new IOException( + "You must parse the document first before calling getEncryption()"); + } + return encryption; + } + + /** + * This will get the AccessPermission. The document must be parsed before this is called. + * + * @return The access permission of document that was parsed. + * + * @throws IOException If there is an error getting the document. + */ + public AccessPermission getAccessPermission() throws IOException + { + if (document == null) + { + throw new IOException( + "You must parse the document first before calling getAccessPermission()"); + } + return accessPermission; + } + + /** * Parse the values of the trailer dictionary and return the root object. * * @param trailer The trailer dictionary. * @return The parsed root object. - * @throws IOException If an IO error occurs or if the root object is - * missing in the trailer dictionary. + * @throws IOException If an IO error occurs or if the root object is missing in the trailer dictionary. */ protected COSBase parseTrailerValuesDynamically(COSDictionary trailer) throws IOException { @@ -2721,7 +2857,7 @@ } } // parse catalog or root object - COSObject root = (COSObject) trailer.getItem(COSName.ROOT); + COSObject root = trailer.getCOSObject(COSName.ROOT); if (root == null) { throw new IOException("Missing root object specification in trailer."); @@ -2729,4 +2865,94 @@ return root.getObject(); } + /** + * Prepare for decryption. + * + * @throws InvalidPasswordException If the password is incorrect. + * @throws IOException if something went wrong + */ + private void prepareDecryption() throws InvalidPasswordException, IOException + { + if (encryption == null) + { + COSBase trailerEncryptItem = document.getTrailer().getItem(COSName.ENCRYPT); + if (trailerEncryptItem != null && !(trailerEncryptItem instanceof COSNull)) + { + if (trailerEncryptItem instanceof COSObject) + { + COSObject trailerEncryptObj = (COSObject) trailerEncryptItem; + parseDictionaryRecursive(trailerEncryptObj); + } + try + { + encryption = new PDEncryption(document.getEncryptionDictionary()); + DecryptionMaterial decryptionMaterial; + if (keyStoreInputStream != null) + { + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(keyStoreInputStream, password.toCharArray()); + + decryptionMaterial = new PublicKeyDecryptionMaterial(ks, keyAlias, + password); + } + else + { + decryptionMaterial = new StandardDecryptionMaterial(password); + } + + securityHandler = encryption.getSecurityHandler(); + securityHandler.prepareForDecryption(encryption, document.getDocumentID(), + decryptionMaterial); + accessPermission = securityHandler.getCurrentAccessPermission(); + } + catch (IOException e) + { + throw e; + } + catch (Exception e) + { + throw new IOException("Error (" + e.getClass().getSimpleName() + + ") while creating security handler for decryption", e); + } + finally + { + if (keyStoreInputStream != null) + { + IOUtils.closeQuietly(keyStoreInputStream); + } + } + } + } + } + + /** + * Resolves all not already parsed objects of a dictionary recursively. + * + * @param dictionaryObject dictionary to be parsed + * @throws IOException if something went wrong + * + */ + private void parseDictionaryRecursive(COSObject dictionaryObject) throws IOException + { + parseObjectDynamically(dictionaryObject, true); + if (!(dictionaryObject.getObject() instanceof COSDictionary)) + { + // we can't be lenient here, this is called by prepareDecryption() + // to get the encryption directory + throw new IOException("Dictionary object expected at offset " + source.getPosition()); + } + COSDictionary dictionary = (COSDictionary) dictionaryObject.getObject(); + for (COSBase value : dictionary.getValues()) + { + if (value instanceof COSObject) + { + COSObject object = (COSObject) value; + if (object.getObject() == null) + { + parseDictionaryRecursive(object); + } + } + } + } + } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/InputStreamSource.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/InputStreamSource.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/InputStreamSource.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/InputStreamSource.java 2018-11-28 17:18:38.000000000 +0000 @@ -52,16 +52,30 @@ public int read(byte[] b) throws IOException { int n = input.read(b); - position += n; - return n; + if (n > 0) + { + position += n; + return n; + } + else + { + return -1; + } } @Override public int read(byte[] b, int offset, int length) throws IOException { int n = input.read(b, offset, length); - position += n; - return n; + if (n > 0) + { + position += n; + return n; + } + else + { + return -1; + } } @Override @@ -111,9 +125,16 @@ while (len > 0) { int n = this.read(bytes, off, len); - off += n; - len -= n; - position += n; + if (n > 0) + { + off += n; + len -= n; + position += n; + } + else + { + break; + } } return bytes; } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParser.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParser.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParser.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParser.java 2018-11-28 17:18:38.000000000 +0000 @@ -69,6 +69,10 @@ { //need to first parse the header. int numberOfObjects = stream.getInt( "N" ); + if (numberOfObjects == -1) + { + throw new IOException("/N entry missing in object stream"); + } List objectNumbers = new ArrayList( numberOfObjects ); streamObjects = new ArrayList( numberOfObjects ); for( int i=0; i objNums = new ArrayList(); @@ -89,11 +92,25 @@ * Populates objNums with all object numbers available */ Iterator indexIter = indexArray.iterator(); - while(indexIter.hasNext()) + while (indexIter.hasNext()) { - long objID = ((COSInteger)indexIter.next()).longValue(); - int size = ((COSInteger)indexIter.next()).intValue(); - for(int i = 0; i < size; i++) + base = indexIter.next(); + if (!(base instanceof COSInteger)) + { + throw new IOException("Xref stream must have integer in /Index array"); + } + long objID = ((COSInteger) base).longValue(); + if (!indexIter.hasNext()) + { + break; + } + base = indexIter.next(); + if (!(base instanceof COSInteger)) + { + throw new IOException("Xref stream must have integer in /Index array"); + } + int size = ((COSInteger) base).intValue(); + for (int i = 0; i < size; i++) { objNums.add(objID + i); } @@ -102,9 +119,9 @@ /* * Calculating the size of the line in bytes */ - int w0 = xrefFormat.getInt(0); - int w1 = xrefFormat.getInt(1); - int w2 = xrefFormat.getInt(2); + int w0 = xrefFormat.getInt(0, 0); + int w1 = xrefFormat.getInt(1, 0); + int w2 = xrefFormat.getInt(2, 0); int lineSize = w0 + w1 + w2; while(!seqSource.isEOF() && objIter.hasNext()) diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSStandardOutputStream.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSStandardOutputStream.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSStandardOutputStream.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSStandardOutputStream.java 2018-11-28 17:18:38.000000000 +0000 @@ -49,7 +49,7 @@ private boolean onNewLine = false; /** - * COSOutputStream constructor comment. + * Constructor. * * @param out The underlying stream to write to. */ @@ -59,7 +59,7 @@ } /** - * COSOutputStream constructor comment. + * Constructor. * * @param out The underlying stream to write to. * @param position The current position of output stream. @@ -73,7 +73,7 @@ } /** - * COSOutputStream constructor comment. + * Constructor. * * @param out The underlying stream to write to. * @param position The current position of output stream. diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSWriter.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSWriter.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSWriter.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSWriter.java 2018-11-28 17:18:38.000000000 +0000 @@ -220,9 +220,10 @@ private COSArray byteRangeArray; /** - * COSWriter constructor comment. + * COSWriter constructor. * - * @param outputStream The wrapped output stream. + * @param outputStream The output stream to write the PDF. It will be closed when this object is + * closed. */ public COSWriter(OutputStream outputStream) { @@ -231,11 +232,12 @@ } /** - * COSWriter constructor for incremental updates. + * COSWriter constructor for incremental updates. * - * @param outputStream output stream where the new PDF data will be written + * @param outputStream output stream where the new PDF data will be written. It will be closed + * when this object is closed. * @param inputData random access read containing source PDF data - * + * * @throws IOException if something went wrong */ public COSWriter(OutputStream outputStream, RandomAccessRead inputData) throws IOException @@ -259,7 +261,7 @@ Map xrefTable = cosDoc.getXrefTable(); Set keySet = xrefTable.keySet(); - long highestNumber=0; + long highestNumber = doc.getDocument().getHighestXRefObjectNumber(); for ( COSObjectKey cosObjectKey : keySet ) { COSBase object = cosDoc.getObjectFromPool(cosObjectKey).getObject(); @@ -309,10 +311,6 @@ { getStandardOutput().close(); } - if (getOutput() != null) - { - getOutput().close(); - } if (incrementalOutput != null) { incrementalOutput.close(); @@ -930,8 +928,12 @@ else if( current instanceof COSObject ) { COSBase subValue = ((COSObject)current).getObject(); - if (incrementalUpdate || subValue instanceof COSDictionary || subValue == null) + if (willEncrypt || incrementalUpdate || subValue instanceof COSDictionary || subValue == null) { + // PDFBOX-4308: added willEncrypt to prevent an object + // that is referenced several times from being written + // direct and indirect, thus getting encrypted + // with wrong object number or getting encrypted twice addObjectToWrite( current ); writeReference( current ); } @@ -1028,8 +1030,12 @@ else if( value instanceof COSObject ) { COSBase subValue = ((COSObject)value).getObject(); - if (incrementalUpdate || subValue instanceof COSDictionary || subValue == null) + if (willEncrypt || incrementalUpdate || subValue instanceof COSDictionary || subValue == null) { + // PDFBOX-4308: added willEncrypt to prevent an object + // that is referenced several times from being written + // direct and indirect, thus getting encrypted + // with wrong object number or getting encrypted twice addObjectToWrite( value ); writeReference( value ); } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSWriterXRefEntry.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSWriterXRefEntry.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSWriterXRefEntry.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdfwriter/COSWriterXRefEntry.java 2018-11-28 17:18:38.000000000 +0000 @@ -41,7 +41,7 @@ } /** - * COSWriterXRefEntry constructor comment. + * Constructor. * * @param start The start attribute. * @param obj The COS object that this entry represents. diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/COSDictionaryMap.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/COSDictionaryMap.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/COSDictionaryMap.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/COSDictionaryMap.java 2018-11-28 17:18:36.000000000 +0000 @@ -176,7 +176,7 @@ boolean retval = false; if( o instanceof COSDictionaryMap ) { - COSDictionaryMap other = (COSDictionaryMap)o; + COSDictionaryMap other = (COSDictionaryMap) o; retval = other.map.equals( this.map ); } return retval; diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType3.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType3.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType3.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType3.java 2018-11-28 17:18:36.000000000 +0000 @@ -34,6 +34,7 @@ private COSArray encode = null; private COSArray bounds = null; private PDFunction[] functionsArray = null; + private float[] boundsValues = null; /** * Constructor. @@ -88,7 +89,10 @@ } else { - float[] boundsValues = getBounds().toFloatArray(); + if (boundsValues == null) + { + boundsValues = getBounds().toFloatArray(); + } int boundsSize = boundsValues.length; // create a combined array containing the domain and the bounds values // domain.min, bounds[0], bounds[1], ...., bounds[boundsSize-1], domain.max diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionTypeIdentity.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionTypeIdentity.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionTypeIdentity.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionTypeIdentity.java 2018-11-28 17:18:36.000000000 +0000 @@ -36,6 +36,7 @@ { // shouldn't be called throw new UnsupportedOperationException(); + //TODO this is a violation of the interface segregation principle } @Override diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNameTreeNode.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNameTreeNode.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNameTreeNode.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNameTreeNode.java 2018-11-28 17:18:36.000000000 +0000 @@ -36,6 +36,8 @@ * This class represents a node in a name tree. * * @author Ben Litchfield + * + * @param The type of the values in this name tree. */ public abstract class PDNameTreeNode implements COSObjectable { @@ -244,12 +246,14 @@ } /** - * This will return a map of names. The key will be a string, and the - * value will depend on where this class is being used. + * This will return a map of names on this level. The key will be a string, + * and the value will depend on where this class is being used. + * + * @return ordered map of COS objects or null if the dictionary + * contains no 'Names' entry on this level. * - * @return ordered map of cos objects or null if dictionary - * contains no 'Names' entry * @throws IOException If there is an error while creating the sub types. + * @see #getKids() */ public Map getNames() throws IOException { diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNumberTreeNode.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNumberTreeNode.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNumberTreeNode.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNumberTreeNode.java 2018-11-28 17:18:36.000000000 +0000 @@ -174,13 +174,20 @@ public Map getNumbers() throws IOException { Map indices = null; - COSArray namesArray = (COSArray)node.getDictionaryObject( COSName.NUMS ); - if( namesArray != null ) + COSBase numBase = node.getDictionaryObject(COSName.NUMS); + if (numBase instanceof COSArray) { + COSArray namesArray = (COSArray) numBase; indices = new HashMap(); for( int i=0; i getPageIndices() { - return new TreeSet(labels.keySet()); + return new TreeSet(labels.keySet()); } /** diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java 2018-11-28 17:18:36.000000000 +0000 @@ -174,6 +174,10 @@ while (it.hasNext()) { COSBase item = it.next(); + if (item instanceof COSObject) + { + item = ((COSObject) item).getObject(); + } if (item instanceof COSDictionary) { ao = PDAttributeObject.create((COSDictionary) item); @@ -344,6 +348,10 @@ while (it.hasNext()) { COSBase item = it.next(); + if (item instanceof COSObject) + { + item = ((COSObject) item).getObject(); + } if (item instanceof COSName) { className = ((COSName) item).getName(); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java 2018-11-28 17:18:36.000000000 +0000 @@ -92,9 +92,10 @@ } /** - * Returns the K entry. - * - * @return the K entry + * Returns the K entry. This can be a dictionary representing a structure element, or an array + * of them. + * + * @return the K entry. */ public COSBase getK() { diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/SecurityHandler.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/SecurityHandler.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/SecurityHandler.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/SecurityHandler.java 2018-11-28 17:18:36.000000000 +0000 @@ -557,7 +557,7 @@ { ByteArrayInputStream data = new ByteArrayInputStream(string.getBytes()); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - encryptData(objNum, genNum, data, buffer, false /* decrypt */); + encryptData(objNum, genNum, data, buffer, false /* encrypt */); string.setValue(buffer.toByteArray()); } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotation.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotation.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotation.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotation.java 2018-11-28 17:18:36.000000000 +0000 @@ -37,10 +37,12 @@ import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderEffectDictionary; import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; import org.apache.pdfbox.util.DateConverter; +import org.w3c.dom.CDATASection; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.w3c.dom.Text; /** * This represents an FDF annotation that is part of the FDF document. @@ -959,43 +961,49 @@ private String richContentsToString(Node node, boolean root) { - String retval = ""; - XPath xpath = XPathFactory.newInstance().newXPath(); - try + String subString = ""; + + NodeList nodelist = node.getChildNodes(); + for (int i = 0; i < nodelist.getLength(); i++) { - NodeList nodelist = (NodeList) xpath.evaluate("*", node, XPathConstants.NODESET); - String subString = ""; - if (nodelist.getLength() == 0) - { - subString = node.getFirstChild().getNodeValue(); - } - for (int i = 0; i < nodelist.getLength(); i++) + Node child = nodelist.item(i); + if (child instanceof Element) { - Node child = nodelist.item(i); - if (child instanceof Element) - { - subString += richContentsToString(child, false); - } + subString += richContentsToString(child, false); } - NamedNodeMap attributes = node.getAttributes(); - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < attributes.getLength(); i++) + else if (child instanceof CDATASection) { - Node attribute = attributes.item(i); - builder.append(String.format(" %s=\"%s\"", attribute.getNodeName(), - attribute.getNodeValue())); + subString += ""; } - if (root) + else if (child instanceof Text) { - return subString; + String cdata = ((Text) child).getData(); + if (cdata!=null) + { + cdata = cdata.replace("&", "&").replace("<", "<"); + } + subString += cdata; } - retval = String.format("<%s%s>%s", node.getNodeName(), builder.toString(), - subString, node.getNodeName()); } - catch (XPathExpressionException e) + if (root) { - LOG.debug("Error while evaluating XPath expression for richtext contents"); + return subString; } - return retval; + + NamedNodeMap attributes = node.getAttributes(); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < attributes.getLength(); i++) + { + Node attribute = attributes.item(i); + String attributeNodeValue = attribute.getNodeValue(); + if (attributeNodeValue!=null) + { + attributeNodeValue = attributeNodeValue.replace("\"", """); + } + builder.append(String.format(" %s=\"%s\"", attribute.getNodeName(), + attributeNodeValue)); + } + return String.format("<%s%s>%s", node.getNodeName(), builder.toString(), + subString, node.getNodeName()); } -} +} \ No newline at end of file diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/GlyphList.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/GlyphList.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/GlyphList.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/GlyphList.java 2018-11-28 17:18:38.000000000 +0000 @@ -25,6 +25,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * PostScript glyph list, maps glyph names to sequences of Unicode characters. @@ -96,7 +97,7 @@ private final Map unicodeToName; // additional read/write cache for uniXXXX names - private final Map uniNameToUnicodeCache = new HashMap(); + private final Map uniNameToUnicodeCache = new ConcurrentHashMap(); /** * Creates a new GlyphList from a glyph list file. @@ -290,7 +291,11 @@ LOG.warn("Not a number in Unicode character name: " + name); } } - uniNameToUnicodeCache.put(name, unicode); + if (unicode != null) + { + // null value not allowed in ConcurrentHashMap + uniNameToUnicodeCache.put(name, unicode); + } } return unicode; } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java 2018-11-28 17:18:38.000000000 +0000 @@ -560,6 +560,14 @@ PDPanoseClassification panose = fontDescriptor.getPanose().getPanose(); if (panose.getFamilyKind() == info.getPanose().getFamilyKind()) { + if (panose.getFamilyKind() == 0 && + (info.getPostScriptName().toLowerCase().contains("barcode") || + info.getPostScriptName().startsWith("Code")) && + !probablyBarcodeFont(fontDescriptor)) + { + // PDFBOX-4268: ignore barcode font if we aren't searching for one. + continue; + } // serifs if (panose.getSerifStyle() == info.getPanose().getSerifStyle()) { @@ -624,6 +632,22 @@ return queue; } + private boolean probablyBarcodeFont(PDFontDescriptor fontDescriptor) + { + String ff = fontDescriptor.getFontFamily(); + if (ff == null) + { + ff = ""; + } + String fn = fontDescriptor.getFontName(); + if (fn == null) + { + fn = ""; + } + return ff.startsWith("Code") || ff.toLowerCase().contains("barcode") || + fn.startsWith("Code") || fn.toLowerCase().contains("barcode"); + } + /** * Returns true if the character set described by CIDSystemInfo is present in the given font. * Only applies to Adobe-GB1, Adobe-CNS1, Adobe-Japan1, Adobe-Korea1, as per the PDF spec. diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java 2018-11-28 17:18:38.000000000 +0000 @@ -315,7 +315,8 @@ public final byte[] encode(String text) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); - for (int offset = 0; offset < text.length(); ) + int offset = 0; + while (offset < text.length()) { int codePoint = text.codePointAt(offset); @@ -439,13 +440,16 @@ // if the font dictionary containsName a ToUnicode CMap, use that CMap if (toUnicodeCMap != null) { - if (toUnicodeCMap.getName() != null && toUnicodeCMap.getName().startsWith("Identity-") && - dict.getDictionaryObject(COSName.TO_UNICODE) instanceof COSName) + if (toUnicodeCMap.getName() != null && + toUnicodeCMap.getName().startsWith("Identity-") && + (dict.getDictionaryObject(COSName.TO_UNICODE) instanceof COSName || + !toUnicodeCMap.hasUnicodeMappings())) { // handle the undocumented case of using Identity-H/V as a ToUnicode CMap, this // isn't actually valid as the Identity-x CMaps are code->CID maps, not // code->Unicode maps. See sample_fonts_solidconvertor.pdf for an example. // PDFBOX-3123: do this only if the /ToUnicode entry is a name + // PDFBOX-4322: identity streams are OK too return new String(new char[] { (char) code }); } else diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDTrueTypeFont.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDTrueTypeFont.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDTrueTypeFont.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDTrueTypeFont.java 2018-11-28 17:18:38.000000000 +0000 @@ -18,7 +18,6 @@ import java.awt.geom.GeneralPath; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; @@ -309,7 +308,7 @@ glyphList = GlyphList.getAdobeGlyphList(); if (closeTTF) { - // the TTF is fully loaded and it is save to close the underlying data source + // the TTF is fully loaded and it is safe to close the underlying data source ttf.close(); } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType0Font.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType0Font.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType0Font.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType0Font.java 2018-11-28 17:18:38.000000000 +0000 @@ -194,8 +194,20 @@ fetchCMapUCS2(); } + /** + * Private. Creates a new PDType0Font font for embedding. + * + * @param document + * @param ttf + * @param embedSubset + * @param closeTTF whether to close the ttf parameter after embedding. Must be true when the ttf + * parameter was created in the load() method, false when the ttf parameter was passed to the + * load() method. + * @param vertical + * @throws IOException + */ private PDType0Font(PDDocument document, TrueTypeFont ttf, boolean embedSubset, - boolean closeOnSubset, boolean vertical) throws IOException + boolean closeTTF, boolean vertical) throws IOException { if (vertical) { @@ -205,15 +217,16 @@ descendantFont = embedder.getCIDFont(); readEncoding(); fetchCMapUCS2(); - if (closeOnSubset) + if (closeTTF) { if (embedSubset) { this.ttf = ttf; + document.registerTrueTypeFontForClosing(ttf); } else { - // the TTF is fully loaded and it is save to close the underlying data source + // the TTF is fully loaded and it is safe to close the underlying data source ttf.close(); } } @@ -550,7 +563,7 @@ { descendant = getDescendantFont().getClass().getSimpleName(); } - return getClass().getSimpleName() + "/" + descendant + " " + getBaseFont(); + return getClass().getSimpleName() + "/" + descendant + ", PostScript name: " + getBaseFont(); } @Override diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1Font.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1Font.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1Font.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1Font.java 2018-11-28 17:18:38.000000000 +0000 @@ -453,7 +453,7 @@ Map inverted = encoding.getNameToCodeMap(); int code = inverted.get(name); bytes = new byte[] { (byte)code }; - codeToBytesMap.put(code, bytes); + codeToBytesMap.put(unicode, bytes); return bytes; } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType3Font.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType3Font.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType3Font.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType3Font.java 2018-11-28 17:18:38.000000000 +0000 @@ -19,6 +19,8 @@ import java.awt.geom.GeneralPath; import java.io.IOException; import java.io.InputStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.fontbox.FontBoxFont; import org.apache.fontbox.util.BoundingBox; import org.apache.pdfbox.cos.COSArray; @@ -41,6 +43,11 @@ */ public class PDType3Font extends PDSimpleFont { + /** + * Log instance. + */ + private static final Log LOG = LogFactory.getLog(PDType3Font.class); + private PDResources resources; private COSDictionary charProcs; private Matrix fontMatrix; @@ -66,8 +73,20 @@ @Override protected final void readEncoding() throws IOException { - COSDictionary encodingDict = (COSDictionary)dict.getDictionaryObject(COSName.ENCODING); - encoding = new DictionaryEncoding(encodingDict); + COSBase encodingBase = dict.getDictionaryObject(COSName.ENCODING); + if (encodingBase instanceof COSName) + { + COSName encodingName = (COSName) encodingBase; + encoding = Encoding.getInstance(encodingName); + if (encoding == null) + { + LOG.warn("Unknown encoding: " + encodingName.getName()); + } + } + else if (encodingBase instanceof COSDictionary) + { + encoding = new DictionaryEncoding((COSDictionary) encodingBase); + } glyphList = GlyphList.getAdobeGlyphList(); } @@ -94,8 +113,8 @@ @Override public boolean hasGlyph(String name) throws IOException { - COSStream stream = (COSStream) getCharProcs().getDictionaryObject(COSName.getPDFName(name)); - return stream != null; + COSBase base = getCharProcs().getDictionaryObject(COSName.getPDFName(name)); + return base instanceof COSStream; } @Override @@ -139,7 +158,8 @@ public float getWidthFromFont(int code) throws IOException { PDType3CharProc charProc = getCharProc(code); - if (charProc == null) + if (charProc == null || charProc.getContentStream() == null || + charProc.getContentStream().getLength() == 0) { return 0; } @@ -205,10 +225,10 @@ { if (fontMatrix == null) { - COSArray array = (COSArray) dict.getDictionaryObject(COSName.FONT_MATRIX); - if (array != null) + COSBase base = dict.getDictionaryObject(COSName.FONT_MATRIX); + if (base instanceof COSArray) { - fontMatrix = new Matrix(array); + fontMatrix = new Matrix((COSArray) base); } else { @@ -234,10 +254,10 @@ { if (resources == null) { - COSDictionary resourcesDict = (COSDictionary) dict.getDictionaryObject(COSName.RESOURCES); - if (resourcesDict != null) + COSBase base = dict.getDictionaryObject(COSName.RESOURCES); + if (base instanceof COSDictionary) { - this.resources = new PDResources(resourcesDict); + this.resources = new PDResources((COSDictionary) base); } } return resources; @@ -250,11 +270,11 @@ */ public PDRectangle getFontBBox() { - COSArray rect = (COSArray) dict.getDictionaryObject(COSName.FONT_BBOX); + COSBase base = dict.getDictionaryObject(COSName.FONT_BBOX); PDRectangle retval = null; - if(rect != null) + if (base instanceof COSArray) { - retval = new PDRectangle(rect); + retval = new PDRectangle((COSArray) base); } return retval; } @@ -329,15 +349,10 @@ public PDType3CharProc getCharProc(int code) { String name = getEncoding().getName(code); - if (!".notdef".equals(name)) + COSBase base = getCharProcs().getDictionaryObject(COSName.getPDFName(name)); + if (base instanceof COSStream) { - COSStream stream; - stream = (COSStream)getCharProcs().getDictionaryObject(COSName.getPDFName(name)); - if (stream == null) - { - return null; - } - return new PDType3CharProc(this, stream); + return new PDType3CharProc(this, (COSStream) base); } return null; } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDVectorFont.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDVectorFont.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDVectorFont.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDVectorFont.java 2018-11-28 17:18:38.000000000 +0000 @@ -28,15 +28,15 @@ public interface PDVectorFont { /** - * Returns the glyph path for the given character code. + * Returns the glyph path for the given character code in a PDF. * * @param code character code in a PDF. Not to be confused with unicode. * @throws java.io.IOException if the font could not be read */ GeneralPath getPath(int code) throws IOException; - + /** - * Returns true if this font contains a glyph for the given character code. + * Returns true if this font contains a glyph for the given character code in a PDF. * * @param code character code in a PDF. Not to be confused with unicode. */ diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/ToUnicodeWriter.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/ToUnicodeWriter.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/ToUnicodeWriter.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/ToUnicodeWriter.java 2018-11-28 17:18:38.000000000 +0000 @@ -39,6 +39,11 @@ private int wMode; /** + * To test corner case of PDFBOX-4302. + */ + static final int MAX_ENTRIES_PER_OPERATOR = 100; + + /** * Creates a new ToUnicode CMap writer. */ ToUnicodeWriter() @@ -145,15 +150,18 @@ dstPrev = text; } - // limit of 100 entries per operator - int batchCount = (int)Math.ceil(srcFrom.size() / 100.0); + // limit entries per operator + int batchCount = (int) Math.ceil(srcFrom.size() / + (double) MAX_ENTRIES_PER_OPERATOR); for (int batch = 0; batch < batchCount; batch++) { - int count = batch == batchCount - 1 ? srcFrom.size() % 100 : 100; + int count = batch == batchCount - 1 ? + srcFrom.size() - MAX_ENTRIES_PER_OPERATOR * batch : + MAX_ENTRIES_PER_OPERATOR; writer.write(count + " beginbfrange\n"); for (int j = 0; j < count; j++) { - int index = batch * 100 + j; + int index = batch * MAX_ENTRIES_PER_OPERATOR + j; writer.write('<'); writer.write(Hex.getChars(srcFrom.get(index).shortValue())); writer.write("> "); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendComposite.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendComposite.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendComposite.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendComposite.java 2018-11-28 17:18:36.000000000 +0000 @@ -1,228 +1,274 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.pdfbox.pdmodel.graphics.blend; - -import java.awt.AlphaComposite; -import java.awt.Composite; -import java.awt.CompositeContext; -import java.awt.RenderingHints; -import java.awt.color.ColorSpace; -import java.awt.image.ColorModel; -import java.awt.image.Raster; -import java.awt.image.WritableRaster; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * AWT composite for blend modes. - * - * @author Kühn & Weyh Software GmbH - */ -public final class BlendComposite implements Composite -{ - /** - * Log instance. - */ - private static final Log LOG = LogFactory.getLog(BlendComposite.class); - - /** - * Creates a blend composite - * - * @param blendMode Desired blend mode - * @param constantAlpha Constant alpha, must be in the inclusive range - * [0.0...1.0] or it will be clipped. - * @return a blend composite. - */ - public static Composite getInstance(BlendMode blendMode, float constantAlpha) - { - if (constantAlpha < 0) - { - LOG.warn("using 0 instead of incorrect Alpha " + constantAlpha); - constantAlpha = 0; - } - else if (constantAlpha > 1) - { - LOG.warn("using 1 instead of incorrect Alpha " + constantAlpha); - constantAlpha = 1; - } - if (blendMode == BlendMode.NORMAL) - { - return AlphaComposite.getInstance(AlphaComposite.SRC_OVER, constantAlpha); - } - else - { - return new BlendComposite(blendMode, constantAlpha); - } - } - - // TODO - non-separable blending modes - - private final BlendMode blendMode; - private final float constantAlpha; - - private BlendComposite(BlendMode blendMode, float constantAlpha) - { - super(); - this.blendMode = blendMode; - this.constantAlpha = constantAlpha; - } - - @Override - public CompositeContext createContext(ColorModel srcColorModel, ColorModel dstColorModel, - RenderingHints hints) - { - return new BlendCompositeContext(srcColorModel, dstColorModel, hints); - } - - class BlendCompositeContext implements CompositeContext - { - private final ColorModel srcColorModel; - private final ColorModel dstColorModel; - private final RenderingHints hints; - - BlendCompositeContext(ColorModel srcColorModel, ColorModel dstColorModel, - RenderingHints hints) - { - this.srcColorModel = srcColorModel; - this.dstColorModel = dstColorModel; - this.hints = hints; - } - - @Override - public void dispose() - { - // nothing needed - } - - @Override - public void compose(Raster src, Raster dstIn, WritableRaster dstOut) - { - int x0 = src.getMinX(); - int y0 = src.getMinY(); - int width = Math.min(Math.min(src.getWidth(), dstIn.getWidth()), dstOut.getWidth()); - int height = Math.min(Math.min(src.getHeight(), dstIn.getHeight()), dstOut.getHeight()); - int x1 = x0 + width; - int y1 = y0 + height; - int dstInXShift = dstIn.getMinX() - x0; - int dstInYShift = dstIn.getMinY() - y0; - int dstOutXShift = dstOut.getMinX() - x0; - int dstOutYShift = dstOut.getMinY() - y0; - - ColorSpace srcColorSpace = srcColorModel.getColorSpace(); - int numSrcColorComponents = srcColorModel.getNumColorComponents(); - int numSrcComponents = src.getNumBands(); - boolean srcHasAlpha = (numSrcComponents > numSrcColorComponents); - ColorSpace dstColorSpace = dstColorModel.getColorSpace(); - int numDstColorComponents = dstColorModel.getNumColorComponents(); - int numDstComponents = dstIn.getNumBands(); - boolean dstHasAlpha = (numDstComponents > numDstColorComponents); - - int colorSpaceType = dstColorSpace.getType(); - boolean subtractive = (colorSpaceType != ColorSpace.TYPE_RGB) - && (colorSpaceType != ColorSpace.TYPE_GRAY); - - boolean blendModeIsSeparable = blendMode instanceof SeparableBlendMode; - SeparableBlendMode separableBlendMode = blendModeIsSeparable ? - (SeparableBlendMode) blendMode : null; - - boolean needsColorConversion = !srcColorSpace.equals(dstColorSpace); - - Object srcPixel = null; - Object dstPixel = null; - float[] srcComponents = new float[numSrcComponents]; - // PDFBOX-3501 let getNormalizedComponents allocate to avoid - // ArrayIndexOutOfBoundsException for bitonal target - float[] dstComponents = null; - - float[] srcColor = new float[numSrcColorComponents]; - float[] srcConverted; - - for (int y = y0; y < y1; y++) - { - for (int x = x0; x < x1; x++) - { - srcPixel = src.getDataElements(x, y, srcPixel); - dstPixel = dstIn.getDataElements(dstInXShift + x, dstInYShift + y, dstPixel); - - srcComponents = srcColorModel.getNormalizedComponents(srcPixel, srcComponents, - 0); - dstComponents = dstColorModel.getNormalizedComponents(dstPixel, dstComponents, - 0); - - float srcAlpha = srcHasAlpha ? srcComponents[numSrcColorComponents] : 1.0f; - float dstAlpha = dstHasAlpha ? dstComponents[numDstColorComponents] : 1.0f; - - srcAlpha = srcAlpha * constantAlpha; - - float resultAlpha = dstAlpha + srcAlpha - srcAlpha * dstAlpha; - float srcAlphaRatio = (resultAlpha > 0) ? srcAlpha / resultAlpha : 0; - - // convert color - System.arraycopy(srcComponents, 0, srcColor, 0, numSrcColorComponents); - if (needsColorConversion) - { - // TODO - very very slow - Hash results??? - float[] cieXYZ = srcColorSpace.toCIEXYZ(srcColor); - srcConverted = dstColorSpace.fromCIEXYZ(cieXYZ); - } - else - { - srcConverted = srcColor; - } - - if (separableBlendMode != null) - { - for (int k = 0; k < numDstColorComponents; k++) - { - float srcValue = srcConverted[k]; - float dstValue = dstComponents[k]; - - if (subtractive) - { - srcValue = 1 - srcValue; - dstValue = 1 - dstValue; - } - - float value = separableBlendMode.blendChannel(srcValue, dstValue); - value = srcValue + dstAlpha * (value - srcValue); - value = dstValue + srcAlphaRatio * (value - dstValue); - - if (subtractive) - { - value = 1 - value; - } - - dstComponents[k] = value; - } - } - else - { - // TODO - nonseparable modes - } - - if (dstHasAlpha) - { - dstComponents[numDstColorComponents] = resultAlpha; - } - - dstPixel = dstColorModel.getDataElements(dstComponents, 0, dstPixel); - dstOut.setDataElements(dstOutXShift + x, dstOutYShift + y, dstPixel); - } - } - } - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdmodel.graphics.blend; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.CompositeContext; +import java.awt.RenderingHints; +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * AWT composite for blend modes. + * + * @author Kühn & Weyh Software GmbH + */ +public final class BlendComposite implements Composite +{ + /** + * Log instance. + */ + private static final Log LOG = LogFactory.getLog(BlendComposite.class); + + /** + * Creates a blend composite + * + * @param blendMode Desired blend mode + * @param constantAlpha Constant alpha, must be in the inclusive range + * [0.0...1.0] or it will be clipped. + * @return a blend composite. + */ + public static Composite getInstance(BlendMode blendMode, float constantAlpha) + { + if (constantAlpha < 0) + { + LOG.warn("using 0 instead of incorrect Alpha " + constantAlpha); + constantAlpha = 0; + } + else if (constantAlpha > 1) + { + LOG.warn("using 1 instead of incorrect Alpha " + constantAlpha); + constantAlpha = 1; + } + if (blendMode == BlendMode.NORMAL) + { + return AlphaComposite.getInstance(AlphaComposite.SRC_OVER, constantAlpha); + } + else + { + return new BlendComposite(blendMode, constantAlpha); + } + } + + private final BlendMode blendMode; + private final float constantAlpha; + + private BlendComposite(BlendMode blendMode, float constantAlpha) + { + super(); + this.blendMode = blendMode; + this.constantAlpha = constantAlpha; + } + + @Override + public CompositeContext createContext(ColorModel srcColorModel, ColorModel dstColorModel, + RenderingHints hints) + { + return new BlendCompositeContext(srcColorModel, dstColorModel, hints); + } + + class BlendCompositeContext implements CompositeContext + { + private final ColorModel srcColorModel; + private final ColorModel dstColorModel; + private final RenderingHints hints; + + BlendCompositeContext(ColorModel srcColorModel, ColorModel dstColorModel, + RenderingHints hints) + { + this.srcColorModel = srcColorModel; + this.dstColorModel = dstColorModel; + this.hints = hints; + } + + @Override + public void dispose() + { + // nothing needed + } + + @Override + public void compose(Raster src, Raster dstIn, WritableRaster dstOut) + { + int x0 = src.getMinX(); + int y0 = src.getMinY(); + int width = Math.min(Math.min(src.getWidth(), dstIn.getWidth()), dstOut.getWidth()); + int height = Math.min(Math.min(src.getHeight(), dstIn.getHeight()), dstOut.getHeight()); + int x1 = x0 + width; + int y1 = y0 + height; + int dstInXShift = dstIn.getMinX() - x0; + int dstInYShift = dstIn.getMinY() - y0; + int dstOutXShift = dstOut.getMinX() - x0; + int dstOutYShift = dstOut.getMinY() - y0; + + ColorSpace srcColorSpace = srcColorModel.getColorSpace(); + int numSrcColorComponents = srcColorModel.getNumColorComponents(); + int numSrcComponents = src.getNumBands(); + boolean srcHasAlpha = (numSrcComponents > numSrcColorComponents); + ColorSpace dstColorSpace = dstColorModel.getColorSpace(); + int numDstColorComponents = dstColorModel.getNumColorComponents(); + int numDstComponents = dstIn.getNumBands(); + boolean dstHasAlpha = (numDstComponents > numDstColorComponents); + + int srcColorSpaceType = srcColorSpace.getType(); + int dstColorSpaceType = dstColorSpace.getType(); + boolean subtractive = (dstColorSpaceType != ColorSpace.TYPE_RGB) + && (dstColorSpaceType != ColorSpace.TYPE_GRAY); + + boolean blendModeIsSeparable = blendMode instanceof SeparableBlendMode; + SeparableBlendMode separableBlendMode = blendModeIsSeparable ? + (SeparableBlendMode) blendMode : null; + NonSeparableBlendMode nonSeparableBlendMode = !blendModeIsSeparable ? + (NonSeparableBlendMode) blendMode : null; + + boolean needsColorConversion = !srcColorSpace.equals(dstColorSpace); + + Object srcPixel = null; + Object dstPixel = null; + float[] srcComponents = new float[numSrcComponents]; + // PDFBOX-3501 let getNormalizedComponents allocate to avoid + // ArrayIndexOutOfBoundsException for bitonal target + float[] dstComponents = null; + + float[] srcColor = new float[numSrcColorComponents]; + float[] srcConverted; + float[] dstConverted; + float[] rgbResult = blendModeIsSeparable ? null : new float[dstHasAlpha ? 4 : 3]; + + for (int y = y0; y < y1; y++) + { + for (int x = x0; x < x1; x++) + { + srcPixel = src.getDataElements(x, y, srcPixel); + dstPixel = dstIn.getDataElements(dstInXShift + x, dstInYShift + y, dstPixel); + + srcComponents = srcColorModel.getNormalizedComponents(srcPixel, srcComponents, + 0); + dstComponents = dstColorModel.getNormalizedComponents(dstPixel, dstComponents, + 0); + + float srcAlpha = srcHasAlpha ? srcComponents[numSrcColorComponents] : 1.0f; + float dstAlpha = dstHasAlpha ? dstComponents[numDstColorComponents] : 1.0f; + + srcAlpha = srcAlpha * constantAlpha; + + float resultAlpha = dstAlpha + srcAlpha - srcAlpha * dstAlpha; + float srcAlphaRatio = (resultAlpha > 0) ? srcAlpha / resultAlpha : 0; + + if (separableBlendMode != null) + { + // convert color + System.arraycopy(srcComponents, 0, srcColor, 0, numSrcColorComponents); + if (needsColorConversion) + { + // TODO - very very slow - Hash results??? + float[] cieXYZ = srcColorSpace.toCIEXYZ(srcColor); + srcConverted = dstColorSpace.fromCIEXYZ(cieXYZ); + } + else + { + srcConverted = srcColor; + } + + for (int k = 0; k < numDstColorComponents; k++) + { + float srcValue = srcConverted[k]; + float dstValue = dstComponents[k]; + + if (subtractive) + { + srcValue = 1 - srcValue; + dstValue = 1 - dstValue; + } + + float value = separableBlendMode.blendChannel(srcValue, dstValue); + value = srcValue + dstAlpha * (value - srcValue); + value = dstValue + srcAlphaRatio * (value - dstValue); + + if (subtractive) + { + value = 1 - value; + } + + dstComponents[k] = value; + } + } + else + { + // Nonseparable blend modes are computed in RGB color space. + // TODO - CMYK color spaces need special treatment. + + if (srcColorSpaceType == ColorSpace.TYPE_RGB) + { + srcConverted = srcComponents; + } + else + { + srcConverted = srcColorSpace.toRGB(srcComponents); + } + + if (dstColorSpaceType == ColorSpace.TYPE_RGB) + { + dstConverted = dstComponents; + } + else + { + dstConverted = dstColorSpace.toRGB(dstComponents); + } + + nonSeparableBlendMode.blend(srcConverted, dstConverted, rgbResult); + + for (int k = 0; k < 3; k++) + { + float srcValue = srcConverted[k]; + float dstValue = dstConverted[k]; + float value = rgbResult[k]; + value = Math.max(Math.min(value, 1.0f), 0.0f); + value = srcValue + dstAlpha * (value - srcValue); + value = dstValue + srcAlphaRatio * (value - dstValue); + rgbResult[k] = value; + } + + if (dstColorSpaceType == ColorSpace.TYPE_RGB) + { + System.arraycopy(rgbResult, 0, dstComponents, 0, dstComponents.length); + } + else + { + float[] temp = dstColorSpace.fromRGB(rgbResult); + System.arraycopy(temp, 0, dstComponents, 0, + Math.min(dstComponents.length, temp.length)); + } + } + + if (dstHasAlpha) + { + dstComponents[numDstColorComponents] = resultAlpha; + } + + dstPixel = dstColorModel.getDataElements(dstComponents, 0, dstPixel); + dstOut.setDataElements(dstOutXShift + x, dstOutYShift + y, dstPixel); + } + } + } + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendMode.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendMode.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendMode.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendMode.java 2018-11-28 17:18:36.000000000 +0000 @@ -30,39 +30,6 @@ */ public abstract class BlendMode { - /** - * Determines the blend mode from the BM entry in the COS ExtGState. - * - * @param cosBlendMode name or array - * @return blending mode - */ - public static BlendMode getInstance(COSBase cosBlendMode) - { - BlendMode result = null; - if (cosBlendMode instanceof COSName) - { - result = BLEND_MODES.get(cosBlendMode); - } - else if (cosBlendMode instanceof COSArray) - { - COSArray cosBlendModeArray = (COSArray) cosBlendMode; - for (int i = 0; i < cosBlendModeArray.size(); i++) - { - result = BLEND_MODES.get(cosBlendModeArray.getObject(i)); - if (result != null) - { - break; - } - } - } - - if (result != null) - { - return result; - } - return BlendMode.COMPATIBLE; - } - public static final SeparableBlendMode NORMAL = new SeparableBlendMode() { @Override @@ -202,14 +169,215 @@ } }; - // this map *must* come after the declarations above, otherwise its values will be null + public static final NonSeparableBlendMode HUE = new NonSeparableBlendMode() + { + @Override + public void blend(float[] srcValues, float[] dstValues, float[] result) + { + float[] temp = new float[3]; + getSaturationRGB(dstValues, srcValues, temp); + getLuminosityRGB(dstValues, temp, result); + } + }; + + public static final NonSeparableBlendMode SATURATION = new NonSeparableBlendMode() + { + @Override + public void blend(float[] srcValues, float[] dstValues, float[] result) + { + getSaturationRGB(srcValues, dstValues, result); + } + }; + + public static final NonSeparableBlendMode COLOR = new NonSeparableBlendMode() + { + @Override + public void blend(float[] srcValues, float[] dstValues, float[] result) + { + getLuminosityRGB(dstValues, srcValues, result); + } + }; + + public static final NonSeparableBlendMode LUMINOSITY = new NonSeparableBlendMode() + { + @Override + public void blend(float[] srcValues, float[] dstValues, float[] result) + { + getLuminosityRGB(srcValues, dstValues, result); + } + }; + + // these maps *must* come after the BlendMode.* constant declarations, otherwise their values would be null private static final Map BLEND_MODES = createBlendModeMap(); + BlendMode() + { + } + + /** + * Determines the blend mode from the BM entry in the COS ExtGState. + * + * @param cosBlendMode name or array + * @return blending mode + */ + public static BlendMode getInstance(COSBase cosBlendMode) + { + BlendMode result = null; + if (cosBlendMode instanceof COSName) + { + result = BLEND_MODES.get(cosBlendMode); + } + else if (cosBlendMode instanceof COSArray) + { + COSArray cosBlendModeArray = (COSArray) cosBlendMode; + for (int i = 0; i < cosBlendModeArray.size(); i++) + { + result = BLEND_MODES.get(cosBlendModeArray.getObject(i)); + if (result != null) + { + break; + } + } + } + + if (result != null) + { + return result; + } + return BlendMode.NORMAL; + } + + private static int get255Value(float val) + { + return (int) Math.floor(val >= 1.0 ? 255 : val * 255.0); + } + + private static void getSaturationRGB(float[] srcValues, float[] dstValues, float[] result) + { + int minb; + int maxb; + int mins; + int maxs; + int y; + int scale; + int r; + int g; + int b; + + int rd = get255Value(dstValues[0]); + int gd = get255Value(dstValues[1]); + int bd = get255Value(dstValues[2]); + int rs = get255Value(srcValues[0]); + int gs = get255Value(srcValues[1]); + int bs = get255Value(srcValues[2]); + + minb = Math.min(rd, Math.min(gd, bd)); + maxb = Math.max(rd, Math.max(gd, bd)); + if (minb == maxb) + { + /* backdrop has zero saturation, avoid divide by 0 */ + result[0] = gd / 255.0f; + result[1] = gd / 255.0f; + result[2] = gd / 255.0f; + return; + } + + mins = Math.min(rs, Math.min(gs, bs)); + maxs = Math.max(rs, Math.max(gs, bs)); + + scale = ((maxs - mins) << 16) / (maxb - minb); + y = (rd * 77 + gd * 151 + bd * 28 + 0x80) >> 8; + r = y + ((((rd - y) * scale) + 0x8000) >> 16); + g = y + ((((gd - y) * scale) + 0x8000) >> 16); + b = y + ((((bd - y) * scale) + 0x8000) >> 16); + + if (((r | g | b) & 0x100) == 0x100) + { + int scalemin; + int scalemax; + int min; + int max; + + min = Math.min(r, Math.min(g, b)); + max = Math.max(r, Math.max(g, b)); + + if (min < 0) + { + scalemin = (y << 16) / (y - min); + } + else + { + scalemin = 0x10000; + } + + if (max > 255) + { + scalemax = ((255 - y) << 16) / (max - y); + } + else + { + scalemax = 0x10000; + } + + scale = Math.min(scalemin, scalemax); + r = y + (((r - y) * scale + 0x8000) >> 16); + g = y + (((g - y) * scale + 0x8000) >> 16); + b = y + (((b - y) * scale + 0x8000) >> 16); + } + result[0] = r / 255.0f; + result[1] = g / 255.0f; + result[2] = b / 255.0f; + } + + private static void getLuminosityRGB(float[] srcValues, float[] dstValues, float[] result) + { + int delta; + int scale; + int r; + int g; + int b; + int y; + int rd = get255Value(dstValues[0]); + int gd = get255Value(dstValues[1]); + int bd = get255Value(dstValues[2]); + int rs = get255Value(srcValues[0]); + int gs = get255Value(srcValues[1]); + int bs = get255Value(srcValues[2]); + delta = ((rs - rd) * 77 + (gs - gd) * 151 + (bs - bd) * 28 + 0x80) >> 8; + r = rd + delta; + g = gd + delta; + b = bd + delta; + + if (((r | g | b) & 0x100) == 0x100) + { + y = (rs * 77 + gs * 151 + bs * 28 + 0x80) >> 8; + if (delta > 0) + { + int max; + max = Math.max(r, Math.max(g, b)); + scale = max == y ? 0 : ((255 - y) << 16) / (max - y); + } + else + { + int min; + min = Math.min(r, Math.min(g, b)); + scale = y == min ? 0 : (y << 16) / (y - min); + } + r = y + (((r - y) * scale + 0x8000) >> 16); + g = y + (((g - y) * scale + 0x8000) >> 16); + b = y + (((b - y) * scale + 0x8000) >> 16); + } + result[0] = r / 255.0f; + result[1] = g / 255.0f; + result[2] = b / 255.0f; + } + private static Map createBlendModeMap() { Map map = new HashMap(13); map.put(COSName.NORMAL, BlendMode.NORMAL); - map.put(COSName.COMPATIBLE, BlendMode.COMPATIBLE); + // BlendMode.COMPATIBLE should not be used + map.put(COSName.COMPATIBLE, BlendMode.NORMAL); map.put(COSName.MULTIPLY, BlendMode.MULTIPLY); map.put(COSName.SCREEN, BlendMode.SCREEN); map.put(COSName.OVERLAY, BlendMode.OVERLAY); @@ -221,11 +389,10 @@ map.put(COSName.SOFT_LIGHT, BlendMode.SOFT_LIGHT); map.put(COSName.DIFFERENCE, BlendMode.DIFFERENCE); map.put(COSName.EXCLUSION, BlendMode.EXCLUSION); - // TODO - non-separable blending modes + map.put(COSName.HUE, BlendMode.HUE); + map.put(COSName.SATURATION, BlendMode.SATURATION); + map.put(COSName.LUMINOSITY, BlendMode.LUMINOSITY); + map.put(COSName.COLOR, BlendMode.COLOR); return map; } - - BlendMode() - { - } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDColor.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDColor.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDColor.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDColor.java 2018-11-28 17:18:36.000000000 +0000 @@ -113,7 +113,15 @@ */ public float[] getComponents() { - return components.clone(); + if (colorSpace instanceof PDPattern || colorSpace == null) + { + // colorspace of the pattern color isn't known, so just clone + // null colorspace can happen with empty annotation color + // see PDFBOX-3351-538928-p4.pdf + return components.clone(); + } + // PDFBOX-4279: copyOf instead of clone in case array is too small + return Arrays.copyOf(components, colorSpace.getNumberOfComponents()); } /** diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDDeviceRGB.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDDeviceRGB.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDDeviceRGB.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDDeviceRGB.java 2018-11-28 17:18:38.000000000 +0000 @@ -20,10 +20,7 @@ import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.io.IOException; -import java.util.StringTokenizer; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.apache.pdfbox.cos.COSName; /** @@ -41,8 +38,6 @@ private final PDColor initialColor = new PDColor(new float[] { 0, 0, 0 }, this); private volatile ColorSpace awtColorSpace; - private static final Log LOG = LogFactory.getLog(PDDeviceRGB.class); - private PDDeviceRGB() { } @@ -58,8 +53,6 @@ return; } - suggestKCMS(); - synchronized (this) { // we might have been waiting for another thread, so check again @@ -124,47 +117,4 @@ image.setData(raster); return image; } - - private static void suggestKCMS() - { - String cmmProperty = System.getProperty("sun.java2d.cmm"); - if (isMinJdk8() && !"sun.java2d.cmm.kcms.KcmsServiceProvider".equals(cmmProperty)) - { - try - { - // Make sure that class exists - Class.forName("sun.java2d.cmm.kcms.KcmsServiceProvider"); - - LOG.info("To get higher rendering speed on JDK8 or later,"); - LOG.info(" use the option -Dsun.java2d.cmm=sun.java2d.cmm.kcms.KcmsServiceProvider"); - LOG.info(" or call System.setProperty(\"sun.java2d.cmm\", \"sun.java2d.cmm.kcms.KcmsServiceProvider\")"); - } - catch (ClassNotFoundException e) - { - LOG.debug("KCMS doesn't exist anymore. SO SAD!"); - } - } - } - - private static boolean isMinJdk8() - { - // strategy from lucene-solr/lucene/core/src/java/org/apache/lucene/util/Constants.java - String version = System.getProperty("java.specification.version"); - final StringTokenizer st = new StringTokenizer(version, "."); - try - { - int major = Integer.parseInt(st.nextToken()); - int minor = 0; - if (st.hasMoreTokens()) - { - minor = Integer.parseInt(st.nextToken()); - } - return major > 1 || (major == 1 && minor >= 8); - } - catch (NumberFormatException nfe) - { - // maybe some new numbering scheme in the 22nd century - return true; - } - } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDICCBased.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDICCBased.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDICCBased.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDICCBased.java 2018-11-28 17:18:38.000000000 +0000 @@ -63,6 +63,10 @@ private ICC_ColorSpace awtColorSpace; private PDColor initialColor; private boolean isRGB = false; + // allows to force using alternate color space instead of ICC color space for performance + // reasons with LittleCMS (LCMS), see PDFBOX-4309 + // WARNING: do not activate this in a conforming reader + private boolean useOnlyAlternateColorSpace = false; /** * Creates a new ICC color space with an empty stream. @@ -93,6 +97,8 @@ { throw new IOException("ICCBased colorspace array must have a stream as second element"); } + useOnlyAlternateColorSpace = System + .getProperty("org.apache.pdfbox.rendering.UseAlternateInsteadOfICCColorSpace") != null; array = iccArray; stream = new PDStream((COSStream) iccArray.getObject(1)); loadICCProfile(); @@ -118,6 +124,18 @@ */ private void loadICCProfile() throws IOException { + if (useOnlyAlternateColorSpace) + { + try + { + fallbackToAlternateColorSpace(null); + return; + } + catch ( IOException e ) + { + LOG.warn("Error initializing alternate color space: " + e.getLocalizedMessage()); + } + } InputStream input = null; try { @@ -161,28 +179,25 @@ Transparency.OPAQUE, DataBuffer.TYPE_BYTE); } } - catch (RuntimeException e) + catch (ProfileDataException e) { - if (e instanceof ProfileDataException || - e instanceof CMMException || - e instanceof IllegalArgumentException || - e instanceof ArrayIndexOutOfBoundsException) - { - // fall back to alternateColorSpace color space - awtColorSpace = null; - alternateColorSpace = getAlternateColorSpace(); - if (alternateColorSpace.equals(PDDeviceRGB.INSTANCE)) - { - isRGB = true; - } - LOG.warn("Can't read embedded ICC profile (" + e.getLocalizedMessage() + - "), using alternate color space: " + alternateColorSpace.getName()); - initialColor = alternateColorSpace.getInitialColor(); - } - else - { - throw e; - } + fallbackToAlternateColorSpace(e); + } + catch (CMMException e) + { + fallbackToAlternateColorSpace(e); + } + catch (IllegalArgumentException e) + { + fallbackToAlternateColorSpace(e); + } + catch (ArrayIndexOutOfBoundsException e) + { + fallbackToAlternateColorSpace(e); + } + catch (IOException e) + { + fallbackToAlternateColorSpace(e); } finally { @@ -190,6 +205,22 @@ } } + private void fallbackToAlternateColorSpace(Exception e) throws IOException + { + awtColorSpace = null; + alternateColorSpace = getAlternateColorSpace(); + if (alternateColorSpace.equals(PDDeviceRGB.INSTANCE)) + { + isRGB = true; + } + if ( e != null ) + { + LOG.warn("Can't read embedded ICC profile (" + e.getLocalizedMessage() + + "), using alternate color space: " + alternateColorSpace.getName()); + } + initialColor = alternateColorSpace.getInitialColor(); + } + /** * Returns true if the given profile is represents sRGB. */ diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDFormXObject.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDFormXObject.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDFormXObject.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDFormXObject.java 2018-11-28 17:18:36.000000000 +0000 @@ -153,11 +153,18 @@ @Override public PDResources getResources() { - COSDictionary resources = (COSDictionary) getCOSObject().getDictionaryObject(COSName.RESOURCES); + COSDictionary resources = getCOSObject().getCOSDictionary(COSName.RESOURCES); if (resources != null) { return new PDResources(resources, cache); } + if (getCOSObject().containsKey(COSName.RESOURCES)) + { + // PDFBOX-4372 if the resource key exists but has nothing, return empty resources, + // to avoid a self-reference (xobject form Fm0 contains "/Fm0 Do") + // See also the mention of PDFBOX-1359 in PDFStreamEngine + return new PDResources(); + } return null; } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java 2018-11-28 17:18:36.000000000 +0000 @@ -33,6 +33,7 @@ import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.metadata.IIOMetadata; import javax.imageio.plugins.jpeg.JPEGImageWriteParam; @@ -200,7 +201,9 @@ * The image will be created with a dpi value of 72 to be stored in metadata. * @param document the document where the image will be created * @param image the BufferedImage to embed - * @param quality the desired JPEG compression quality + * @param quality The desired JPEG compression quality; between 0 (best + * compression) and 1 (best image quality). See + * {@link ImageWriteParam#setCompressionQuality(float)} for more details. * @return a new Image XObject * @throws IOException if the JPEG data cannot be written */ @@ -219,7 +222,9 @@ * * @param document the document where the image will be created * @param image the BufferedImage to embed - * @param quality the desired JPEG compression quality + * @param quality The desired JPEG compression quality; between 0 (best + * compression) and 1 (best image quality). See + * {@link ImageWriteParam#setCompressionQuality(float)} for more details. * @param dpi the desired dpi (resolution) value of the JPEG to be stored in metadata. This * value has no influence on image content or size. * @return a new Image XObject diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/LosslessFactory.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/LosslessFactory.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/LosslessFactory.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/LosslessFactory.java 2018-11-28 17:18:36.000000000 +0000 @@ -16,21 +16,32 @@ package org.apache.pdfbox.pdmodel.graphics.image; import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; import java.awt.image.BufferedImage; -import java.awt.image.WritableRaster; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; import javax.imageio.stream.MemoryCacheImageOutputStream; import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSInteger; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.filter.Filter; import org.apache.pdfbox.filter.FilterFactory; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceCMYK; import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceColorSpace; import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray; import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB; +import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased; /** * Factory for creating a PDImageXObject containing a lossless compressed image. @@ -39,209 +50,174 @@ */ public final class LosslessFactory { + /** + * Internal, only for benchmark purpose + */ + static boolean usePredictorEncoder = true; + private LosslessFactory() { } - + /** - * Creates a new lossless encoded Image XObject from a Buffered Image. + * Creates a new lossless encoded image XObject from a BufferedImage. + *

+ * New for advanced users from 2.0.12 on:
+ * If you created your image with a non standard ICC colorspace, it will be + * preserved. (If you load images in java using ImageIO then no need to read + * this segment) However a new colorspace will be created for each image. So + * if you create a PDF with several such images, consider replacing the + * colorspace with a common object to save space. This is done with + * {@link PDImageXObject#getColorSpace()} and + * {@link PDImageXObject#setColorSpace(org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace) PDImageXObject.setColorSpace()} * * @param document the document where the image will be created - * @param image the buffered image to embed - * @return a new Image XObject + * @param image the BufferedImage to embed + * @return a new image XObject * @throws IOException if something goes wrong */ public static PDImageXObject createFromImage(PDDocument document, BufferedImage image) throws IOException { - int bpc; - PDDeviceColorSpace deviceColorSpace; - - int height = image.getHeight(); - int width = image.getWidth(); - int[] rgbLineBuffer = new int[width]; - byte[] imageData; - if ((image.getType() == BufferedImage.TYPE_BYTE_GRAY && image.getColorModel().getPixelSize() <= 8) || (image.getType() == BufferedImage.TYPE_BYTE_BINARY && image.getColorModel().getPixelSize() == 1)) { - // grayscale images need one color per sample - bpc = image.getColorModel().getPixelSize(); - deviceColorSpace = PDDeviceGray.INSTANCE; - - ByteArrayOutputStream bos = new ByteArrayOutputStream((width*bpc/8)+(width*bpc%8 != 0 ? 1:0)*height); - MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(bos); - - for (int y = 0; y < height; ++y) - { - for (int pixel : image.getRGB(0, y, width, 1, rgbLineBuffer, 0, width)) - { - mcios.writeBits(pixel & 0xFF, bpc); - } - - int bitOffset = mcios.getBitOffset(); - if (bitOffset != 0) - { - mcios.writeBits(0, 8-bitOffset); - } - } - mcios.flush(); - mcios.close(); - - imageData = bos.toByteArray(); + return createFromGrayImage(image, document); } else { - // RGB - bpc = 8; - deviceColorSpace = PDDeviceRGB.INSTANCE; - imageData = new byte[width*height*3]; - int byteIdx = 0; - - for (int y = 0; y < height; ++y) + // We try to encode the image with predictor + if (usePredictorEncoder) { - for (int pixel : image.getRGB(0, y, width, 1, rgbLineBuffer, 0, width)) + PDImageXObject pdImageXObject = new PredictorEncoder(document, image).encode(); + if (pdImageXObject != null) { - imageData[byteIdx++] = (byte)((pixel >> 16) & 0xFF); - imageData[byteIdx++] = (byte)((pixel >> 8) & 0xFF); - imageData[byteIdx++] = (byte)(pixel & 0xFF); + if (pdImageXObject.getColorSpace() == PDDeviceRGB.INSTANCE && + pdImageXObject.getBitsPerComponent() < 16 && + image.getWidth() * image.getHeight() <= 50 * 50) + { + // also create classic compressed image, compare sizes + PDImageXObject pdImageXObjectClassic = createFromRGBImage(image, document); + if (pdImageXObjectClassic.getCOSObject().getLength() < + pdImageXObject.getCOSObject().getLength()) + { + pdImageXObject.getCOSObject().close(); + return pdImageXObjectClassic; + } + else + { + pdImageXObjectClassic.getCOSObject().close(); + } + } + return pdImageXObject; } } - } - - PDImageXObject pdImage = prepareImageXObject(document, imageData, - image.getWidth(), image.getHeight(), bpc, deviceColorSpace); - // alpha -> soft mask - PDImage xAlpha = createAlphaFromARGBImage(document, image); - if (xAlpha != null) - { - pdImage.getCOSObject().setItem(COSName.SMASK, xAlpha); + // Fallback: We export the image as 8-bit sRGB and might loose color information + return createFromRGBImage(image, document); } - - return pdImage; } - /** - * Creates a grayscale Flate encoded PDImageXObject from the alpha channel - * of an image. - * - * @param document the document where the image will be created. - * @param image an ARGB image. - * - * @return the alpha channel of an image as a grayscale image. - * - * @throws IOException if something goes wrong - */ - private static PDImageXObject createAlphaFromARGBImage(PDDocument document, BufferedImage image) + // grayscale images need one color per sample + private static PDImageXObject createFromGrayImage(BufferedImage image, PDDocument document) throws IOException { - // this implementation makes the assumption that the raster values can be used 1:1 for - // the stream. - // Sadly the type of the databuffer is usually TYPE_INT and not TYPE_BYTE so we can't just - // save it directly - if (!image.getColorModel().hasAlpha()) - { - return null; - } - - // extract the alpha information - WritableRaster alphaRaster = image.getAlphaRaster(); - if (alphaRaster == null) - { - // happens sometimes (PDFBOX-2654) despite colormodel claiming to have alpha - return createAlphaFromARGBImage2(document, image); - } - - int[] pixels = alphaRaster.getPixels(0, 0, - alphaRaster.getWidth(), - alphaRaster.getHeight(), - (int[]) null); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - int bpc; - if (image.getTransparency() == Transparency.BITMASK) + int height = image.getHeight(); + int width = image.getWidth(); + int[] rgbLineBuffer = new int[width]; + int bpc = image.getColorModel().getPixelSize(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(((width*bpc/8)+(width*bpc%8 != 0 ? 1:0))*height); + MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(baos); + for (int y = 0; y < height; ++y) { - bpc = 1; - MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(bos); - int width = alphaRaster.getWidth(); - int p = 0; - for (int pixel : pixels) + for (int pixel : image.getRGB(0, y, width, 1, rgbLineBuffer, 0, width)) { - mcios.writeBit(pixel); - ++p; - if (p % width == 0) - { - while (mcios.getBitOffset() != 0) - { - mcios.writeBit(0); - } - } + mcios.writeBits(pixel & 0xFF, bpc); } - mcios.flush(); - mcios.close(); - } - else - { - bpc = 8; - for (int pixel : pixels) + + int bitOffset = mcios.getBitOffset(); + if (bitOffset != 0) { - bos.write(pixel); + mcios.writeBits(0, 8 - bitOffset); } } - - PDImageXObject pdImage = prepareImageXObject(document, bos.toByteArray(), + mcios.flush(); + mcios.close(); + return prepareImageXObject(document, baos.toByteArray(), image.getWidth(), image.getHeight(), bpc, PDDeviceGray.INSTANCE); - - return pdImage; } - // create alpha image the hard way: get the alpha through getRGB() - private static PDImageXObject createAlphaFromARGBImage2(PDDocument document, BufferedImage bi) - throws IOException + private static PDImageXObject createFromRGBImage(BufferedImage image, PDDocument document) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - int bpc; - if (bi.getTransparency() == Transparency.BITMASK) + int height = image.getHeight(); + int width = image.getWidth(); + int[] rgbLineBuffer = new int[width]; + int bpc = 8; + PDDeviceColorSpace deviceColorSpace = PDDeviceRGB.INSTANCE; + byte[] imageData = new byte[width * height * 3]; + int byteIdx = 0; + int alphaByteIdx = 0; + int alphaBitPos = 7; + int transparency = image.getTransparency(); + int apbc = transparency == Transparency.BITMASK ? 1 : 8; + byte[] alphaImageData; + if (transparency != Transparency.OPAQUE) { - bpc = 1; - MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(bos); - for (int y = 0, h = bi.getHeight(); y < h; ++y) - { - for (int x = 0, w = bi.getWidth(); x < w; ++x) - { - int alpha = bi.getRGB(x, y) >>> 24; - mcios.writeBit(alpha); - } - while (mcios.getBitOffset() != 0) - { - mcios.writeBit(0); - } - } - mcios.flush(); - mcios.close(); + alphaImageData = new byte[((width * apbc / 8) + (width * apbc % 8 != 0 ? 1 : 0)) * height]; } else { - bpc = 8; - for (int y = 0, h = bi.getHeight(); y < h; ++y) + alphaImageData = new byte[0]; + } + for (int y = 0; y < height; ++y) + { + for (int pixel : image.getRGB(0, y, width, 1, rgbLineBuffer, 0, width)) { - for (int x = 0, w = bi.getWidth(); x < w; ++x) + imageData[byteIdx++] = (byte) ((pixel >> 16) & 0xFF); + imageData[byteIdx++] = (byte) ((pixel >> 8) & 0xFF); + imageData[byteIdx++] = (byte) (pixel & 0xFF); + if (transparency != Transparency.OPAQUE) { - int alpha = bi.getRGB(x, y) >>> 24; - bos.write(alpha); + // we have the alpha right here, so no need to do it separately + // as done prior April 2018 + if (transparency == Transparency.BITMASK) + { + // write a bit + alphaImageData[alphaByteIdx] |= ((pixel >> 24) & 1) << alphaBitPos; + if (--alphaBitPos < 0) + { + alphaBitPos = 7; + ++alphaByteIdx; + } + } + else + { + // write a byte + alphaImageData[alphaByteIdx++] = (byte) ((pixel >> 24) & 0xFF); + } } } - } - - PDImageXObject pdImage = prepareImageXObject(document, bos.toByteArray(), - bi.getWidth(), bi.getHeight(), bpc, PDDeviceGray.INSTANCE); + // skip boundary if needed + if (transparency == Transparency.BITMASK && alphaBitPos != 7) + { + alphaBitPos = 7; + ++alphaByteIdx; + } + } + PDImageXObject pdImage = prepareImageXObject(document, imageData, + image.getWidth(), image.getHeight(), bpc, deviceColorSpace); + if (transparency != Transparency.OPAQUE) + { + PDImageXObject pdMask = prepareImageXObject(document, alphaImageData, + image.getWidth(), image.getHeight(), apbc, PDDeviceGray.INSTANCE); + pdImage.getCOSObject().setItem(COSName.SMASK, pdMask); + } return pdImage; - } + } /** - * Create a PDImageXObject while making a decision whether not to - * compress, use Flate filter only, or Flate and LZW filters. + * Create a PDImageXObject using the Flate filter. * * @param document The document. * @param byteArray array with data. @@ -267,4 +243,460 @@ width, height, bitsPerComponent, initColorSpace); } + private static class PredictorEncoder + { + private final PDDocument document; + private final BufferedImage image; + private final int componentsPerPixel; + private final int transferType; + private final int bytesPerComponent; + private final int bytesPerPixel; + + private final int height; + private final int width; + + private final byte[] dataRawRowNone; + private final byte[] dataRawRowSub; + private final byte[] dataRawRowUp; + private final byte[] dataRawRowAverage; + private final byte[] dataRawRowPaeth; + + final int imageType; + final boolean hasAlpha; + final byte[] alphaImageData; + + final byte[] aValues; + final byte[] cValues; + final byte[] bValues; + final byte[] xValues; + final byte[] tmpResultValues; + + /** + * Initialize the encoder and set all final fields + */ + PredictorEncoder(PDDocument document, BufferedImage image) + { + this.document = document; + this.image = image; + + // The raw count of components per pixel including optional alpha + this.componentsPerPixel = image.getColorModel().getNumComponents(); + this.transferType = image.getRaster().getTransferType(); + this.bytesPerComponent = (transferType == DataBuffer.TYPE_SHORT + || transferType == DataBuffer.TYPE_USHORT) ? 2 : 1; + + // Only the bytes we need in the output (excluding alpha) + this.bytesPerPixel = image.getColorModel().getNumColorComponents() * bytesPerComponent; + + this.height = image.getHeight(); + this.width = image.getWidth(); + this.imageType = image.getType(); + this.hasAlpha = image.getColorModel().getNumComponents() != image.getColorModel() + .getNumColorComponents(); + this.alphaImageData = hasAlpha ? new byte[width * height * bytesPerComponent] : null; + + // The rows have 1-byte encoding marker and width * BYTES_PER_PIXEL pixel-bytes + int dataRowByteCount = width * bytesPerPixel + 1; + this.dataRawRowNone = new byte[dataRowByteCount]; + this.dataRawRowSub = new byte[dataRowByteCount]; + this.dataRawRowUp = new byte[dataRowByteCount]; + this.dataRawRowAverage = new byte[dataRowByteCount]; + this.dataRawRowPaeth = new byte[dataRowByteCount]; + + // Write the encoding markers + dataRawRowNone[0] = 0; + dataRawRowSub[0] = 1; + dataRawRowUp[0] = 2; + dataRawRowAverage[0] = 3; + dataRawRowPaeth[0] = 4; + + // c | b + // ----- + // a | x + // + // x => current pixel + this.aValues = new byte[bytesPerPixel]; + this.cValues = new byte[bytesPerPixel]; + this.bValues = new byte[bytesPerPixel]; + this.xValues = new byte[bytesPerPixel]; + this.tmpResultValues = new byte[bytesPerPixel]; + } + + /** + * Tries to compress the image using a predictor. + * + * @return the image or null if it is not possible to encoded the image (e.g. not supported + * raster format etc.) + */ + PDImageXObject encode() throws IOException + { + Raster imageRaster = image.getRaster(); + final int elementsInRowPerPixel; + + // These variables store a row of the image each, the exact type depends + // on the image encoding. Can be a int[], short[] or byte[] + Object prevRow; + Object transferRow; + + switch (imageType) + { + case BufferedImage.TYPE_CUSTOM: + { + switch (imageRaster.getTransferType()) + { + case DataBuffer.TYPE_USHORT: + elementsInRowPerPixel = componentsPerPixel; + prevRow = new short[width * elementsInRowPerPixel]; + transferRow = new short[width * elementsInRowPerPixel]; + break; + case DataBuffer.TYPE_BYTE: + elementsInRowPerPixel = componentsPerPixel; + prevRow = new byte[width * elementsInRowPerPixel]; + transferRow = new byte[width * elementsInRowPerPixel]; + break; + default: + return null; + } + break; + } + + case BufferedImage.TYPE_3BYTE_BGR: + case BufferedImage.TYPE_4BYTE_ABGR: + { + elementsInRowPerPixel = componentsPerPixel; + prevRow = new byte[width * elementsInRowPerPixel]; + transferRow = new byte[width * elementsInRowPerPixel]; + break; + } + + case BufferedImage.TYPE_INT_BGR: + case BufferedImage.TYPE_INT_ARGB: + case BufferedImage.TYPE_INT_RGB: + { + elementsInRowPerPixel = 1; + prevRow = new int[width * elementsInRowPerPixel]; + transferRow = new int[width * elementsInRowPerPixel]; + break; + } + + default: + // We can not handle this unknown format + return null; + } + + final int elementsInTransferRow = width * elementsInRowPerPixel; + + // pre-size the output stream to half of the maximum size + ByteArrayOutputStream stream = new ByteArrayOutputStream( + height * width * bytesPerPixel / 2); + Deflater deflater = new Deflater(Filter.getCompressionLevel()); + DeflaterOutputStream zip = new DeflaterOutputStream(stream, deflater); + + int alphaPtr = 0; + + for (int rowNum = 0; rowNum < height; rowNum++) + { + imageRaster.getDataElements(0, rowNum, width, 1, transferRow); + + // We start to write at index one, as the predictor marker is in index zero + int writerPtr = 1; + Arrays.fill(aValues, (byte) 0); + Arrays.fill(cValues, (byte) 0); + + final byte[] transferRowByte; + final byte[] prevRowByte; + final int[] transferRowInt; + final int[] prevRowInt; + final short[] transferRowShort; + final short[] prevRowShort; + + if (transferRow instanceof byte[]) + { + transferRowByte = (byte[]) transferRow; + prevRowByte = (byte[]) prevRow; + transferRowInt = prevRowInt = null; + transferRowShort = prevRowShort = null; + } + else if (transferRow instanceof int[]) + { + transferRowInt = (int[]) transferRow; + prevRowInt = (int[]) prevRow; + transferRowShort = prevRowShort = null; + transferRowByte = prevRowByte = null; + } + else + { + // This must be short[] + transferRowShort = (short[]) transferRow; + prevRowShort = (short[]) prevRow; + transferRowInt = prevRowInt = null; + transferRowByte = prevRowByte = null; + } + + for (int indexInTransferRow = 0; indexInTransferRow < elementsInTransferRow; + indexInTransferRow += elementsInRowPerPixel, alphaPtr += bytesPerComponent) + { + // Copy the pixel values into the byte array + if (transferRowByte != null) + { + copyImageBytes(transferRowByte, indexInTransferRow, xValues, alphaImageData, + alphaPtr); + copyImageBytes(prevRowByte, indexInTransferRow, bValues, null, 0); + } + else if (transferRowInt != null) + { + copyIntToBytes(transferRowInt, indexInTransferRow, xValues, alphaImageData, + alphaPtr); + copyIntToBytes(prevRowInt, indexInTransferRow, bValues, null, 0); + } + else + { + // This must be short[] + copyShortsToBytes(transferRowShort, indexInTransferRow, xValues, alphaImageData, alphaPtr); + copyShortsToBytes(prevRowShort, indexInTransferRow, bValues, null, 0); + } + + // Encode the pixel values in the different encodings + int length = xValues.length; + for (int bytePtr = 0; bytePtr < length; bytePtr++) + { + int x = xValues[bytePtr] & 0xFF; + int a = aValues[bytePtr] & 0xFF; + int b = bValues[bytePtr] & 0xFF; + int c = cValues[bytePtr] & 0xFF; + dataRawRowNone[writerPtr] = (byte) x; + dataRawRowSub[writerPtr] = pngFilterSub(x, a); + dataRawRowUp[writerPtr] = pngFilterUp(x, b); + dataRawRowAverage[writerPtr] = pngFilterAverage(x, a, b); + dataRawRowPaeth[writerPtr] = pngFilterPaeth(x, a, b, c); + writerPtr++; + } + + // We shift the values into the prev / upper left values for the next pixel + System.arraycopy(xValues, 0, aValues, 0, bytesPerPixel); + System.arraycopy(bValues, 0, cValues, 0, bytesPerPixel); + } + + byte[] rowToWrite = chooseDataRowToWrite(); + + // Write and compress the row as long it is hot (CPU cache wise) + zip.write(rowToWrite, 0, rowToWrite.length); + + // We swap prev and transfer row, so that we have the prev row for the next row. + Object temp = prevRow; + prevRow = transferRow; + transferRow = temp; + } + zip.close(); + deflater.end(); + + return preparePredictorPDImage(stream, bytesPerComponent * 8); + } + + private void copyIntToBytes(int[] transferRow, int indexInTranferRow, byte[] targetValues, + byte[] alphaImageData, int alphaPtr) + { + int val = transferRow[indexInTranferRow]; + byte b0 = (byte) (val & 0xFF); + byte b1 = (byte) ((val >> 8) & 0xFF); + byte b2 = (byte) ((val >> 16) & 0xFF); + + switch (imageType) + { + case BufferedImage.TYPE_INT_BGR: + targetValues[0] = b0; + targetValues[1] = b1; + targetValues[2] = b2; + break; + case BufferedImage.TYPE_INT_ARGB: + targetValues[0] = b2; + targetValues[1] = b1; + targetValues[2] = b0; + if (alphaImageData != null) + { + byte b3 = (byte) ((val >> 24) & 0xFF); + alphaImageData[alphaPtr] = b3; + } + break; + case BufferedImage.TYPE_INT_RGB: + targetValues[0] = b2; + targetValues[1] = b1; + targetValues[2] = b0; + break; + } + } + + private void copyImageBytes(byte[] transferRow, int indexInTranferRow, byte[] targetValues, + byte[] alphaImageData, int alphaPtr) + { + System.arraycopy(transferRow, indexInTranferRow, targetValues, 0, targetValues.length); + if (alphaImageData != null) + { + alphaImageData[alphaPtr] = transferRow[indexInTranferRow + targetValues.length]; + } + } + + private static void copyShortsToBytes(short[] transferRow, int indexInTranferRow, + byte[] targetValues, byte[] alphaImageData, int alphaPtr) + { + int itr = indexInTranferRow; + for (int i = 0; i < targetValues.length; i += 2) + { + short val = transferRow[itr++]; + targetValues[i] = (byte) ((val >> 8) & 0xFF); + targetValues[i + 1] = (byte) (val & 0xFF); + } + if (alphaImageData != null) + { + short alpha = transferRow[itr]; + alphaImageData[alphaPtr] = (byte) ((alpha >> 8) & 0xFF); + alphaImageData[alphaPtr + 1] = (byte) (alpha & 0xFF); + } + } + + private PDImageXObject preparePredictorPDImage(ByteArrayOutputStream stream, + int bitsPerComponent) throws IOException + { + int h = image.getHeight(); + int w = image.getWidth(); + + ColorSpace srcCspace = image.getColorModel().getColorSpace(); + PDColorSpace pdColorSpace = srcCspace.getType() != ColorSpace.TYPE_CMYK + ? PDDeviceRGB.INSTANCE : PDDeviceCMYK.INSTANCE; + + // Encode the image profile if the image has one + if (srcCspace instanceof ICC_ColorSpace) + { + ICC_Profile profile = ((ICC_ColorSpace) srcCspace).getProfile(); + // We only encode a color profile if it is not sRGB + if (profile != ICC_Profile.getInstance(ColorSpace.CS_sRGB)) + { + PDICCBased pdProfile = new PDICCBased(document); + OutputStream outputStream = pdProfile.getPDStream() + .createOutputStream(COSName.FLATE_DECODE); + outputStream.write(profile.getData()); + outputStream.close(); + pdProfile.getPDStream().getCOSObject().setInt(COSName.N, + srcCspace.getNumComponents()); + pdProfile.getPDStream().getCOSObject().setItem(COSName.ALTERNATE, + srcCspace.getType() == ColorSpace.TYPE_CMYK ? + COSName.DEVICECMYK : + COSName.DEVICERGB); + pdColorSpace = pdProfile; + } + } + + PDImageXObject imageXObject = new PDImageXObject(document, + new ByteArrayInputStream(stream.toByteArray()), COSName.FLATE_DECODE, w, + h, bitsPerComponent, pdColorSpace); + + COSDictionary decodeParms = new COSDictionary(); + decodeParms.setItem(COSName.BITS_PER_COMPONENT, COSInteger.get(bitsPerComponent)); + decodeParms.setItem(COSName.PREDICTOR, COSInteger.get(15)); + decodeParms.setItem(COSName.COLUMNS, COSInteger.get(w)); + decodeParms.setItem(COSName.COLORS, COSInteger.get(srcCspace.getNumComponents())); + imageXObject.getCOSObject().setItem(COSName.DECODE_PARMS, decodeParms); + + if (image.getTransparency() != Transparency.OPAQUE) + { + PDImageXObject pdMask = prepareImageXObject(document, alphaImageData, + image.getWidth(), image.getHeight(), 8 * bytesPerComponent, PDDeviceGray.INSTANCE); + imageXObject.getCOSObject().setItem(COSName.SMASK, pdMask); + } + return imageXObject; + } + + /** + * We look which row encoding is the "best" one, ie. has the lowest sum. We don't implement + * anything fancier to choose the right row encoding. This is just the recommend algorithm + * in the spec. The get the perfect encoding you would need to do a brute force check how + * all the different encoded rows compress in the zip stream together. You have would have + * to check 5*image-height permutations... + * + * @return the "best" row encoding of the row encodings + */ + private byte[] chooseDataRowToWrite() + { + byte[] rowToWrite = dataRawRowNone; + long estCompressSum = estCompressSum(dataRawRowNone); + long estCompressSumSub = estCompressSum(dataRawRowSub); + long estCompressSumUp = estCompressSum(dataRawRowUp); + long estCompressSumAvg = estCompressSum(dataRawRowAverage); + long estCompressSumPaeth = estCompressSum(dataRawRowPaeth); + if (estCompressSum > estCompressSumSub) + { + rowToWrite = dataRawRowSub; + estCompressSum = estCompressSumSub; + } + if (estCompressSum > estCompressSumUp) + { + rowToWrite = dataRawRowUp; + estCompressSum = estCompressSumUp; + } + if (estCompressSum > estCompressSumAvg) + { + rowToWrite = dataRawRowAverage; + estCompressSum = estCompressSumAvg; + } + if (estCompressSum > estCompressSumPaeth) + { + rowToWrite = dataRawRowPaeth; + } + return rowToWrite; + } + + /* + * PNG Filters, see https://www.w3.org/TR/PNG-Filters.html + */ + private static byte pngFilterSub(int x, int a) + { + return (byte) ((x & 0xFF) - (a & 0xFF)); + } + + private static byte pngFilterUp(int x, int b) + { + // Same as pngFilterSub, just called with the prior row + return pngFilterSub(x, b); + } + + private static byte pngFilterAverage(int x, int a, int b) + { + return (byte) (x - ((b + a) / 2)); + } + + private static byte pngFilterPaeth(int x, int a, int b, int c) + { + int p = a + b - c; + int pa = Math.abs(p - a); + int pb = Math.abs(p - b); + int pc = Math.abs(p - c); + final int pr; + if (pa <= pb && pa <= pc) + { + pr = a; + } + else if (pb <= pc) + { + pr = b; + } + else + { + pr = c; + } + + int r = x - pr; + return (byte) (r); + } + + private static long estCompressSum(byte[] dataRawRowSub) + { + long sum = 0; + for (byte aDataRawRowSub : dataRawRowSub) + { + // https://www.w3.org/TR/PNG-Encoders.html#E.Filter-selection + sum += Math.abs(aDataRawRowSub); + } + return sum; + } + } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java 2018-11-28 17:18:36.000000000 +0000 @@ -292,14 +292,25 @@ } if (fileType.equals(FileType.TIFF)) { - return CCITTFactory.createFromFile(doc, file); + try + { + return CCITTFactory.createFromFile(doc, file); + } + catch (IOException ex) + { + LOG.debug("Reading as TIFF failed, setting fileType to PNG", ex); + // Plan B: try reading with ImageIO + // common exception: + // First image in tiff is not CCITT T4 or T6 compressed + fileType = FileType.PNG; + } } if (fileType.equals(FileType.BMP) || fileType.equals(FileType.GIF) || fileType.equals(FileType.PNG)) { BufferedImage bim = ImageIO.read(file); return LosslessFactory.createFromImage(doc, bim); } - throw new IllegalArgumentException("Image type not supported: " + file.getName()); + throw new IllegalArgumentException("Image type " + fileType + " not supported: " + file.getName()); } /** @@ -340,7 +351,18 @@ } if (fileType.equals(FileType.TIFF)) { - return CCITTFactory.createFromByteArray(document, byteArray); + try + { + return CCITTFactory.createFromByteArray(document, byteArray); + } + catch (IOException ex) + { + LOG.debug("Reading as TIFF failed, setting fileType to PNG", ex); + // Plan B: try reading with ImageIO + // common exception: + // First image in tiff is not CCITT T4 or T6 compressed + fileType = FileType.PNG; + } } if (fileType.equals(FileType.BMP) || fileType.equals(FileType.GIF) || fileType.equals(FileType.PNG)) { @@ -348,7 +370,7 @@ BufferedImage bim = ImageIO.read(bais); return LosslessFactory.createFromImage(document, bim); } - throw new IllegalArgumentException("Image type not supported: " + name); + throw new IllegalArgumentException("Image type " + fileType + " not supported: " + name); } /** @@ -424,7 +446,8 @@ PDImageXObject softMask = getSoftMask(); if (softMask != null) { - image = applyMask(image, softMask.getOpaqueImage(), true); + float[] matte = extractMatte(softMask); + image = applyMask(image, softMask.getOpaqueImage(), true, matte); } else { @@ -432,7 +455,7 @@ PDImageXObject mask = getMask(); if (mask != null && mask.isStencil()) { - image = applyMask(image, mask.getOpaqueImage(), false); + image = applyMask(image, mask.getOpaqueImage(), false, null); } } @@ -447,6 +470,21 @@ return image; } + private float[] extractMatte(PDImageXObject softMask) throws IOException + { + COSBase base = softMask.getCOSObject().getItem(COSName.MATTE); + float[] matte = null; + if (base instanceof COSArray) + { + // PDFBOX-4267: process /Matte + // see PDF specification 1.7, 11.6.5.3 Soft-Mask Images + matte = ((COSArray) base).toFloatArray(); + // convert to RGB + matte = getColorSpace().toRGB(matte); + } + return matte; + } + /** * {@inheritDoc} * The returned images are not cached. @@ -474,7 +512,8 @@ // explicit mask: RGB + Binary -> ARGB // soft mask: RGB + Gray -> ARGB - private BufferedImage applyMask(BufferedImage image, BufferedImage mask, boolean isSoft) + private BufferedImage applyMask(BufferedImage image, BufferedImage mask, + boolean isSoft, float[] matte) throws IOException { if (mask == null) @@ -520,6 +559,12 @@ if (isSoft) { rgba[3] = alphaPixel[0]; + if (matte != null && Float.compare(alphaPixel[0], 0) != 0) + { + rgba[0] = clampColor(((rgba[0] / 255 - matte[0]) / (alphaPixel[0] / 255) + matte[0]) * 255); + rgba[1] = clampColor(((rgba[1] / 255 - matte[1]) / (alphaPixel[0] / 255) + matte[1]) * 255); + rgba[2] = clampColor(((rgba[2] / 255 - matte[2]) / (alphaPixel[0] / 255) + matte[2]) * 255); + } } else { @@ -533,6 +578,11 @@ return masked; } + private float clampColor(float color) + { + return color < 0 ? 0 : (color > 255 ? 255 : color); + } + /** * High-quality image scaling. */ @@ -540,10 +590,13 @@ { BufferedImage image2 = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = image2.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_BICUBIC); - g.setRenderingHint(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY); + if (getInterpolate()) + { + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + } g.drawImage(image, 0, 0, width, height, 0, 0, image.getWidth(), image.getHeight(), null); g.dispose(); return image2; diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDInlineImage.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDInlineImage.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDInlineImage.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDInlineImage.java 2018-11-28 17:18:36.000000000 +0000 @@ -346,13 +346,13 @@ @Override public BufferedImage getImage() throws IOException { - return SampledImageReader.getRGBImage(this, getColorKeyMask()); + return SampledImageReader.getRGBImage(this, null); } @Override public BufferedImage getImage(Rectangle region, int subsampling) throws IOException { - return SampledImageReader.getRGBImage(this, region, subsampling, getColorKeyMask()); + return SampledImageReader.getRGBImage(this, region, subsampling, null); } @Override @@ -370,7 +370,9 @@ * there is none. * * @return Mask Image XObject + * @deprecated inline images don't have a color key mask. */ + @Deprecated public COSArray getColorKeyMask() { COSBase mask = parameters.getDictionaryObject(COSName.IM, COSName.MASK); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/SampledImageReader.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/SampledImageReader.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/SampledImageReader.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/SampledImageReader.java 2018-11-28 17:18:36.000000000 +0000 @@ -1,636 +1,674 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.pdfbox.pdmodel.graphics.image; - -import java.awt.Graphics2D; -import java.awt.Paint; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.image.BufferedImage; -import java.awt.image.DataBuffer; -import java.awt.image.DataBufferByte; -import java.awt.image.Raster; -import java.awt.image.WritableRaster; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import javax.imageio.stream.ImageInputStream; -import javax.imageio.stream.MemoryCacheImageInputStream; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.pdfbox.cos.COSArray; -import org.apache.pdfbox.cos.COSNumber; -import org.apache.pdfbox.filter.DecodeOptions; -import org.apache.pdfbox.io.IOUtils; -import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; -import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray; -import org.apache.pdfbox.pdmodel.graphics.color.PDIndexed; - -/** - * Reads a sampled image from a PDF file. - * @author John Hewson - */ -final class SampledImageReader -{ - private static final Log LOG = LogFactory.getLog(SampledImageReader.class); - - private SampledImageReader() - { - } - - /** - * Returns an ARGB image filled with the given paint and using the given image as a mask. - * @param paint the paint to fill the visible portions of the image with - * @return a masked image filled with the given paint - * @throws IOException if the image cannot be read - * @throws IllegalStateException if the image is not a stencil. - */ - public static BufferedImage getStencilImage(PDImage pdImage, Paint paint) throws IOException - { - int width = pdImage.getWidth(); - int height = pdImage.getHeight(); - - // compose to ARGB - BufferedImage masked = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = masked.createGraphics(); - - // draw the mask - //g.drawImage(mask, 0, 0, null); - - // fill with paint using src-in - //g.setComposite(AlphaComposite.SrcIn); - g.setPaint(paint); - g.fillRect(0, 0, width, height); - g.dispose(); - - // set the alpha - WritableRaster raster = masked.getRaster(); - - final int[] transparent = new int[4]; - - // avoid getting a BufferedImage for the mask to lessen memory footprint. - // Such masks are always bpc=1 and have no colorspace, but have a decode. - // (see 8.9.6.2 Stencil Masking) - ImageInputStream iis = null; - try - { - iis = new MemoryCacheImageInputStream(pdImage.createInputStream()); - final float[] decode = getDecodeArray(pdImage); - int value = decode[0] < decode[1] ? 1 : 0; - int rowLen = width / 8; - if (width % 8 > 0) - { - rowLen++; - } - byte[] buff = new byte[rowLen]; - for (int y = 0; y < height; y++) - { - int x = 0; - int readLen = iis.read(buff); - for (int r = 0; r < rowLen && r < readLen; r++) - { - int byteValue = buff[r]; - int mask = 128; - int shift = 7; - for (int i = 0; i < 8; i++) - { - int bit = (byteValue & mask) >> shift; - mask >>= 1; - --shift; - if (bit == value) - { - raster.setPixel(x, y, transparent); - } - x++; - if (x == width) - { - break; - } - } - } - if (readLen != rowLen) - { - LOG.warn("premature EOF, image will be incomplete"); - break; - } - } - } - finally - { - if (iis != null) - { - iis.close(); - } - } - - return masked; - } - - /** - * Returns the content of the given image as an AWT buffered image with an RGB color space. - * If a color key mask is provided then an ARGB image is returned instead. - * This method never returns null. - * @param pdImage the image to read - * @param colorKey an optional color key mask - * @return content of this image as an RGB buffered image - * @throws IOException if the image cannot be read - */ - public static BufferedImage getRGBImage(PDImage pdImage, COSArray colorKey) throws IOException - { - return getRGBImage(pdImage, null, 1, colorKey); - } - - private static Rectangle clipRegion(PDImage pdImage, Rectangle region) - { - if (region == null) - { - return new Rectangle(0, 0, pdImage.getWidth(), pdImage.getHeight()); - } - else - { - int x = Math.max(0, region.x); - int y = Math.max(0, region.y); - int width = Math.min(region.width, pdImage.getWidth() - x); - int height = Math.min(region.height, pdImage.getHeight() - y); - return new Rectangle(x, y, width, height); - } - } - - public static BufferedImage getRGBImage(PDImage pdImage, Rectangle region, int subsampling, - COSArray colorKey) throws IOException - { - if (pdImage.isEmpty()) - { - throw new IOException("Image stream is empty"); - } - Rectangle clipped = clipRegion(pdImage, region); - - // get parameters, they must be valid or have been repaired - final PDColorSpace colorSpace = pdImage.getColorSpace(); - final int numComponents = colorSpace.getNumberOfComponents(); - final int width = (int) Math.ceil(clipped.getWidth() / subsampling); - final int height = (int) Math.ceil(clipped.getHeight() / subsampling); - final int bitsPerComponent = pdImage.getBitsPerComponent(); - final float[] decode = getDecodeArray(pdImage); - - if (width <= 0 || height <= 0 || pdImage.getWidth() <= 0 || pdImage.getHeight() <= 0) - { - throw new IOException("image width and height must be positive"); - } - - if (bitsPerComponent == 1 && colorKey == null && numComponents == 1) - { - return from1Bit(pdImage, clipped, subsampling, width, height); - } - - // - // An AWT raster must use 8/16/32 bits per component. Images with < 8bpc - // will be unpacked into a byte-backed raster. Images with 16bpc will be reduced - // in depth to 8bpc as they will be drawn to TYPE_INT_RGB images anyway. All code - // in PDColorSpace#toRGBImage expects an 8-bit range, i.e. 0-255. - // - WritableRaster raster = Raster.createBandedRaster(DataBuffer.TYPE_BYTE, width, height, - numComponents, new Point(0, 0)); - final float[] defaultDecode = pdImage.getColorSpace().getDefaultDecode(8); - if (bitsPerComponent == 8 && Arrays.equals(decode, defaultDecode) && colorKey == null) - { - // convert image, faster path for non-decoded, non-colormasked 8-bit images - return from8bit(pdImage, raster, clipped, subsampling, width, height); - } - return fromAny(pdImage, raster, colorKey, clipped, subsampling, width, height); - } - - private static BufferedImage from1Bit(PDImage pdImage, Rectangle clipped, int subsampling, - final int width, final int height) throws IOException - { - final PDColorSpace colorSpace = pdImage.getColorSpace(); - final float[] decode = getDecodeArray(pdImage); - BufferedImage bim = null; - WritableRaster raster; - byte[] output; - - DecodeOptions options = new DecodeOptions(subsampling); - options.setSourceRegion(clipped); - // read bit stream - InputStream iis = null; - try - { - final int inputWidth; - final int startx; - final int starty; - final int scanWidth; - final int scanHeight; - if (options.isFilterSubsampled()) - { - // Decode options were honored, and so there is no need for additional clipping or subsampling - inputWidth = width; - startx = 0; - starty = 0; - scanWidth = width; - scanHeight = height; - subsampling = 1; - } - else - { - // Decode options not honored, so we need to clip and subsample ourselves. - inputWidth = pdImage.getWidth(); - startx = clipped.x; - starty = clipped.y; - scanWidth = clipped.width; - scanHeight = clipped.height; - } - if (colorSpace instanceof PDDeviceGray) - { - // TYPE_BYTE_GRAY and not TYPE_BYTE_BINARY because this one is handled - // without conversion to RGB by Graphics.drawImage - // this reduces the memory footprint, only one byte per pixel instead of three. - bim = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); - raster = bim.getRaster(); - } - else - { - raster = Raster.createBandedRaster(DataBuffer.TYPE_BYTE, width, height, 1, new Point(0, 0)); - } - output = ((DataBufferByte) raster.getDataBuffer()).getData(); - final boolean isIndexed = colorSpace instanceof PDIndexed; - - // create stream - iis = pdImage.createInputStream(options); - - int rowLen = inputWidth / 8; - if (inputWidth % 8 > 0) - { - rowLen++; - } - - // read stream - byte value0; - byte value1; - if (isIndexed || decode[0] < decode[1]) - { - value0 = 0; - value1 = (byte) 255; - } - else - { - value0 = (byte) 255; - value1 = 0; - } - byte[] buff = new byte[rowLen]; - int idx = 0; - for (int y = 0; y < starty + scanHeight; y++) - { - int x = 0; - int readLen = iis.read(buff); - if (y < starty || y % subsampling > 0) - { - continue; - } - for (int r = 0; r < rowLen && r < readLen; r++) - { - int value = buff[r]; - int mask = 128; - for (int i = 0; i < 8; i++) - { - if (x >= startx + scanWidth) - { - break; - } - int bit = value & mask; - mask >>= 1; - if (x >= startx && x % subsampling == 0) - { - output[idx++] = bit == 0 ? value0 : value1; - } - x++; - } - } - if (readLen != rowLen) - { - LOG.warn("premature EOF, image will be incomplete"); - break; - } - } - - if (bim != null) - { - return bim; - } - - // use the color space to convert the image to RGB - return colorSpace.toRGBImage(raster); - } - finally - { - if (iis != null) - { - iis.close(); - } - } - } - - // faster, 8-bit non-decoded, non-colormasked image conversion - private static BufferedImage from8bit(PDImage pdImage, WritableRaster raster, Rectangle clipped, int subsampling, - final int width, final int height) throws IOException - { - DecodeOptions options = new DecodeOptions(subsampling); - options.setSourceRegion(clipped); - InputStream input = pdImage.createInputStream(options); - try - { - final int inputWidth; - final int startx; - final int starty; - final int scanWidth; - final int scanHeight; - if (options.isFilterSubsampled()) - { - // Decode options were honored, and so there is no need for additional clipping or subsampling - inputWidth = width; - startx = 0; - starty = 0; - scanWidth = width; - scanHeight = height; - subsampling = 1; - } - else - { - // Decode options not honored, so we need to clip and subsample ourselves. - inputWidth = pdImage.getWidth(); - startx = clipped.x; - starty = clipped.y; - scanWidth = clipped.width; - scanHeight = clipped.height; - } - final int numComponents = pdImage.getColorSpace().getNumberOfComponents(); - // get the raster's underlying byte buffer - byte[][] banks = ((DataBufferByte) raster.getDataBuffer()).getBankData(); - byte[] tempBytes = new byte[numComponents * inputWidth]; - // compromise between memory and time usage: - // reading the whole image consumes too much memory - // reading one pixel at a time makes it slow in our buffering infrastructure - int i = 0; - for (int y = 0; y < starty + scanHeight; ++y) - { - input.read(tempBytes); - if (y < starty || y % subsampling > 0) - { - continue; - } - - for (int x = startx; x < startx + scanWidth; x += subsampling) - { - for (int c = 0; c < numComponents; c++) - { - banks[c][i] = tempBytes[x * numComponents + c]; - } - ++i; - } - } - // use the color space to convert the image to RGB - return pdImage.getColorSpace().toRGBImage(raster); - } - finally - { - IOUtils.closeQuietly(input); - } - } - - // slower, general-purpose image conversion from any image format - private static BufferedImage fromAny(PDImage pdImage, WritableRaster raster, COSArray colorKey, Rectangle clipped, - int subsampling, final int width, final int height) - throws IOException - { - final PDColorSpace colorSpace = pdImage.getColorSpace(); - final int numComponents = colorSpace.getNumberOfComponents(); - final int bitsPerComponent = pdImage.getBitsPerComponent(); - final float[] decode = getDecodeArray(pdImage); - - DecodeOptions options = new DecodeOptions(subsampling); - options.setSourceRegion(clipped); - // read bit stream - ImageInputStream iis = null; - try - { - final int inputWidth; - final int startx; - final int starty; - final int scanWidth; - final int scanHeight; - if (options.isFilterSubsampled()) - { - // Decode options were honored, and so there is no need for additional clipping or subsampling - inputWidth = width; - startx = 0; - starty = 0; - scanWidth = width; - scanHeight = height; - subsampling = 1; - } - else - { - // Decode options not honored, so we need to clip and subsample ourselves. - inputWidth = pdImage.getWidth(); - startx = clipped.x; - starty = clipped.y; - scanWidth = clipped.width; - scanHeight = clipped.height; - } - // create stream - final float sampleMax = (float) Math.pow(2, bitsPerComponent) - 1f; - iis = new MemoryCacheImageInputStream(pdImage.createInputStream(options)); - final boolean isIndexed = colorSpace instanceof PDIndexed; - - // init color key mask - float[] colorKeyRanges = null; - BufferedImage colorKeyMask = null; - if (colorKey != null) - { - colorKeyRanges = colorKey.toFloatArray(); - colorKeyMask = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); - } - - // calculate row padding - int padding = 0; - if (inputWidth * numComponents * bitsPerComponent % 8 > 0) - { - padding = 8 - (inputWidth * numComponents * bitsPerComponent % 8); - } - - // read stream - byte[] srcColorValues = new byte[numComponents]; - byte[] alpha = new byte[1]; - for (int y = 0; y < starty + scanHeight; y++) - { - for (int x = 0; x < startx + scanWidth; x++) - { - boolean isMasked = true; - for (int c = 0; c < numComponents; c++) - { - int value = (int)iis.readBits(bitsPerComponent); - - // color key mask requires values before they are decoded - if (colorKeyRanges != null) - { - isMasked &= value >= colorKeyRanges[c * 2] && - value <= colorKeyRanges[c * 2 + 1]; - } - - // decode array - final float dMin = decode[c * 2]; - final float dMax = decode[(c * 2) + 1]; - - // interpolate to domain - float output = dMin + (value * ((dMax - dMin) / sampleMax)); - - if (isIndexed) - { - // indexed color spaces get the raw value, because the TYPE_BYTE - // below cannot be reversed by the color space without it having - // knowledge of the number of bits per component - srcColorValues[c] = (byte)Math.round(output); - } - else - { - // interpolate to TYPE_BYTE - int outputByte = Math.round(((output - Math.min(dMin, dMax)) / - Math.abs(dMax - dMin)) * 255f); - - srcColorValues[c] = (byte)outputByte; - } - } - // only write to output if within requested region and subsample. - if (x >= startx && y >= starty && x % subsampling == 0 && y % subsampling == 0) - { - raster.setDataElements((x - startx) / subsampling, (y - starty) / subsampling, srcColorValues); - - // set alpha channel in color key mask, if any - if (colorKeyMask != null) - { - alpha[0] = (byte)(isMasked ? 255 : 0); - colorKeyMask.getRaster().setDataElements((x - startx) / subsampling, (y - starty) / subsampling, alpha); - } - } - } - - // rows are padded to the nearest byte - iis.readBits(padding); - } - - // use the color space to convert the image to RGB - BufferedImage rgbImage = colorSpace.toRGBImage(raster); - - // apply color mask, if any - if (colorKeyMask != null) - { - return applyColorKeyMask(rgbImage, colorKeyMask); - } - else - { - return rgbImage; - } - } - finally - { - if (iis != null) - { - iis.close(); - } - } - } - - // color key mask: RGB + Binary -> ARGB - private static BufferedImage applyColorKeyMask(BufferedImage image, BufferedImage mask) - throws IOException - { - int width = image.getWidth(); - int height = image.getHeight(); - - // compose to ARGB - BufferedImage masked = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - - WritableRaster src = image.getRaster(); - WritableRaster dest = masked.getRaster(); - WritableRaster alpha = mask.getRaster(); - - float[] rgb = new float[3]; - float[] rgba = new float[4]; - float[] alphaPixel = null; - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - src.getPixel(x, y, rgb); - - rgba[0] = rgb[0]; - rgba[1] = rgb[1]; - rgba[2] = rgb[2]; - alphaPixel = alpha.getPixel(x, y, alphaPixel); - rgba[3] = 255 - alphaPixel[0]; - - dest.setPixel(x, y, rgba); - } - } - - return masked; - } - - // gets decode array from dictionary or returns default - private static float[] getDecodeArray(PDImage pdImage) throws IOException - { - final COSArray cosDecode = pdImage.getDecode(); - float[] decode = null; - - if (cosDecode != null) - { - int numberOfComponents = pdImage.getColorSpace().getNumberOfComponents(); - if (cosDecode.size() != numberOfComponents * 2) - { - if (pdImage.isStencil() && cosDecode.size() >= 2 - && cosDecode.get(0) instanceof COSNumber - && cosDecode.get(1) instanceof COSNumber) - { - float decode0 = ((COSNumber) cosDecode.get(0)).floatValue(); - float decode1 = ((COSNumber) cosDecode.get(1)).floatValue(); - if (decode0 >= 0 && decode0 <= 1 && decode1 >= 0 && decode1 <= 1) - { - LOG.warn("decode array " + cosDecode - + " not compatible with color space, using the first two entries"); - return new float[] - { - decode0, decode1 - }; - } - } - LOG.error("decode array " + cosDecode - + " not compatible with color space, using default"); - } - else - { - decode = cosDecode.toFloatArray(); - } - } - - // use color space default - if (decode == null) - { - return pdImage.getColorSpace().getDefaultDecode(pdImage.getBitsPerComponent()); - } - - return decode; - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdmodel.graphics.image; + +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSNumber; +import org.apache.pdfbox.filter.DecodeOptions; +import org.apache.pdfbox.io.IOUtils; +import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray; +import org.apache.pdfbox.pdmodel.graphics.color.PDIndexed; + +/** + * Reads a sampled image from a PDF file. + * @author John Hewson + */ +final class SampledImageReader +{ + private static final Log LOG = LogFactory.getLog(SampledImageReader.class); + + private SampledImageReader() + { + } + + /** + * Returns an ARGB image filled with the given paint and using the given image as a mask. + * @param paint the paint to fill the visible portions of the image with + * @return a masked image filled with the given paint + * @throws IOException if the image cannot be read + * @throws IllegalStateException if the image is not a stencil. + */ + public static BufferedImage getStencilImage(PDImage pdImage, Paint paint) throws IOException + { + int width = pdImage.getWidth(); + int height = pdImage.getHeight(); + + // compose to ARGB + BufferedImage masked = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = masked.createGraphics(); + + // draw the mask + //g.drawImage(mask, 0, 0, null); + + // fill with paint using src-in + //g.setComposite(AlphaComposite.SrcIn); + g.setPaint(paint); + g.fillRect(0, 0, width, height); + g.dispose(); + + // set the alpha + WritableRaster raster = masked.getRaster(); + + final int[] transparent = new int[4]; + + // avoid getting a BufferedImage for the mask to lessen memory footprint. + // Such masks are always bpc=1 and have no colorspace, but have a decode. + // (see 8.9.6.2 Stencil Masking) + ImageInputStream iis = null; + try + { + iis = new MemoryCacheImageInputStream(pdImage.createInputStream()); + final float[] decode = getDecodeArray(pdImage); + int value = decode[0] < decode[1] ? 1 : 0; + int rowLen = width / 8; + if (width % 8 > 0) + { + rowLen++; + } + byte[] buff = new byte[rowLen]; + for (int y = 0; y < height; y++) + { + int x = 0; + int readLen = iis.read(buff); + for (int r = 0; r < rowLen && r < readLen; r++) + { + int byteValue = buff[r]; + int mask = 128; + int shift = 7; + for (int i = 0; i < 8; i++) + { + int bit = (byteValue & mask) >> shift; + mask >>= 1; + --shift; + if (bit == value) + { + raster.setPixel(x, y, transparent); + } + x++; + if (x == width) + { + break; + } + } + } + if (readLen != rowLen) + { + LOG.warn("premature EOF, image will be incomplete"); + break; + } + } + } + finally + { + if (iis != null) + { + iis.close(); + } + } + + return masked; + } + + /** + * Returns the content of the given image as an AWT buffered image with an RGB color space. + * If a color key mask is provided then an ARGB image is returned instead. + * This method never returns null. + * @param pdImage the image to read + * @param colorKey an optional color key mask + * @return content of this image as an RGB buffered image + * @throws IOException if the image cannot be read + */ + public static BufferedImage getRGBImage(PDImage pdImage, COSArray colorKey) throws IOException + { + return getRGBImage(pdImage, null, 1, colorKey); + } + + private static Rectangle clipRegion(PDImage pdImage, Rectangle region) + { + if (region == null) + { + return new Rectangle(0, 0, pdImage.getWidth(), pdImage.getHeight()); + } + else + { + int x = Math.max(0, region.x); + int y = Math.max(0, region.y); + int width = Math.min(region.width, pdImage.getWidth() - x); + int height = Math.min(region.height, pdImage.getHeight() - y); + return new Rectangle(x, y, width, height); + } + } + + /** + * Returns the content of the given image as an AWT buffered image with an RGB color space. + * If a color key mask is provided then an ARGB image is returned instead. + * This method never returns null. + * @param pdImage the image to read + * @param region The region of the source image to get, or null if the entire image is needed. + * The actual region will be clipped to the dimensions of the source image. + * @param subsampling The amount of rows and columns to advance for every output pixel, a value + * of 1 meaning every pixel will be read + * @param colorKey an optional color key mask + * @return content of this image as an (A)RGB buffered image + * @throws IOException if the image cannot be read + */ + public static BufferedImage getRGBImage(PDImage pdImage, Rectangle region, int subsampling, + COSArray colorKey) throws IOException + { + if (pdImage.isEmpty()) + { + throw new IOException("Image stream is empty"); + } + Rectangle clipped = clipRegion(pdImage, region); + + // get parameters, they must be valid or have been repaired + final PDColorSpace colorSpace = pdImage.getColorSpace(); + final int numComponents = colorSpace.getNumberOfComponents(); + final int width = (int) Math.ceil(clipped.getWidth() / subsampling); + final int height = (int) Math.ceil(clipped.getHeight() / subsampling); + final int bitsPerComponent = pdImage.getBitsPerComponent(); + final float[] decode = getDecodeArray(pdImage); + + if (width <= 0 || height <= 0 || pdImage.getWidth() <= 0 || pdImage.getHeight() <= 0) + { + throw new IOException("image width and height must be positive"); + } + + if (bitsPerComponent == 1 && colorKey == null && numComponents == 1) + { + return from1Bit(pdImage, clipped, subsampling, width, height); + } + + // + // An AWT raster must use 8/16/32 bits per component. Images with < 8bpc + // will be unpacked into a byte-backed raster. Images with 16bpc will be reduced + // in depth to 8bpc as they will be drawn to TYPE_INT_RGB images anyway. All code + // in PDColorSpace#toRGBImage expects an 8-bit range, i.e. 0-255. + // Interleaved raster allows chunk-copying for 8-bit images. + WritableRaster raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, + numComponents, new Point(0, 0)); + final float[] defaultDecode = pdImage.getColorSpace().getDefaultDecode(8); + if (bitsPerComponent == 8 && Arrays.equals(decode, defaultDecode) && colorKey == null) + { + // convert image, faster path for non-decoded, non-colormasked 8-bit images + return from8bit(pdImage, raster, clipped, subsampling, width, height); + } + return fromAny(pdImage, raster, colorKey, clipped, subsampling, width, height); + } + + private static BufferedImage from1Bit(PDImage pdImage, Rectangle clipped, final int subsampling, + final int width, final int height) throws IOException + { + int currentSubsampling = subsampling; + final PDColorSpace colorSpace = pdImage.getColorSpace(); + final float[] decode = getDecodeArray(pdImage); + BufferedImage bim = null; + WritableRaster raster; + byte[] output; + + DecodeOptions options = new DecodeOptions(currentSubsampling); + options.setSourceRegion(clipped); + // read bit stream + InputStream iis = null; + try + { + final int inputWidth; + final int startx; + final int starty; + final int scanWidth; + final int scanHeight; + if (options.isFilterSubsampled()) + { + // Decode options were honored, and so there is no need for additional clipping or subsampling + inputWidth = width; + startx = 0; + starty = 0; + scanWidth = width; + scanHeight = height; + currentSubsampling = 1; + } + else + { + // Decode options not honored, so we need to clip and subsample ourselves. + inputWidth = pdImage.getWidth(); + startx = clipped.x; + starty = clipped.y; + scanWidth = clipped.width; + scanHeight = clipped.height; + } + if (colorSpace instanceof PDDeviceGray) + { + // TYPE_BYTE_GRAY and not TYPE_BYTE_BINARY because this one is handled + // without conversion to RGB by Graphics.drawImage + // this reduces the memory footprint, only one byte per pixel instead of three. + bim = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + raster = bim.getRaster(); + } + else + { + raster = Raster.createBandedRaster(DataBuffer.TYPE_BYTE, width, height, 1, new Point(0, 0)); + } + output = ((DataBufferByte) raster.getDataBuffer()).getData(); + final boolean isIndexed = colorSpace instanceof PDIndexed; + + // create stream + iis = pdImage.createInputStream(options); + + int rowLen = inputWidth / 8; + if (inputWidth % 8 > 0) + { + rowLen++; + } + + // read stream + byte value0; + byte value1; + if (isIndexed || decode[0] < decode[1]) + { + value0 = 0; + value1 = (byte) 255; + } + else + { + value0 = (byte) 255; + value1 = 0; + } + byte[] buff = new byte[rowLen]; + int idx = 0; + for (int y = 0; y < starty + scanHeight; y++) + { + int x = 0; + int readLen = iis.read(buff); + if (y < starty || y % currentSubsampling > 0) + { + continue; + } + for (int r = 0; r < rowLen && r < readLen; r++) + { + int value = buff[r]; + int mask = 128; + for (int i = 0; i < 8; i++) + { + if (x >= startx + scanWidth) + { + break; + } + int bit = value & mask; + mask >>= 1; + if (x >= startx && x % currentSubsampling == 0) + { + output[idx++] = bit == 0 ? value0 : value1; + } + x++; + } + } + if (readLen != rowLen) + { + LOG.warn("premature EOF, image will be incomplete"); + break; + } + } + + if (bim != null) + { + return bim; + } + + // use the color space to convert the image to RGB + return colorSpace.toRGBImage(raster); + } + finally + { + if (iis != null) + { + iis.close(); + } + } + } + + // faster, 8-bit non-decoded, non-colormasked image conversion + private static BufferedImage from8bit(PDImage pdImage, WritableRaster raster, Rectangle clipped, final int subsampling, + final int width, final int height) throws IOException + { + int currentSubsampling = subsampling; + DecodeOptions options = new DecodeOptions(currentSubsampling); + options.setSourceRegion(clipped); + InputStream input = pdImage.createInputStream(options); + try + { + final int inputWidth; + final int startx; + final int starty; + final int scanWidth; + final int scanHeight; + if (options.isFilterSubsampled()) + { + // Decode options were honored, and so there is no need for additional clipping or subsampling + inputWidth = width; + startx = 0; + starty = 0; + scanWidth = width; + scanHeight = height; + currentSubsampling = 1; + } + else + { + // Decode options not honored, so we need to clip and subsample ourselves. + inputWidth = pdImage.getWidth(); + startx = clipped.x; + starty = clipped.y; + scanWidth = clipped.width; + scanHeight = clipped.height; + } + final int numComponents = pdImage.getColorSpace().getNumberOfComponents(); + // get the raster's underlying byte buffer + byte[] bank = ((DataBufferByte) raster.getDataBuffer()).getData(); + if (startx == 0 && starty == 0 && scanWidth == width && scanHeight == height && currentSubsampling == 1) + { + // we just need to copy all sample data, then convert to RGB image. + long inputResult = input.read(bank); + if (inputResult != width * height * (long) numComponents) + { + LOG.debug("Tried reading " + width * height * (long) numComponents + " bytes but only " + inputResult + " bytes read"); + } + return pdImage.getColorSpace().toRGBImage(raster); + } + + // either subsampling is required, or reading only part of the image, so its + // not possible to blindly copy all data. + byte[] tempBytes = new byte[numComponents * inputWidth]; + // compromise between memory and time usage: + // reading the whole image consumes too much memory + // reading one pixel at a time makes it slow in our buffering infrastructure + int i = 0; + for (int y = 0; y < starty + scanHeight; ++y) + { + input.read(tempBytes); + if (y < starty || y % currentSubsampling > 0) + { + continue; + } + + if (currentSubsampling == 1) + { + // Not the entire region was requested, but if no subsampling should + // be performed, we can still copy the entire part of this row + System.arraycopy(tempBytes, startx * numComponents, bank, y * inputWidth * numComponents, scanWidth * numComponents); + } + else + { + for (int x = startx; x < startx + scanWidth; x += currentSubsampling) + { + for (int c = 0; c < numComponents; c++) + { + bank[i] = tempBytes[x * numComponents + c]; + ++i; + } + } + } + } + // use the color space to convert the image to RGB + return pdImage.getColorSpace().toRGBImage(raster); + } + finally + { + IOUtils.closeQuietly(input); + } + } + + // slower, general-purpose image conversion from any image format + private static BufferedImage fromAny(PDImage pdImage, WritableRaster raster, COSArray colorKey, Rectangle clipped, + final int subsampling, final int width, final int height) + throws IOException + { + int currentSubsampling = subsampling; + final PDColorSpace colorSpace = pdImage.getColorSpace(); + final int numComponents = colorSpace.getNumberOfComponents(); + final int bitsPerComponent = pdImage.getBitsPerComponent(); + final float[] decode = getDecodeArray(pdImage); + + DecodeOptions options = new DecodeOptions(currentSubsampling); + options.setSourceRegion(clipped); + // read bit stream + ImageInputStream iis = null; + try + { + final int inputWidth; + final int startx; + final int starty; + final int scanWidth; + final int scanHeight; + if (options.isFilterSubsampled()) + { + // Decode options were honored, and so there is no need for additional clipping or subsampling + inputWidth = width; + startx = 0; + starty = 0; + scanWidth = width; + scanHeight = height; + currentSubsampling = 1; + } + else + { + // Decode options not honored, so we need to clip and subsample ourselves. + inputWidth = pdImage.getWidth(); + startx = clipped.x; + starty = clipped.y; + scanWidth = clipped.width; + scanHeight = clipped.height; + } + // create stream + final float sampleMax = (float) Math.pow(2, bitsPerComponent) - 1f; + iis = new MemoryCacheImageInputStream(pdImage.createInputStream(options)); + final boolean isIndexed = colorSpace instanceof PDIndexed; + + // init color key mask + float[] colorKeyRanges = null; + BufferedImage colorKeyMask = null; + if (colorKey != null) + { + colorKeyRanges = colorKey.toFloatArray(); + colorKeyMask = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + } + + // calculate row padding + int padding = 0; + if (inputWidth * numComponents * bitsPerComponent % 8 > 0) + { + padding = 8 - (inputWidth * numComponents * bitsPerComponent % 8); + } + + // read stream + byte[] srcColorValues = new byte[numComponents]; + byte[] alpha = new byte[1]; + for (int y = 0; y < starty + scanHeight; y++) + { + for (int x = 0; x < startx + scanWidth; x++) + { + boolean isMasked = true; + for (int c = 0; c < numComponents; c++) + { + int value = (int)iis.readBits(bitsPerComponent); + + // color key mask requires values before they are decoded + if (colorKeyRanges != null) + { + isMasked &= value >= colorKeyRanges[c * 2] && + value <= colorKeyRanges[c * 2 + 1]; + } + + // decode array + final float dMin = decode[c * 2]; + final float dMax = decode[(c * 2) + 1]; + + // interpolate to domain + float output = dMin + (value * ((dMax - dMin) / sampleMax)); + + if (isIndexed) + { + // indexed color spaces get the raw value, because the TYPE_BYTE + // below cannot be reversed by the color space without it having + // knowledge of the number of bits per component + srcColorValues[c] = (byte)Math.round(output); + } + else + { + // interpolate to TYPE_BYTE + int outputByte = Math.round(((output - Math.min(dMin, dMax)) / + Math.abs(dMax - dMin)) * 255f); + + srcColorValues[c] = (byte)outputByte; + } + } + // only write to output if within requested region and subsample. + if (x >= startx && y >= starty && x % currentSubsampling == 0 && y % currentSubsampling == 0) + { + raster.setDataElements((x - startx) / currentSubsampling, (y - starty) / currentSubsampling, srcColorValues); + + // set alpha channel in color key mask, if any + if (colorKeyMask != null) + { + alpha[0] = (byte)(isMasked ? 255 : 0); + colorKeyMask.getRaster().setDataElements((x - startx) / currentSubsampling, (y - starty) / currentSubsampling, alpha); + } + } + } + + // rows are padded to the nearest byte + iis.readBits(padding); + } + + // use the color space to convert the image to RGB + BufferedImage rgbImage = colorSpace.toRGBImage(raster); + + // apply color mask, if any + if (colorKeyMask != null) + { + return applyColorKeyMask(rgbImage, colorKeyMask); + } + else + { + return rgbImage; + } + } + finally + { + if (iis != null) + { + iis.close(); + } + } + } + + // color key mask: RGB + Binary -> ARGB + private static BufferedImage applyColorKeyMask(BufferedImage image, BufferedImage mask) + throws IOException + { + int width = image.getWidth(); + int height = image.getHeight(); + + // compose to ARGB + BufferedImage masked = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + WritableRaster src = image.getRaster(); + WritableRaster dest = masked.getRaster(); + WritableRaster alpha = mask.getRaster(); + + float[] rgb = new float[3]; + float[] rgba = new float[4]; + float[] alphaPixel = null; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + src.getPixel(x, y, rgb); + + rgba[0] = rgb[0]; + rgba[1] = rgb[1]; + rgba[2] = rgb[2]; + alphaPixel = alpha.getPixel(x, y, alphaPixel); + rgba[3] = 255 - alphaPixel[0]; + + dest.setPixel(x, y, rgba); + } + } + + return masked; + } + + // gets decode array from dictionary or returns default + private static float[] getDecodeArray(PDImage pdImage) throws IOException + { + final COSArray cosDecode = pdImage.getDecode(); + float[] decode = null; + + if (cosDecode != null) + { + int numberOfComponents = pdImage.getColorSpace().getNumberOfComponents(); + if (cosDecode.size() != numberOfComponents * 2) + { + if (pdImage.isStencil() && cosDecode.size() >= 2 + && cosDecode.get(0) instanceof COSNumber + && cosDecode.get(1) instanceof COSNumber) + { + float decode0 = ((COSNumber) cosDecode.get(0)).floatValue(); + float decode1 = ((COSNumber) cosDecode.get(1)).floatValue(); + if (decode0 >= 0 && decode0 <= 1 && decode1 >= 0 && decode1 <= 1) + { + LOG.warn("decode array " + cosDecode + + " not compatible with color space, using the first two entries"); + return new float[] + { + decode0, decode1 + }; + } + } + LOG.error("decode array " + cosDecode + + " not compatible with color space, using default"); + } + else + { + decode = cosDecode.toFloatArray(); + } + } + + // use color space default + if (decode == null) + { + return pdImage.getColorSpace().getDefaultDecode(pdImage.getBitsPerComponent()); + } + + return decode; + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/AxialShadingPaint.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/AxialShadingPaint.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/AxialShadingPaint.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/AxialShadingPaint.java 2018-11-28 17:18:36.000000000 +0000 @@ -17,7 +17,6 @@ package org.apache.pdfbox.pdmodel.graphics.shading; import java.awt.Color; -import java.awt.Paint; import java.awt.PaintContext; import java.awt.Rectangle; import java.awt.RenderingHints; @@ -33,13 +32,10 @@ * AWT Paint for axial shading. * */ -public class AxialShadingPaint implements Paint +public class AxialShadingPaint extends ShadingPaint { private static final Log LOG = LogFactory.getLog(AxialShadingPaint.class); - private final PDShadingType2 shading; - private final Matrix matrix; - /** * Constructor. * @@ -48,8 +44,7 @@ */ AxialShadingPaint(PDShadingType2 shadingType2, Matrix matrix) { - shading = shadingType2; - this.matrix = matrix; + super(shadingType2, matrix); } @Override diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/RadialShadingPaint.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/RadialShadingPaint.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/RadialShadingPaint.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/RadialShadingPaint.java 2018-11-28 17:18:36.000000000 +0000 @@ -17,7 +17,6 @@ package org.apache.pdfbox.pdmodel.graphics.shading; import java.awt.Color; -import java.awt.Paint; import java.awt.PaintContext; import java.awt.Rectangle; import java.awt.RenderingHints; @@ -34,13 +33,10 @@ * AWT Paint for radial shading. * */ -public class RadialShadingPaint implements Paint +public class RadialShadingPaint extends ShadingPaint { private static final Log LOG = LogFactory.getLog(RadialShadingPaint.class); - private final PDShadingType3 shading; - private final Matrix matrix; - /** * Constructor. * @@ -49,8 +45,7 @@ */ RadialShadingPaint(PDShadingType3 shading, Matrix matrix) { - this.shading = shading; - this.matrix = matrix; + super(shading, matrix); } @Override diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/ShadingPaint.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/ShadingPaint.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/ShadingPaint.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/ShadingPaint.java 2018-11-28 17:18:36.000000000 +0000 @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdmodel.graphics.shading; + +import java.awt.Paint; + +import org.apache.pdfbox.util.Matrix; + +/** + * This is base class for all PDShading-Paints to allow other low level libraries access to the + * shading source data. One user of this interface is the PdfBoxGraphics2D-adapter. + * + * @param the actual PDShading class. + */ +public abstract class ShadingPaint implements Paint +{ + protected final T shading; + protected final Matrix matrix; + + ShadingPaint(T shading, Matrix matrix) + { + this.shading = shading; + this.matrix = matrix; + } + + /** + * @return the PDShading of this paint + */ + public T getShading() + { + return shading; + } + + /** + * @return the active Matrix of this paint + */ + public Matrix getMatrix() + { + return matrix; + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type1ShadingPaint.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type1ShadingPaint.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type1ShadingPaint.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type1ShadingPaint.java 2018-11-28 17:18:36.000000000 +0000 @@ -17,7 +17,6 @@ package org.apache.pdfbox.pdmodel.graphics.shading; import java.awt.Color; -import java.awt.Paint; import java.awt.PaintContext; import java.awt.Rectangle; import java.awt.RenderingHints; @@ -32,13 +31,10 @@ /** * AWT PaintContext for function-based (Type 1) shading. */ -class Type1ShadingPaint implements Paint +class Type1ShadingPaint extends ShadingPaint { private static final Log LOG = LogFactory.getLog(Type1ShadingPaint.class); - private final PDShadingType1 shading; - private final Matrix matrix; - /** * Constructor. * @@ -47,8 +43,7 @@ */ Type1ShadingPaint(PDShadingType1 shading, Matrix matrix) { - this.shading = shading; - this.matrix = matrix; + super(shading, matrix); } @Override diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type4ShadingPaint.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type4ShadingPaint.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type4ShadingPaint.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type4ShadingPaint.java 2018-11-28 17:18:36.000000000 +0000 @@ -32,13 +32,10 @@ /** * AWT PaintContext for Gouraud Triangle Mesh (Type 4) shading. */ -class Type4ShadingPaint implements Paint +class Type4ShadingPaint extends ShadingPaint { private static final Log LOG = LogFactory.getLog(Type4ShadingPaint.class); - private final PDShadingType4 shading; - private final Matrix matrix; - /** * Constructor. * @@ -47,8 +44,7 @@ */ Type4ShadingPaint(PDShadingType4 shading, Matrix matrix) { - this.shading = shading; - this.matrix = matrix; + super(shading, matrix); } @Override diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type5ShadingPaint.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type5ShadingPaint.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type5ShadingPaint.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type5ShadingPaint.java 2018-11-28 17:18:36.000000000 +0000 @@ -17,7 +17,6 @@ package org.apache.pdfbox.pdmodel.graphics.shading; import java.awt.Color; -import java.awt.Paint; import java.awt.PaintContext; import java.awt.Rectangle; import java.awt.RenderingHints; @@ -32,13 +31,10 @@ /** * AWT Paint for Gouraud Triangle Lattice (Type 5) shading. */ -class Type5ShadingPaint implements Paint +class Type5ShadingPaint extends ShadingPaint { private static final Log LOG = LogFactory.getLog(Type5ShadingPaint.class); - private final PDShadingType5 shading; - private final Matrix matrix; - /** * Constructor. * @@ -47,8 +43,7 @@ */ Type5ShadingPaint(PDShadingType5 shading, Matrix matrix) { - this.shading = shading; - this.matrix = matrix; + super(shading, matrix); } @Override diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type6ShadingPaint.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type6ShadingPaint.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type6ShadingPaint.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type6ShadingPaint.java 2018-11-28 17:18:36.000000000 +0000 @@ -16,7 +16,6 @@ package org.apache.pdfbox.pdmodel.graphics.shading; import java.awt.Color; -import java.awt.Paint; import java.awt.PaintContext; import java.awt.Rectangle; import java.awt.RenderingHints; @@ -34,13 +33,10 @@ * * @author Shaola Ren */ -class Type6ShadingPaint implements Paint +class Type6ShadingPaint extends ShadingPaint { private static final Log LOG = LogFactory.getLog(Type6ShadingPaint.class); - private final PDShadingType6 shading; - private final Matrix matrix; - /** * Constructor. * @@ -49,8 +45,7 @@ */ Type6ShadingPaint(PDShadingType6 shading, Matrix matrix) { - this.shading = shading; - this.matrix = matrix; + super(shading, matrix); } @Override diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type7ShadingPaint.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type7ShadingPaint.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type7ShadingPaint.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/shading/Type7ShadingPaint.java 2018-11-28 17:18:36.000000000 +0000 @@ -16,7 +16,6 @@ package org.apache.pdfbox.pdmodel.graphics.shading; import java.awt.Color; -import java.awt.Paint; import java.awt.PaintContext; import java.awt.Rectangle; import java.awt.RenderingHints; @@ -34,13 +33,10 @@ * * @author Shaola Ren */ -class Type7ShadingPaint implements Paint +class Type7ShadingPaint extends ShadingPaint { private static final Log LOG = LogFactory.getLog(Type7ShadingPaint.class); - private final PDShadingType7 shading; - private final Matrix matrix; - /** * Constructor. * @@ -49,8 +45,7 @@ */ Type7ShadingPaint(PDShadingType7 shading, Matrix matrix) { - this.shading = shading; - this.matrix = matrix; + super(shading, matrix); } @Override diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java 2018-11-28 17:18:36.000000000 +0000 @@ -70,7 +70,7 @@ { if( key.equals( COSName.LW ) ) { - gs.setLineWidth( getLineWidth() ); + gs.setLineWidth( defaultIfNull( getLineWidth(), 1 ) ); } else if( key.equals( COSName.LC ) ) { @@ -82,7 +82,7 @@ } else if( key.equals( COSName.ML ) ) { - gs.setMiterLimit( getMiterLimit() ); + gs.setMiterLimit( defaultIfNull( getMiterLimit(), 10 ) ); } else if( key.equals( COSName.D ) ) { @@ -94,7 +94,7 @@ } else if( key.equals( COSName.OPM ) ) { - gs.setOverprintMode( getOverprintMode().doubleValue() ); + gs.setOverprintMode( defaultIfNull( getOverprintMode(), 0 ) ); } else if( key.equals( COSName.OP ) ) { @@ -115,11 +115,11 @@ } else if( key.equals( COSName.FL ) ) { - gs.setFlatness( getFlatnessTolerance() ); + gs.setFlatness( defaultIfNull( getFlatnessTolerance(), 1.0f ) ); } else if( key.equals( COSName.SM ) ) { - gs.setSmoothness( getSmoothnessTolerance() ); + gs.setSmoothness( defaultIfNull( getSmoothnessTolerance(), 0 ) ); } else if( key.equals( COSName.SA ) ) { @@ -127,11 +127,11 @@ } else if( key.equals( COSName.CA ) ) { - gs.setAlphaConstant(getStrokingAlphaConstant()); + gs.setAlphaConstant( defaultIfNull( getStrokingAlphaConstant(), 1.0f ) ); } else if( key.equals( COSName.CA_NS ) ) { - gs.setNonStrokeAlphaConstant(getNonStrokingAlphaConstant() ); + gs.setNonStrokeAlphaConstant( defaultIfNull( getNonStrokingAlphaConstant(), 1.0f ) ); } else if( key.equals( COSName.AIS ) ) { @@ -174,6 +174,22 @@ } /** + * Returns the provided default value in case 'standard' valu + * is null. To be used in cases unboxing may + * lead to a NPE. + * + * @param _value 'standard' value + * @param _default default value + * + * @return 'standard' value if not null + * otherwise default value + */ + private float defaultIfNull( Float _value, float _default ) + { + return _value != null ? _value : _default; + } + + /** * This will get the underlying dictionary that this class acts on. * * @return The underlying dictionary for this class. @@ -273,15 +289,15 @@ public PDLineDashPattern getLineDashPattern() { PDLineDashPattern retval = null; - COSArray dp = (COSArray) dict.getDictionaryObject( COSName.D ); - if( dp != null ) + COSBase dp = dict.getDictionaryObject( COSName.D ); + if( dp instanceof COSArray && ((COSArray)dp).size() == 2) { - COSArray array = new COSArray(); - dp.addAll(dp); - dp.remove(dp.size() - 1); - int phase = dp.getInt(dp.size() - 1); - - retval = new PDLineDashPattern( array, phase ); + COSBase dashArray = ((COSArray)dp).getObject(0); + COSBase phase = ((COSArray)dp).getObject(1); + if (dashArray instanceof COSArray && phase instanceof COSNumber) + { + retval = new PDLineDashPattern((COSArray) dashArray, ((COSNumber) phase).intValue()); + } } return retval; } @@ -588,9 +604,10 @@ private Float getFloatItem( COSName key ) { Float retval = null; - COSNumber value = (COSNumber) dict.getDictionaryObject( key ); - if( value != null ) + COSBase base = dict.getDictionaryObject(key); + if (base instanceof COSNumber) { + COSNumber value = (COSNumber) base; retval = value.floatValue(); } return retval; diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationWidget.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationWidget.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationWidget.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationWidget.java 2018-11-28 17:18:38.000000000 +0000 @@ -25,7 +25,9 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField; /** - * This is the class that represents a widget. + * This is the class that represents a widget annotation. This represents the + * appearance of a field and manages user interactions. A field may have several + * widget annotations, which may be on several pages. * * @author Ben Litchfield */ diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceCharacteristicsDictionary.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceCharacteristicsDictionary.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceCharacteristicsDictionary.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceCharacteristicsDictionary.java 2018-11-28 17:18:38.000000000 +0000 @@ -126,7 +126,7 @@ */ public String getNormalCaption() { - return this.getCOSObject().getString("CA"); + return this.getCOSObject().getString(COSName.CA); } /** @@ -136,7 +136,7 @@ */ public void setNormalCaption(String caption) { - this.getCOSObject().setString("CA", caption); + this.getCOSObject().setString(COSName.CA, caption); } /** @@ -146,7 +146,7 @@ */ public String getRolloverCaption() { - return this.getCOSObject().getString("RC"); + return this.getCOSObject().getString(COSName.RC); } /** @@ -156,7 +156,7 @@ */ public void setRolloverCaption(String caption) { - this.getCOSObject().setString("RC", caption); + this.getCOSObject().setString(COSName.RC, caption); } /** @@ -166,7 +166,7 @@ */ public String getAlternateCaption() { - return this.getCOSObject().getString("AC"); + return this.getCOSObject().getString(COSName.AC); } /** @@ -176,7 +176,7 @@ */ public void setAlternateCaption(String caption) { - this.getCOSObject().setString("AC", caption); + this.getCOSObject().setString(COSName.AC, caption); } /** @@ -186,7 +186,7 @@ */ public PDFormXObject getNormalIcon() { - COSBase i = this.getCOSObject().getDictionaryObject("I"); + COSBase i = this.getCOSObject().getDictionaryObject(COSName.I); if (i instanceof COSStream) { return new PDFormXObject((COSStream)i); @@ -201,7 +201,7 @@ */ public PDFormXObject getRolloverIcon() { - COSBase i = this.getCOSObject().getDictionaryObject("RI"); + COSBase i = this.getCOSObject().getDictionaryObject(COSName.RI); if (i instanceof COSStream) { return new PDFormXObject((COSStream)i); @@ -216,7 +216,7 @@ */ public PDFormXObject getAlternateIcon() { - COSBase i = this.getCOSObject().getDictionaryObject("IX"); + COSBase i = this.getCOSObject().getDictionaryObject(COSName.IX); if (i instanceof COSStream) { return new PDFormXObject((COSStream)i); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDBorderEffectDictionary.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDBorderEffectDictionary.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDBorderEffectDictionary.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDBorderEffectDictionary.java 2018-11-28 17:18:38.000000000 +0000 @@ -106,7 +106,7 @@ /** * This will retrieve the border effect, see the STYLE_* constants for valid values. * - * @return the effect of the border + * @return the effect of the border or {@link #STYLE_SOLID} if none is found. */ public String getStyle() { diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/digitalsignature/visible/PDVisibleSignDesigner.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/digitalsignature/visible/PDVisibleSignDesigner.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/digitalsignature/visible/PDVisibleSignDesigner.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/digitalsignature/visible/PDVisibleSignDesigner.java 2018-11-28 17:18:38.000000000 +0000 @@ -226,20 +226,21 @@ yAxis = pageHeight - xAxis - imageWidth; xAxis = temp; + affineTransform = new AffineTransform( + 0, imageHeight / imageWidth, -imageWidth / imageHeight, 0, imageWidth, 0); + temp = imageHeight; imageHeight = imageWidth; imageWidth = temp; - - affineTransform = new AffineTransform(0, 0.5, -2, 0, 100, 0); break; - + case 180: float newX = pageWidth - xAxis - imageWidth; float newY = pageHeight - yAxis - imageHeight; xAxis = newX; yAxis = newY; - - affineTransform = new AffineTransform(-1, 0, 0, -1, 100, 50); + + affineTransform = new AffineTransform(-1, 0, 0, -1, imageWidth, imageHeight); break; case 270: @@ -247,11 +248,12 @@ xAxis = pageWidth - yAxis - imageHeight; yAxis = temp; + affineTransform = new AffineTransform( + 0, -imageHeight / imageWidth, imageWidth / imageHeight, 0, 0, imageHeight); + temp = imageHeight; imageHeight = imageWidth; imageWidth = temp; - - affineTransform = new AffineTransform(0, -0.5, 2, 0, 0, 50); break; case 0: diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java 2018-11-28 17:18:38.000000000 +0000 @@ -411,6 +411,10 @@ // get the font PDFont font = defaultAppearance.getFont(); + if (font == null) + { + throw new IllegalArgumentException("font is null, check whether /DA entry is incomplete or incorrect"); + } // calculate the fontSize (because 0 = autosize) float fontSize = defaultAppearance.getFontSize(); @@ -424,7 +428,7 @@ // options if (field instanceof PDListBox) { - insertGeneratedSelectionHighlight(contents, appearanceStream, font, fontSize); + insertGeneratedListboxSelectionHighlight(contents, appearanceStream, font, fontSize); } // start the text output @@ -609,7 +613,7 @@ } } - private void insertGeneratedSelectionHighlight(PDPageContentStream contents, PDAppearanceStream appearanceStream, + private void insertGeneratedListboxSelectionHighlight(PDPageContentStream contents, PDAppearanceStream appearanceStream, PDFont font, float fontSize) throws IOException { List indexEntries = ((PDListBox) field).getSelectedOptionsIndex(); @@ -698,7 +702,7 @@ contents.newLineAtOffset(contentRect.getLowerLeftX(), yTextPos); contents.showText(options.get(i)); - if (i - topIndex != (numOptions - 1)) + if (i != (numOptions - 1)) { contents.endText(); } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/FieldUtils.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/FieldUtils.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/FieldUtils.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/FieldUtils.java 2018-11-28 17:18:38.000000000 +0000 @@ -25,7 +25,6 @@ import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSString; -import org.apache.pdfbox.pdmodel.common.COSArrayList; /** * A set of utility methods to help with common AcroForm form and field related functions. @@ -175,37 +174,24 @@ } else if (items instanceof COSArray) { - // test if there is a single text or a two-element array - COSBase entry = ((COSArray) items).get(0); - if (entry instanceof COSString) + List entryList = new ArrayList(); + for (COSBase entry : (COSArray) items) { - return COSArrayList.convertCOSStringCOSArrayToList((COSArray)items); - } - else - { - return getItemsFromPair(items, pairIdx); - } + if (entry instanceof COSString) + { + entryList.add(((COSString) entry).getString()); + } + else if (entry instanceof COSArray) + { + COSArray cosArray = (COSArray) entry; + if (cosArray.size() >= pairIdx +1 && cosArray.get(pairIdx) instanceof COSString) + { + entryList.add(((COSString) cosArray.get(pairIdx)).getString()); + } + } + } + return entryList; } return Collections.emptyList(); - } - - /** - * Return either one of a list of two-element arrays entries. - * - * @param items the array of elements or two-element arrays - * @param pairIdx the index into the two-element array - * @return a List of single elements - */ - private static List getItemsFromPair(COSBase items, int pairIdx) - { - List exportValues = new ArrayList(); - int numItems = ((COSArray) items).size(); - for (int i=0;i fields, boolean refreshAppearances) throws IOException { + // Nothing to flatten if there are no fields provided + if (fields.isEmpty()) { + return; + } + // for dynamic XFA forms there is no flatten as this would mean to do a rendering // from the XFA content into a static PDF. if (xfaIsDynamic()) @@ -271,17 +276,21 @@ // the content stream to write to PDPageContentStream contentStream; + + // get the widgets per page + Map> pagesWidgetsMap = buildPagesWidgetsMap(fields); // preserve all non widget annotations for (PDPage page : document.getPages()) { + Map widgetsForPageMap = pagesWidgetsMap.get(page.getCOSObject()); isContentStreamWrapped = false; List annotations = new ArrayList(); for (PDAnnotation annotation: page.getAnnotations()) { - if (!(annotation instanceof PDAnnotationWidget)) + if (widgetsForPageMap != null && widgetsForPageMap.get(annotation.getCOSObject()) == null) { annotations.add(annotation); } @@ -350,7 +359,7 @@ } // remove the fields - setFields(Collections.emptyList()); + removeFields(fields); // remove XFA for hybrid forms dictionary.removeItem(COSName.XFA); @@ -703,25 +712,6 @@ dictionary.setFlag(COSName.SIG_FLAGS, FLAG_APPEND_ONLY, appendOnly); } - private Map buildAnnotationToPageRef() { - Map annotationToPageRef = new HashMap(); - - int idx = 0; - for (PDPage page : document.getPages()) { - try { - for (PDAnnotation annotation : page.getAnnotations()) { - if (annotation instanceof PDAnnotationWidget) { - annotationToPageRef.put(annotation.getCOSObject(), idx); - } - } - } catch (IOException e) { - LOG.warn("Can't retriev annotations for page " + idx); - } - idx++; - } - return annotationToPageRef; - } - /** * Check if there is a translation needed to place the annotations content. * @@ -730,7 +720,7 @@ */ private boolean resolveNeedsTranslation(PDAppearanceStream appearanceStream) { - boolean needsTranslation = false; + boolean needsTranslation = true; PDResources resources = appearanceStream.getResources(); if (resources != null && resources.getXObjectNames().iterator().hasNext()) @@ -750,9 +740,9 @@ PDRectangle bbox = ((PDFormXObject)xObject).getBBox(); float llX = bbox.getLowerLeftX(); float llY = bbox.getLowerLeftY(); - if (llX == 0 && llY == 0) + if (Float.compare(llX, 0) != 0 && Float.compare(llY, 0) != 0) { - needsTranslation = true; + needsTranslation = false; } } } @@ -780,4 +770,75 @@ PDResources resources = appearanceStream.getResources(); return resources != null && resources.getXObjectNames().iterator().hasNext(); } + + private Map> buildPagesWidgetsMap(List fields) + { + Map> pagesAnnotationsMap = new HashMap>(); + boolean hasMissingPageRef = false; + + for (PDField field : fields) + { + List widgets = field.getWidgets(); + for (PDAnnotationWidget widget : widgets) + { + PDPage pageForWidget = widget.getPage(); + if (pageForWidget != null) + { + if (pagesAnnotationsMap.get(pageForWidget.getCOSObject()) == null) + { + Map widgetsForPage = new HashMap(); + widgetsForPage.put(widget.getCOSObject(), widget); + pagesAnnotationsMap.put(pageForWidget.getCOSObject(), widgetsForPage); + } + else + { + Map widgetsForPage = pagesAnnotationsMap.get(pageForWidget.getCOSObject()); + widgetsForPage.put(widget.getCOSObject(), widget); + } + } + else + { + hasMissingPageRef = true; + } + } + } + + // TODO: if there is a widget with a missing page reference + // we'd need to build the map reverse i.e. form the annotations to the + // widget. But this will be much slower so will be omitted for now. + if (hasMissingPageRef) + { + LOG.warn("There has been a widget with a missing page reference. Please report to the PDFBox project"); + } + + return pagesAnnotationsMap; + } + + private void removeFields(List fields) + { + for (PDField field : fields) { + if (field.getParent() == null) + { + COSArray cosFields = (COSArray) dictionary.getDictionaryObject(COSName.FIELDS); + for (int i=0; iOff is the default value which will also be returned if the + * value hasn't been set at all. * * @return A non-null string. */ @@ -136,7 +138,9 @@ } else { - return ""; + // Off is the default value if there is nothing else set. + // See PDF Spec. + return "Off"; } } @@ -378,14 +382,18 @@ // update the appearance state (AS) for (PDAnnotationWidget widget : getWidgets()) { + if (widget.getAppearance() == null) + { + continue; + } PDAppearanceEntry appearanceEntry = widget.getAppearance().getNormalAppearance(); if (((COSDictionary) appearanceEntry.getCOSObject()).containsKey(value)) { - widget.getCOSObject().setName(COSName.AS, value); + widget.setAppearanceState(value); } else { - widget.getCOSObject().setItem(COSName.AS, COSName.Off); + widget.setAppearanceState(COSName.Off.getName()); } } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocumentCatalog.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocumentCatalog.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocumentCatalog.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocumentCatalog.java 2018-11-28 17:18:38.000000000 +0000 @@ -22,7 +22,6 @@ import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; -import org.apache.pdfbox.cos.COSBoolean; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSObject; @@ -161,8 +160,8 @@ */ public PDDocumentOutline getDocumentOutline() { - COSDictionary dict = (COSDictionary)root.getDictionaryObject(COSName.OUTLINES); - return dict == null ? null : new PDDocumentOutline(dict); + COSBase cosObj = root.getDictionaryObject(COSName.OUTLINES); + return cosObj instanceof COSDictionary ? new PDDocumentOutline((COSDictionary)cosObj) : null; } /** @@ -251,22 +250,7 @@ public PDDestinationOrAction getOpenAction() throws IOException { COSBase openAction = root.getDictionaryObject(COSName.OPEN_ACTION); - if (openAction == null) - { - return null; - } - else if (openAction instanceof COSBoolean) - { - if (((COSBoolean) openAction).getValue() == false) - { - return null; - } - else - { - throw new IOException("Can't create OpenAction from COSBoolean"); - } - } - else if (openAction instanceof COSDictionary) + if (openAction instanceof COSDictionary) { return PDActionFactory.createAction((COSDictionary)openAction); } @@ -276,7 +260,7 @@ } else { - throw new IOException("Unknown OpenAction " + openAction); + return null; } } /** diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocument.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocument.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocument.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocument.java 2018-11-28 17:18:36.000000000 +0000 @@ -35,6 +35,7 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.fontbox.ttf.TrueTypeFont; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; @@ -145,7 +146,10 @@ // fonts to subset before saving private final Set fontsToSubset = new HashSet(); - + + // fonts to close when closing document + private final Set fontsToClose = new HashSet(); + // Signature interface private SignatureInterface signInterface; @@ -154,7 +158,10 @@ // document-wide cached resources private ResourceCache resourceCache = new DefaultResourceCache(); - + + // to make sure only one signature is added + private boolean signatureAdded = false; + /** * Creates an empty PDF document. * You need to add at least one page for the document to be valid. @@ -225,9 +232,14 @@ * Add parameters of signature to be created externally using default signature options. See * {@link #saveIncrementalForExternalSigning(OutputStream)} method description on external * signature creation scenario details. + *

+ * Only one signature may be added in a document. To sign several times, + * load document, add signature, save incremental and close again. * * @param sigObject is the PDSignatureField model * @throws IOException if there is an error creating required fields + * @throws IllegalStateException if one attempts to add several signature + * fields. */ public void addSignature(PDSignature sigObject) throws IOException { @@ -238,10 +250,15 @@ * Add parameters of signature to be created externally. See * {@link #saveIncrementalForExternalSigning(OutputStream)} method description on external * signature creation scenario details. + *

+ * Only one signature may be added in a document. To sign several times, + * load document, add signature, save incremental and close again. * * @param sigObject is the PDSignatureField model * @param options signature options * @throws IOException if there is an error creating required fields + * @throws IllegalStateException if one attempts to add several signature + * fields. */ public void addSignature(PDSignature sigObject, SignatureOptions options) throws IOException { @@ -250,10 +267,16 @@ /** * Add a signature to be created using the instance of given interface. + *

+ * Only one signature may be added in a document. To sign several times, + * load document, add signature, save incremental and close again. * * @param sigObject is the PDSignatureField model - * @param signatureInterface is an interface which provides signing capabilities + * @param signatureInterface is an interface whose implementation provides + * signing capabilities. Can be null if external signing if used. * @throws IOException if there is an error creating required fields + * @throws IllegalStateException if one attempts to add several signature + * fields. */ public void addSignature(PDSignature sigObject, SignatureInterface signatureInterface) throws IOException { @@ -264,15 +287,27 @@ * This will add a signature to the document. If the 0-based page number in the options * parameter is smaller than 0 or larger than max, the nearest valid page number will be used * (i.e. 0 or max) and no exception will be thrown. + *

+ * Only one signature may be added in a document. To sign several times, + * load document, add signature, save incremental and close again. * * @param sigObject is the PDSignatureField model - * @param signatureInterface is an interface which provides signing capabilities + * @param signatureInterface is an interface whose implementation provides + * signing capabilities. Can be null if external signing if used. * @param options signature options * @throws IOException if there is an error creating required fields + * @throws IllegalStateException if one attempts to add several signature + * fields. */ public void addSignature(PDSignature sigObject, SignatureInterface signatureInterface, SignatureOptions options) throws IOException { + if (signatureAdded) + { + throw new IllegalStateException("Only one signature may be added in a document"); + } + signatureAdded = true; + // Reserve content // We need to reserve some space for the signature. Some signatures including // big certificate chain and we need enough space to store it. @@ -580,10 +615,14 @@ * This will add a list of signature fields to the document. * * @param sigFields are the PDSignatureFields that should be added to the document - * @param signatureInterface is a interface which provides signing capabilities + * @param signatureInterface is an interface whose implementation provides + * signing capabilities. Can be null if external signing if used. * @param options signature options * @throws IOException if there is an error creating required fields + * @deprecated The method is misleading, because only one signature may be + * added in a document. The method will be removed in the future. */ + @Deprecated public void addSignatureField(List sigFields, SignatureInterface signatureInterface, SignatureOptions options) throws IOException { @@ -656,23 +695,28 @@ } /** - * This will import and copy the contents from another location. Currently the content stream is stored in a scratch - * file. The scratch file is associated with the document. If you are adding a page to this document from another - * document and want to copy the contents to this - * document's scratch file then use this method otherwise just use the {@link #addPage addPage} + * This will import and copy the contents from another location. Currently the content stream is + * stored in a scratch file. The scratch file is associated with the document. If you are adding + * a page to this document from another document and want to copy the contents to this + * document's scratch file then use this method otherwise just use the {@link #addPage addPage()} * method. *

- * Unlike {@link #addPage addPage}, this method creates a new PDPage object. If your page has + * Unlike {@link #addPage addPage()}, this method creates a new PDPage object. If your page has * annotations, and if these link to pages not in the target document, then the target document * might become huge. What you need to do is to delete page references of such annotations. See * here for how to do this. *

- * Inherited (global) resources are ignored. If you need them, call - * importedPage.setRotation(page.getRotation()); + * Inherited (global) resources are ignored because these can contain resources not needed for + * this page which could bloat your document, see + * PDFBOX-28 and related issues. + * If you need them, call importedPage.setResources(page.getResources()); + *

+ * This method should only be used to import a page from a loaded document, not from a generated + * document because these can contain unfinished parts, e.g. font subsetting information. * * @param page The page to import. * @return The page that was imported. - * + * * @throws IOException If there is an error copying the page. */ public PDPage importPage(PDPage page) throws IOException @@ -894,6 +938,18 @@ } /** + * For internal PDFBox use when creating PDF documents: register a TrueTypeFont to make sure it + * is closed when the PDDocument is closed to avoid memory leaks. Users don't have to call this + * method, it is done by the appropriate PDFont classes. + * + * @param ttf + */ + public void registerTrueTypeFontForClosing(TrueTypeFont ttf) + { + fontsToClose.add(ttf); + } + + /** * Returns the list of fonts which will be subset before the document is saved. */ Set getFontsToSubset() @@ -1252,9 +1308,9 @@ /** * This will save the document to an output stream. - * - * @param output The stream to write to. It is recommended to wrap it in a - * {@link java.io.BufferedOutputStream}, unless it is already buffered. + * + * @param output The stream to write to. It will be closed when done. It is recommended to wrap + * it in a {@link java.io.BufferedOutputStream}, unless it is already buffered. * * @throws IOException if the output could not be written */ @@ -1288,7 +1344,9 @@ * Save the PDF as an incremental update. This is only possible if the PDF was loaded from a * file or a stream, not if the document was created in PDFBox itself. * - * @param output stream to write. It should not point to the source file. + * @param output stream to write to. It will be closed when done. It + * must never point to the source file or that one will be + * harmed! * @throws IOException if the output could not be written * @throws IllegalStateException if the document was not loaded from a file or a stream. */ @@ -1344,7 +1402,9 @@ * {@code PDDocument} instance and only AFTER {@link ExternalSigningSupport} instance is used. *

* - * @param output stream to write final PDF. It should not point to the source file. + * @param output stream to write the final PDF. It will be closed when the + * document is closed. It must never point to the source file + * or that one will be harmed! * @return instance to be used for external signing and setting CMS signature * @throws IOException if the output could not be written * @throws IllegalStateException if the document was not loaded from a file or a stream or @@ -1380,9 +1440,13 @@ } /** - * Returns the page at the given index. + * Returns the page at the given 0-based index. + *

+ * This method is too slow to get all the pages from a large PDF document + * (1000 pages or more). For such documents, use the iterator of + * {@link PDDocument#getPages()} instead. * - * @param pageIndex the page index + * @param pageIndex the 0-based page index * @return the page at the given index. */ public PDPage getPage(int pageIndex) // todo: REPLACE most calls to this method with BELOW method @@ -1420,19 +1484,38 @@ { if (!document.isClosed()) { + // Make sure that: + // - first Exception is kept + // - all IO resources are closed + // - there's a way to see which errors occured + + IOException firstException = null; + // close resources and COSWriter if (signingSupport != null) { - signingSupport.close(); + firstException = IOUtils.closeAndLogException(signingSupport, LOG, "SigningSupport", firstException); } // close all intermediate I/O streams - document.close(); + firstException = IOUtils.closeAndLogException(document, LOG, "COSDocument", firstException); // close the source PDF stream, if we read from one if (pdfSource != null) { - pdfSource.close(); + firstException = IOUtils.closeAndLogException(pdfSource, LOG, "RandomAccessRead pdfSource", firstException); + } + + // close fonts + for (TrueTypeFont ttf : fontsToClose) + { + firstException = IOUtils.closeAndLogException(ttf, LOG, "TrueTypeFont", firstException); + } + + // rethrow first exception to keep method contract + if (firstException != null) + { + throw firstException; } } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocumentNameDictionary.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocumentNameDictionary.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocumentNameDictionary.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDDocumentNameDictionary.java 2018-11-28 17:18:38.000000000 +0000 @@ -75,8 +75,8 @@ } /** - * Get the destination named tree node. The value in this name tree will be PDDestination - * objects. + * Get the destination name tree node. The values in this name tree will be + * PDPageDestination objects. * * @return The destination name tree node. */ @@ -118,8 +118,8 @@ } /** - * Get the embedded files named tree node. The value in this name tree will be PDComplexFileSpecification - * objects. + * Get the embedded files named tree node. The values in this name tree will + * be PDComplexFileSpecification objects. * * @return The embedded files name tree node. */ @@ -148,7 +148,8 @@ } /** - * Get the document level javascript entries. The value in this name tree will be PDTextStream. + * Get the document level javascript entries. The values in this name tree + * will be PDTextStream objects. * * @return The document level named javascript. */ diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDPageContentStream.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDPageContentStream.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDPageContentStream.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDPageContentStream.java 2018-11-28 17:18:36.000000000 +0000 @@ -465,6 +465,7 @@ * * @param text The Unicode text to show. * @throws IOException If an io exception occurs. + * @throws IllegalArgumentException if a character isn't supported by the current font */ public void showText(String text) throws IOException { @@ -497,7 +498,8 @@ // Unicode code points to keep when subsetting if (font.willBeSubset()) { - for (int offset = 0; offset < text.length(); ) + int offset = 0; + while (offset < text.length()) { int codePoint = text.codePointAt(offset); font.addToSubset(codePoint); @@ -979,6 +981,11 @@ */ public void transform(Matrix matrix) throws IOException { + if (inTextMode) + { + LOG.warn("Modifying the current transformation matrix is not allowed within text objects."); + } + writeAffineTransform(matrix.createAffineTransform()); writeOperator("cm"); } @@ -989,6 +996,11 @@ */ public void saveGraphicsState() throws IOException { + if (inTextMode) + { + LOG.warn("Saving the graphics state is not allowed within text objects."); + } + if (!fontStack.isEmpty()) { fontStack.push(fontStack.peek()); @@ -1010,6 +1022,11 @@ */ public void restoreGraphicsState() throws IOException { + if (inTextMode) + { + LOG.warn("Restoring the graphics state is not allowed within text objects."); + } + if (!fontStack.isEmpty()) { fontStack.pop(); @@ -2094,7 +2111,7 @@ /** * Set line width to the given value. * - * @param lineWidth The width which is used for drwaing. + * @param lineWidth The width which is used for drawing. * @throws IOException If the content stream could not be written * @throws IllegalStateException If the method was called within a text block. */ @@ -2286,7 +2303,7 @@ * * @param commands The commands to append to the stream. * @throws IOException If an error occurs while writing to the stream. - * @deprecated This method will be removed in a future release. + * @deprecated Usage of this method is discouraged. */ @Deprecated public void appendRawCommands(String commands) throws IOException @@ -2299,7 +2316,7 @@ * * @param commands The commands to append to the stream. * @throws IOException If an error occurs while writing to the stream. - * @deprecated This method will be removed in a future release. + * @deprecated Usage of this method is discouraged. */ @Deprecated public void appendRawCommands(byte[] commands) throws IOException @@ -2312,7 +2329,7 @@ * * @param data Append a raw byte to the stream. * @throws IOException If an error occurs while writing to the stream. - * @deprecated This method will be removed in a future release. + * @deprecated Usage of this method is discouraged. */ @Deprecated public void appendRawCommands(int data) throws IOException @@ -2325,7 +2342,7 @@ * * @param data Append a formatted double value to the stream. * @throws IOException If an error occurs while writing to the stream. - * @deprecated This method will be removed in a future release. + * @deprecated Usage of this method is discouraged. */ @Deprecated public void appendRawCommands(double data) throws IOException @@ -2338,7 +2355,7 @@ * * @param data Append a formatted float value to the stream. * @throws IOException If an error occurs while writing to the stream. - * @deprecated This method will be removed in a future release. + * @deprecated Usage of this method is discouraged. */ @Deprecated public void appendRawCommands(float data) throws IOException @@ -2351,7 +2368,7 @@ * * @param name the name * @throws IOException If an error occurs while writing to the stream. - * @deprecated This method will be removed in a future release. + * @deprecated Usage of this method is discouraged. */ @Deprecated public void appendCOSName(COSName name) throws IOException @@ -2486,6 +2503,10 @@ @Override public void close() throws IOException { + if (inTextMode) + { + LOG.warn("You did not call endText(), some viewers won't display your text"); + } if (output != null) { output.close(); @@ -2584,4 +2605,18 @@ writeOperand(scale); writeOperator("Tz"); } + + /** + * Set the text rise value, i.e. move the baseline up or down. This is useful for drawing + * superscripts or subscripts. + * + * @param rise Specifies the distance, in unscaled text space units, to move the baseline up or + * down from its default location. 0 restores the default location. + * @throws IOException + */ + public void setTextRise(float rise) throws IOException + { + writeOperand(rise); + writeOperator("Ts"); + } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDPageTree.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDPageTree.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDPageTree.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDPageTree.java 2018-11-28 17:18:36.000000000 +0000 @@ -72,7 +72,7 @@ { if (root == null) { - throw new IllegalArgumentException("root cannot be null"); + throw new IllegalArgumentException("page tree root cannot be null"); } // repair bad PDFs which contain a Page dict instead of a page tree, see PDFBOX-3154 if (COSName.PAGE.equals(root.getCOSName(COSName.TYPE))) @@ -279,7 +279,7 @@ } } - throw new IllegalStateException(); + throw new IllegalStateException("Index not found: " + pageNum); } else { @@ -294,7 +294,7 @@ } else { - throw new IllegalStateException(); + throw new IllegalStateException("Index not found: " + pageNum); } } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/printing/PDFPrintable.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/printing/PDFPrintable.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/printing/PDFPrintable.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/printing/PDFPrintable.java 2018-11-28 17:18:38.000000000 +0000 @@ -47,6 +47,7 @@ private final Scaling scaling; private final float dpi; private final boolean center; + private boolean subsamplingAllowed = false; /** * Creates a new PDFPrintable. @@ -115,7 +116,35 @@ this.dpi = dpi; this.center = center; } - + + /** + * Value indicating if the renderer is allowed to subsample images before drawing, according to + * image dimensions and requested scale. + * + * Subsampling may be faster and less memory-intensive in some cases, but it may also lead to + * loss of quality, especially in images with high spatial frequency. + * + * @return true if subsampling of images is allowed, false otherwise. + */ + public boolean isSubsamplingAllowed() + { + return subsamplingAllowed; + } + + /** + * Sets a value instructing the renderer whether it is allowed to subsample images before + * drawing. The subsampling frequency is determined according to image size and requested scale. + * + * Subsampling may be faster and less memory-intensive in some cases, but it may also lead to + * loss of quality, especially in images with high spatial frequency. + * + * @param subsamplingAllowed The new value indicating if subsampling is allowed. + */ + public void setSubsamplingAllowed(boolean subsamplingAllowed) + { + this.subsamplingAllowed = subsamplingAllowed; + } + @Override public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException @@ -187,6 +216,7 @@ // draw to graphics using PDFRender AffineTransform transform = (AffineTransform)graphics2D.getTransform().clone(); graphics2D.setBackground(Color.WHITE); + renderer.setSubsamplingAllowed(subsamplingAllowed); renderer.renderPageToGraphics(pageIndex, graphics2D, (float)scale); // draw crop box diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/rendering/GroupGraphics.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/rendering/GroupGraphics.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/rendering/GroupGraphics.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/rendering/GroupGraphics.java 2018-11-28 17:18:38.000000000 +0000 @@ -0,0 +1,728 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.rendering; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Image; +import java.awt.Paint; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferInt; +import java.awt.image.ImageObserver; +import java.awt.image.RenderedImage; +import java.awt.image.renderable.RenderableImage; +import java.text.AttributedCharacterIterator; +import java.util.Map; + +/** + * Graphics implementation for non-isolated transparency groups. + *

+ * Non-isolated groups require that the group backdrop (copied from parent group or + * page) is used as the initial contents of the image to which the group is rendered. + * This allows blend modes to blend the group contents with the graphics behind + * the group. Finally when the group rendering is done, backdrop removal must be + * computed (see {@link #removeBackdrop(java.awt.image.BufferedImage, int, int) removeBackdrop}). + * It ensures the backdrop is not rendered twice on the parent but it leaves the + * effects of blend modes. + *

+ * This class renders the group contents to two images. groupImage is + * initialized with the backdrop and group contents are drawn over it. + * groupAlphaImage is initially fully transparent and it accumulates + * the total alpha of the group contents excluding backdrop. + *

+ * If a non-isolated group uses only the blend mode Normal, it can be optimized + * and rendered like an isolated group; backdrop usage and removal are not needed. + */ + +class GroupGraphics extends Graphics2D +{ + private final BufferedImage groupImage; + private final BufferedImage groupAlphaImage; + private final Graphics2D groupG2D; + private final Graphics2D alphaG2D; + + GroupGraphics(BufferedImage groupImage, Graphics2D groupGraphics) + { + this.groupImage = groupImage; + this.groupG2D = groupGraphics; + this.groupAlphaImage = new BufferedImage(groupImage.getWidth(), groupImage.getHeight(), + BufferedImage.TYPE_INT_ARGB); + this.alphaG2D = groupAlphaImage.createGraphics(); + } + + private GroupGraphics(BufferedImage groupImage, Graphics2D groupGraphics, + BufferedImage groupAlphaImage, Graphics2D alphaGraphics) + { + this.groupImage = groupImage; + this.groupG2D = groupGraphics; + this.groupAlphaImage = groupAlphaImage; + this.alphaG2D = alphaGraphics; + } + + @Override + public void clearRect(int x, int y, int width, int height) + { + groupG2D.clearRect(x, y, width, height); + alphaG2D.clearRect(x, y, width, height); + } + + @Override + public void clipRect(int x, int y, int width, int height) + { + groupG2D.clipRect(x, y, width, height); + alphaG2D.clipRect(x, y, width, height); + } + + @Override + public void copyArea(int x, int y, int width, int height, int dx, int dy) + { + groupG2D.copyArea(x, y, width, height, dx, dy); + alphaG2D.copyArea(x, y, width, height, dx, dy); + } + + @Override + public Graphics create() + { + Graphics g = groupG2D.create(); + Graphics a = alphaG2D.create(); + if (g instanceof Graphics2D && a instanceof Graphics2D) + { + return new GroupGraphics(groupImage, (Graphics2D)g, groupAlphaImage, (Graphics2D)a); + } + throw new UnsupportedOperationException(); + } + + @Override + public void dispose() + { + groupG2D.dispose(); + alphaG2D.dispose(); + } + + @Override + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) + { + groupG2D.drawArc(x, y, width, height, startAngle, arcAngle); + alphaG2D.drawArc(x, y, width, height, startAngle, arcAngle); + } + + @Override + public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) + { + groupG2D.drawImage(img, x, y, bgcolor, observer); + return alphaG2D.drawImage(img, x, y, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, ImageObserver observer) + { + groupG2D.drawImage(img, x, y, observer); + return alphaG2D.drawImage(img, x, y, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, + Color bgcolor, ImageObserver observer) + { + groupG2D.drawImage(img, x, y, width, height, bgcolor, observer); + return alphaG2D.drawImage(img, x, y, width, height, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) + { + groupG2D.drawImage(img, x, y, width, height, observer); + return alphaG2D.drawImage(img, x, y, width, height, observer); + } + + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, + int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) + { + groupG2D.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); + return alphaG2D.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, + int sy1, int sx2, int sy2, ImageObserver observer) + { + groupG2D.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); + return alphaG2D.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); + } + + @Override + public void drawLine(int x1, int y1, int x2, int y2) + { + groupG2D.drawLine(x1, y1, x2, y2); + alphaG2D.drawLine(x1, y1, x2, y2); + } + + @Override + public void drawOval(int x, int y, int width, int height) + { + groupG2D.drawOval(x, y, width, height); + alphaG2D.drawOval(x, y, width, height); + } + + @Override + public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) + { + groupG2D.drawPolygon(xPoints, yPoints, nPoints); + alphaG2D.drawPolygon(xPoints, yPoints, nPoints); + } + + @Override + public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) + { + groupG2D.drawPolyline(xPoints, yPoints, nPoints); + alphaG2D.drawPolyline(xPoints, yPoints, nPoints); + } + + @Override + public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) + { + groupG2D.drawRoundRect(x, y, width, height, arcWidth, arcHeight); + alphaG2D.drawRoundRect(x, y, width, height, arcWidth, arcHeight); + } + + @Override + public void drawString(AttributedCharacterIterator iterator, int x, int y) + { + groupG2D.drawString(iterator, x, y); + alphaG2D.drawString(iterator, x, y); + } + + @Override + public void drawString(String str, int x, int y) + { + groupG2D.drawString(str, x, y); + alphaG2D.drawString(str, x, y); + } + + @Override + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) + { + groupG2D.fillArc(x, y, width, height, startAngle, arcAngle); + alphaG2D.fillArc(x, y, width, height, startAngle, arcAngle); + } + + @Override + public void fillOval(int x, int y, int width, int height) + { + groupG2D.fillOval(x, y, width, height); + alphaG2D.fillOval(x, y, width, height); + } + + @Override + public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) + { + groupG2D.fillPolygon(xPoints, yPoints, nPoints); + alphaG2D.fillPolygon(xPoints, yPoints, nPoints); + } + + @Override + public void fillRect(int x, int y, int width, int height) + { + groupG2D.fillRect(x, y, width, height); + alphaG2D.fillRect(x, y, width, height); + } + + @Override + public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) + { + groupG2D.fillRoundRect(x, y, width, height, arcWidth, arcHeight); + alphaG2D.fillRoundRect(x, y, width, height, arcWidth, arcHeight); + } + + @Override + public Shape getClip() + { + return groupG2D.getClip(); + } + + @Override + public Rectangle getClipBounds() + { + return groupG2D.getClipBounds(); + } + + @Override + public Color getColor() + { + return groupG2D.getColor(); + } + + @Override + public Font getFont() + { + return groupG2D.getFont(); + } + + @Override + public FontMetrics getFontMetrics(Font f) + { + return groupG2D.getFontMetrics(f); + } + + @Override + public void setClip(int x, int y, int width, int height) + { + groupG2D.setClip(x, y, width, height); + alphaG2D.setClip(x, y, width, height); + } + + @Override + public void setClip(Shape clip) + { + groupG2D.setClip(clip); + alphaG2D.setClip(clip); + } + + @Override + public void setColor(Color c) + { + groupG2D.setColor(c); + alphaG2D.setColor(c); + } + + @Override + public void setFont(Font font) + { + groupG2D.setFont(font); + alphaG2D.setFont(font); + } + + @Override + public void setPaintMode() + { + groupG2D.setPaintMode(); + alphaG2D.setPaintMode(); + } + + @Override + public void setXORMode(Color c1) + { + groupG2D.setXORMode(c1); + alphaG2D.setXORMode(c1); + } + + @Override + public void translate(int x, int y) + { + groupG2D.translate(x, y); + alphaG2D.translate(x, y); + } + + @Override + public void addRenderingHints(Map hints) + { + groupG2D.addRenderingHints(hints); + alphaG2D.addRenderingHints(hints); + } + + @Override + public void clip(Shape s) + { + groupG2D.clip(s); + alphaG2D.clip(s); + } + + @Override + public void draw(Shape s) + { + groupG2D.draw(s); + alphaG2D.draw(s); + } + + @Override + public void drawGlyphVector(GlyphVector g, float x, float y) + { + groupG2D.drawGlyphVector(g, x, y); + alphaG2D.drawGlyphVector(g, x, y); + } + + @Override + public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) + { + groupG2D.drawImage(img, op, x, y); + alphaG2D.drawImage(img, op, x, y); + } + + @Override + public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) + { + groupG2D.drawImage(img, xform, obs); + return alphaG2D.drawImage(img, xform, obs); + } + + @Override + public void drawRenderableImage(RenderableImage img, AffineTransform xform) + { + groupG2D.drawRenderableImage(img, xform); + alphaG2D.drawRenderableImage(img, xform); + } + + @Override + public void drawRenderedImage(RenderedImage img, AffineTransform xform) + { + groupG2D.drawRenderedImage(img, xform); + alphaG2D.drawRenderedImage(img, xform); + } + + @Override + public void drawString(AttributedCharacterIterator iterator, float x, float y) + { + groupG2D.drawString(iterator, x, y); + alphaG2D.drawString(iterator, x, y); + } + + @Override + public void drawString(String str, float x, float y) + { + groupG2D.drawString(str, x, y); + alphaG2D.drawString(str, x, y); + } + + @Override + public void fill(Shape s) + { + groupG2D.fill(s); + alphaG2D.fill(s); + } + + @Override + public Color getBackground() + { + return groupG2D.getBackground(); + } + + @Override + public Composite getComposite() + { + return groupG2D.getComposite(); + } + + @Override + public GraphicsConfiguration getDeviceConfiguration() + { + return groupG2D.getDeviceConfiguration(); + } + + @Override + public FontRenderContext getFontRenderContext() + { + return groupG2D.getFontRenderContext(); + } + + @Override + public Paint getPaint() + { + return groupG2D.getPaint(); + } + + @Override + public Object getRenderingHint(RenderingHints.Key hintKey) + { + return groupG2D.getRenderingHint(hintKey); + } + + @Override + public RenderingHints getRenderingHints() + { + return groupG2D.getRenderingHints(); + } + + @Override + public Stroke getStroke() + { + return groupG2D.getStroke(); + } + + @Override + public AffineTransform getTransform() + { + return groupG2D.getTransform(); + } + + @Override + public boolean hit(Rectangle rect, Shape s, boolean onStroke) + { + return groupG2D.hit(rect, s, onStroke); + } + + @Override + public void rotate(double theta) + { + groupG2D.rotate(theta); + alphaG2D.rotate(theta); + } + + @Override + public void rotate(double theta, double x, double y) + { + groupG2D.rotate(theta, x, y); + alphaG2D.rotate(theta, x, y); + } + + @Override + public void scale(double sx, double sy) + { + groupG2D.scale(sx, sy); + alphaG2D.scale(sx, sy); + } + + @Override + public void setBackground(Color color) + { + groupG2D.setBackground(color); + alphaG2D.setBackground(color); + } + + @Override + public void setComposite(Composite comp) + { + groupG2D.setComposite(comp); + alphaG2D.setComposite(comp); + } + + @Override + public void setPaint(Paint paint) + { + groupG2D.setPaint(paint); + alphaG2D.setPaint(paint); + } + + @Override + public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) + { + groupG2D.setRenderingHint(hintKey, hintValue); + alphaG2D.setRenderingHint(hintKey, hintValue); + } + + @Override + public void setRenderingHints(Map hints) + { + groupG2D.setRenderingHints(hints); + alphaG2D.setRenderingHints(hints); + } + + @Override + public void setStroke(Stroke s) + { + groupG2D.setStroke(s); + alphaG2D.setStroke(s); + } + + @Override + public void setTransform(AffineTransform tx) + { + groupG2D.setTransform(tx); + alphaG2D.setTransform(tx); + } + + @Override + public void shear(double shx, double shy) + { + groupG2D.shear(shx, shy); + alphaG2D.shear(shx, shy); + } + + @Override + public void transform(AffineTransform tx) + { + groupG2D.transform(tx); + alphaG2D.transform(tx); + } + + @Override + public void translate(double tx, double ty) + { + groupG2D.translate(tx, ty); + alphaG2D.translate(tx, ty); + } + + /** + * Computes backdrop removal. + * The backdrop removal equation is given in section 11.4.4 in the PDF 32000-1:2008 + * standard. It returns the final color C for each pixel in the group:
+ * C = Cn + (Cn - C0) * (alpha0 / alphagn - alpha0)
+ * where
+ * Cn is the group color including backdrop (read from groupImage),
+ * C0 is the backdrop color,
+ * alpha0 is the backdrop alpha,
+ * alphagn is the group alpha excluding backdrop (read the + * alpha channel from groupAlphaImage)
+ *

+ * The alpha of the result is equal to alphagn, i.e., the alpha + * channel of groupAlphaImage. + *

+ * The backdrop image may be much larger than groupImage if, + * for example, the current page is used as the backdrop. Only a specific rectangular + * region of backdrop is used in the backdrop removal: upper-left corner + * is at (offsetX, offsetY); width and height are equal to those of + * groupImage. + * + * @param backdrop group backdrop + * @param offsetX backdrop left X coordinate + * @param offsetY backdrop upper Y coordinate + */ + void removeBackdrop(BufferedImage backdrop, int offsetX, int offsetY) + { + int groupWidth = groupImage.getWidth(); + int groupHeight = groupImage.getHeight(); + int backdropWidth = backdrop.getWidth(); + int backdropHeight = backdrop.getHeight(); + int groupType = groupImage.getType(); + int groupAlphaType = groupAlphaImage.getType(); + int backdropType = backdrop.getType(); + DataBuffer groupDataBuffer = groupImage.getRaster().getDataBuffer(); + DataBuffer groupAlphaDataBuffer = groupAlphaImage.getRaster().getDataBuffer(); + DataBuffer backdropDataBuffer = backdrop.getRaster().getDataBuffer(); + + if (groupType == BufferedImage.TYPE_INT_ARGB && + groupAlphaType == BufferedImage.TYPE_INT_ARGB && + (backdropType == BufferedImage.TYPE_INT_ARGB || backdropType == BufferedImage.TYPE_INT_RGB) && + groupDataBuffer instanceof DataBufferInt && + groupAlphaDataBuffer instanceof DataBufferInt && + backdropDataBuffer instanceof DataBufferInt) + { + // Optimized computation for int[] buffers. + + int[] groupData = ((DataBufferInt)groupDataBuffer).getData(); + int[] groupAlphaData = ((DataBufferInt)groupAlphaDataBuffer).getData(); + int[] backdropData = ((DataBufferInt)backdropDataBuffer).getData(); + boolean backdropHasAlpha = backdropType == BufferedImage.TYPE_INT_ARGB; + + for (int y = 0; y < groupHeight; y++) + { + for (int x = 0; x < groupWidth; x++) + { + int index = x + y * groupWidth; + + // alphagn is the total alpha of the group contents excluding backdrop. + int alphagn = (groupAlphaData[index] >> 24) & 0xFF; + if (alphagn == 0) + { + // Avoid division by 0 and set the result to fully transparent. + groupData[index] = 0; + continue; + } + + int backdropX = x + offsetX; + int backdropY = y + offsetY; + int backdropRGB; // color of backdrop pixel + float alpha0; // alpha of backdrop pixel + + if (backdropX >= 0 && backdropX < backdropWidth && + backdropY >= 0 && backdropY < backdropHeight) + { + backdropRGB = backdropData[backdropX + backdropY * backdropWidth]; + alpha0 = backdropHasAlpha ? ((backdropRGB >> 24) & 0xFF) : 255; + } + else + { + // Backdrop pixel is out of bounds. Use a transparent value. + backdropRGB = 0; + alpha0 = 0; + } + + // Alpha factor alpha0 / alphagn - alpha0 is in range 0.0-1.0. + float alphaFactor = alpha0 / (float)alphagn - alpha0 / 255.0f; + int groupRGB = groupData[index]; // color of group pixel + + // Compute backdrop removal for RGB components. + int r = backdropRemoval(groupRGB, backdropRGB, 16, alphaFactor); + int g = backdropRemoval(groupRGB, backdropRGB, 8, alphaFactor); + int b = backdropRemoval(groupRGB, backdropRGB, 0, alphaFactor); + + // Copy the result back to groupImage. The alpha of the result + // is equal to alphagn. + groupData[index] = (alphagn << 24) | (r << 16) | (g << 8) | b; + } + } + } + else + { + // Non-optimized computation for other types of color spaces and pixel buffers. + + for (int y = 0; y < groupHeight; y++) + { + for (int x = 0; x < groupWidth; x++) + { + int alphagn = (groupAlphaImage.getRGB(x, y) >> 24) & 0xFF; + if (alphagn == 0) + { + groupImage.setRGB(x, y, 0); + continue; + } + + int backdropX = x + offsetX; + int backdropY = y + offsetY; + int backdropRGB; + float alpha0; + if (backdropX >= 0 && backdropX < backdropWidth && + backdropY >= 0 && backdropY < backdropHeight) + { + backdropRGB = backdrop.getRGB(backdropX, backdropY); + alpha0 = (backdropRGB >> 24) & 0xFF; + } + else + { + backdropRGB = 0; + alpha0 = 0; + } + + int groupRGB = groupImage.getRGB(x, y); + float alphaFactor = alpha0 / alphagn - alpha0 / 255.0f; + + int r = backdropRemoval(groupRGB, backdropRGB, 16, alphaFactor); + int g = backdropRemoval(groupRGB, backdropRGB, 8, alphaFactor); + int b = backdropRemoval(groupRGB, backdropRGB, 0, alphaFactor); + + groupImage.setRGB(x, y, (alphagn << 24) | (r << 16) | (g << 8) | b); + } + } + } + } + + /** + * Computes the backdrop removal equation. + * C = Cn + (Cn - C0) * (alpha0 / alphagn - alpha0) + */ + private int backdropRemoval(int groupRGB, int backdropRGB, int shift, float alphaFactor) + { + float cn = (groupRGB >> shift) & 0xFF; + float c0 = (backdropRGB >> shift) & 0xFF; + int c = Math.round(cn + (cn - c0) * alphaFactor); + return (c < 0) ? 0 : (c > 255 ? 255 : c); + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java 2018-11-28 17:18:38.000000000 +0000 @@ -23,6 +23,7 @@ import java.awt.GraphicsDevice; import java.awt.Paint; import java.awt.Point; +import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; @@ -45,8 +46,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.Stack; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine; @@ -55,6 +60,7 @@ import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSNumber; +import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.function.PDFunction; import org.apache.pdfbox.pdmodel.font.PDCIDFontType0; @@ -65,6 +71,8 @@ import org.apache.pdfbox.pdmodel.font.PDType1CFont; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.graphics.PDLineDashPattern; +import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode; import org.apache.pdfbox.pdmodel.graphics.color.PDColor; import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray; @@ -76,6 +84,7 @@ import org.apache.pdfbox.pdmodel.graphics.pattern.PDShadingPattern; import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern; import org.apache.pdfbox.pdmodel.graphics.shading.PDShading; +import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState; import org.apache.pdfbox.pdmodel.graphics.state.PDSoftMask; import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode; @@ -116,8 +125,6 @@ // the page box to draw (usually the crop box but may be another) private PDRectangle pageSize; - private int pageRotation; - // whether image of a transparency group must be flipped // needed when in a tiling pattern private boolean flipTG = false; @@ -137,6 +144,8 @@ private final TilingPaintFactory tilingPaintFactory = new TilingPaintFactory(this); + private final Stack transparencyGroupStack = new Stack(); + /** * Default annotations filter, returns all annotations */ @@ -233,7 +242,6 @@ graphics = (Graphics2D) g; xform = graphics.getTransform(); this.pageSize = pageSize; - pageRotation = getPage().getRotation() % 360; setRenderingHints(); @@ -598,7 +606,7 @@ { throw new IOException("Invalid soft mask subtype."); } - gray = getRotatedImage(gray); + gray = adjustImage(gray); Rectangle2D tpgBounds = transparencyGroup.getBounds(); adjustRectangle(tpgBounds); return new SoftMask(parentPaint, gray, tpgBounds, backdropColor, softMask.getTransferFunction()); @@ -613,55 +621,35 @@ private void adjustRectangle(Rectangle2D r) { Matrix m = new Matrix(xform); - if (pageRotation == 90) - { - r.setRect(pageSize.getHeight() * m.getScalingFactorY() - r.getY() - r.getHeight(), - r.getX(), - r.getWidth(), - r.getHeight()); - } - if (pageRotation == 180) - { - r.setRect(pageSize.getWidth() * m.getScalingFactorX() - r.getX() - r.getWidth(), - pageSize.getHeight() * m.getScalingFactorY() - r.getY() - r.getHeight(), - r.getWidth(), - r.getHeight()); - } - if (pageRotation == 270) - { - r.setRect(r.getY(), - pageSize.getWidth() * m.getScalingFactorX() - r.getX() - r.getWidth(), - r.getWidth(), - r.getHeight()); - } + double scaleX = m.getScalingFactorX(); + double scaleY = m.getScalingFactorY(); + + AffineTransform adjustedTransform = new AffineTransform(xform); + adjustedTransform.scale(1.0 / scaleX, 1.0 / scaleY); + r.setRect(adjustedTransform.createTransformedShape(r).getBounds2D()); } - // return quadrant-rotated image with adjusted size - private BufferedImage getRotatedImage(BufferedImage gray) throws IOException - { - BufferedImage gray2; - AffineTransform at; - switch (pageRotation % 360) - { - case 90: - gray2 = new BufferedImage(gray.getHeight(), gray.getWidth(), BufferedImage.TYPE_BYTE_GRAY); - at = AffineTransform.getQuadrantRotateInstance(1, gray.getHeight() / 2d, gray.getHeight() / 2d); - break; - case 180: - gray2 = new BufferedImage(gray.getWidth(), gray.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - at = AffineTransform.getQuadrantRotateInstance(2, gray.getWidth()/ 2d, gray.getHeight() / 2d); - break; - case 270: - gray2 = new BufferedImage(gray.getHeight(), gray.getWidth(), BufferedImage.TYPE_BYTE_GRAY); - at = AffineTransform.getQuadrantRotateInstance(3, gray.getWidth()/ 2d, gray.getWidth() / 2d); - break; - default: - return gray; - } - Graphics2D g2 = (Graphics2D) gray2.getGraphics(); + // returns the image adjusted for applySoftMaskToPaint(). + private BufferedImage adjustImage(BufferedImage gray) throws IOException + { + AffineTransform at = new AffineTransform(xform); + Matrix m = new Matrix(at); + at.scale(1.0 / m.getScalingFactorX(), 1.0 / m.getScalingFactorY()); + + Rectangle originalBounds = new Rectangle(gray.getWidth(), gray.getHeight()); + Rectangle2D transformedBounds = at.createTransformedShape(originalBounds).getBounds2D(); + at.preConcatenate(AffineTransform.getTranslateInstance(-transformedBounds.getMinX(), + -transformedBounds.getMinY())); + + int width = (int) Math.ceil(transformedBounds.getWidth()); + int height = (int) Math.ceil(transformedBounds.getHeight()); + BufferedImage transformedGray = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + + Graphics2D g2 = (Graphics2D) transformedGray.getGraphics(); + g2.drawImage(gray, at, null); g2.dispose(); - return gray2; + return transformedGray; } // returns the stroking AWT Paint @@ -1243,7 +1231,12 @@ lastClip = null; //TODO support more annotation flags (Invisible, NoZoom, NoRotate) // Example for NoZoom can be found in p5 of PDFBOX-2348 - int deviceType = graphics.getDeviceConfiguration().getDevice().getType(); + int deviceType = -1; + if (graphics.getDeviceConfiguration() != null && + graphics.getDeviceConfiguration().getDevice() != null) + { + deviceType = graphics.getDeviceConfiguration().getDevice().getType(); + } if (deviceType == GraphicsDevice.TYPE_PRINTER && !annotation.isPrinted()) { return; @@ -1449,10 +1442,9 @@ float xScale = Math.abs(m.getScalingFactorX()); float yScale = Math.abs(m.getScalingFactorY()); - // adjust the initial translation (includes the translation used to "help" the rotation) - graphics.setTransform(AffineTransform.getTranslateInstance(xform.getTranslateX(), xform.getTranslateY())); - - graphics.rotate(Math.toRadians(pageRotation)); + AffineTransform transform = new AffineTransform(xform); + transform.scale(1.0 / xScale, 1.0 / yScale); + graphics.setTransform(transform); // adjust bbox (x,y) position at the initial scale + cropbox float x = bbox.getLowerLeftX() - pageSize.getLowerLeftX(); @@ -1496,6 +1488,8 @@ private final int minX; private final int minY; + private final int maxX; + private final int maxY; private final int width; private final int height; @@ -1533,6 +1527,8 @@ bbox = null; minX = 0; minY = 0; + maxX = 0; + maxY = 0; width = 0; height = 0; return; @@ -1547,8 +1543,8 @@ minX = (int) Math.floor(bounds.getMinX()); minY = (int) Math.floor(bounds.getMinY()); - int maxX = (int) Math.floor(bounds.getMaxX()) + 1; - int maxY = (int) Math.floor(bounds.getMaxY()) + 1; + maxX = (int) Math.floor(bounds.getMaxX()) + 1; + maxY = (int) Math.floor(bounds.getMaxY()) + 1; width = maxX - minX; height = maxY - minY; @@ -1562,7 +1558,40 @@ { image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); } + + boolean needsBackdrop = !isSoftMask && !form.getGroup().isIsolated() && + hasBlendMode(form, new HashSet()); + BufferedImage backdropImage = null; + // Position of this group in parent group's coordinates + int backdropX = 0; + int backdropY = 0; + if (needsBackdrop) + { + if (transparencyGroupStack.isEmpty()) + { + // Use the current page as the parent group. + backdropImage = renderer.getPageImage(); + needsBackdrop = backdropImage != null; + backdropX = minX; + backdropY = (backdropImage != null) ? (backdropImage.getHeight() - maxY) : 0; + } + else + { + TransparencyGroup parentGroup = transparencyGroupStack.peek(); + backdropImage = parentGroup.image; + backdropX = minX - parentGroup.minX; + backdropY = parentGroup.maxY - maxY; + } + } + Graphics2D g = image.createGraphics(); + if (needsBackdrop) + { + // backdropImage must be included in group image but not in group alpha. + g.drawImage(backdropImage, 0, 0, width, height, + backdropX, backdropY, backdropX + width, backdropY + height, null); + g = new GroupGraphics(image, g); + } if (isSoftMask && backdropColor != null) { // "If the subtype is Luminosity, the transparency group XObject G shall be @@ -1590,8 +1619,6 @@ minY / Math.abs(m.getScalingFactorY()), (float) bounds.getWidth() / Math.abs(m.getScalingFactorX()), (float) bounds.getHeight() / Math.abs(m.getScalingFactorY())); - int pageRotationOriginal = pageRotation; - pageRotation = 0; int clipWindingRuleOriginal = clipWindingRule; clipWindingRule = -1; GeneralPath linePathOriginal = linePath; @@ -1610,7 +1637,12 @@ } else { + transparencyGroupStack.push(this); processTransparencyGroup(form); + if (!transparencyGroupStack.isEmpty()) + { + transparencyGroupStack.pop(); + } } } finally @@ -1623,7 +1655,11 @@ linePath = linePathOriginal; pageSize = pageSizeOriginal; xform = xformOriginal; - pageRotation = pageRotationOriginal; + + if (needsBackdrop) + { + ((GroupGraphics) g).removeBackdrop(backdropImage, backdropX, backdropY); + } } } @@ -1699,4 +1735,54 @@ width, height); } } + + private boolean hasBlendMode(PDTransparencyGroup group, Set groupsDone) + { + if (groupsDone.contains(group.getCOSObject())) + { + // The group was already processed. Avoid endless recursion. + return false; + } + groupsDone.add(group.getCOSObject()); + + PDResources resources = group.getResources(); + if (resources == null) + { + return false; + } + for (COSName name : resources.getExtGStateNames()) + { + PDExtendedGraphicsState extGState = resources.getExtGState(name); + if (extGState == null) + { + continue; + } + BlendMode blendMode = extGState.getBlendMode(); + if (blendMode != BlendMode.NORMAL) + { + return true; + } + } + + // Recursively process nested transparency groups + for (COSName name : resources.getXObjectNames()) + { + PDXObject xObject; + try + { + xObject = resources.getXObject(name); + } + catch (IOException ex) + { + continue; + } + if (xObject instanceof PDTransparencyGroup && + hasBlendMode((PDTransparencyGroup)xObject, groupsDone)) + { + return true; + } + } + + return false; + } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/rendering/PDFRenderer.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/rendering/PDFRenderer.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/rendering/PDFRenderer.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/rendering/PDFRenderer.java 2018-11-28 17:18:38.000000000 +0000 @@ -1,348 +1,437 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.pdfbox.rendering; - -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; -import java.io.IOException; -import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDResources; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode; -import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; -import org.apache.pdfbox.pdmodel.interactive.annotation.AnnotationFilter; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; - -/** - * Renders a PDF document to an AWT BufferedImage. - * This class may be overridden in order to perform custom rendering. - * - * @author John Hewson - */ -public class PDFRenderer -{ - protected final PDDocument document; - // TODO keep rendering state such as caches here - - /** - * Default annotations filter, returns all annotations - */ - private AnnotationFilter annotationFilter = new AnnotationFilter() - { - @Override - public boolean accept(PDAnnotation annotation) - { - return true; - } - }; - - private boolean subsamplingAllowed = false; - - /** - * Creates a new PDFRenderer. - * @param document the document to render - */ - public PDFRenderer(PDDocument document) - { - this.document = document; - } - - /** - * Return the AnnotationFilter. - * - * @return the AnnotationFilter - */ - public AnnotationFilter getAnnotationsFilter() - { - return annotationFilter; - } - - /** - * Set the AnnotationFilter. - * - *

Allows to only render annotation accepted by the filter. - * - * @param annotationsFilter the AnnotationFilter - */ - public void setAnnotationsFilter(AnnotationFilter annotationsFilter) - { - this.annotationFilter = annotationsFilter; - } - - /** - * Value indicating if the renderer is allowed to subsample images before drawing, according to - * image dimensions and requested scale. - * - * Subsampling may be faster and less memory-intensive in some cases, but it may also lead to - * loss of quality, especially in images with high spatial frequency. - * - * @return true if subsampling of images is allowed, false otherwise. - */ - public boolean isSubsamplingAllowed() - { - return subsamplingAllowed; - } - - /** - * Sets a value instructing the renderer whether it is allowed to subsample images before - * drawing. The subsampling frequency is determined according to image size and requested scale. - * - * Subsampling may be faster and less memory-intensive in some cases, but it may also lead to - * loss of quality, especially in images with high spatial frequency. - * - * @param subsamplingAllowed The new value indicating if subsampling is allowed. - */ - public void setSubsamplingAllowed(boolean subsamplingAllowed) - { - this.subsamplingAllowed = subsamplingAllowed; - } - - /** - * Returns the given page as an RGB image at 72 DPI - * @param pageIndex the zero-based index of the page to be converted. - * @return the rendered page image - * @throws IOException if the PDF cannot be read - */ - public BufferedImage renderImage(int pageIndex) throws IOException - { - return renderImage(pageIndex, 1); - } - - /** - * Returns the given page as an RGB image at the given scale. - * A scale of 1 will render at 72 DPI. - * @param pageIndex the zero-based index of the page to be converted - * @param scale the scaling factor, where 1 = 72 DPI - * @return the rendered page image - * @throws IOException if the PDF cannot be read - */ - public BufferedImage renderImage(int pageIndex, float scale) throws IOException - { - return renderImage(pageIndex, scale, ImageType.RGB); - } - - /** - * Returns the given page as an RGB image at the given DPI. - * @param pageIndex the zero-based index of the page to be converted - * @param dpi the DPI (dots per inch) to render at - * @return the rendered page image - * @throws IOException if the PDF cannot be read - */ - public BufferedImage renderImageWithDPI(int pageIndex, float dpi) throws IOException - { - return renderImage(pageIndex, dpi / 72f, ImageType.RGB); - } - - /** - * Returns the given page as an RGB image at the given DPI. - * @param pageIndex the zero-based index of the page to be converted - * @param dpi the DPI (dots per inch) to render at - * @param imageType the type of image to return - * @return the rendered page image - * @throws IOException if the PDF cannot be read - */ - public BufferedImage renderImageWithDPI(int pageIndex, float dpi, ImageType imageType) - throws IOException - { - return renderImage(pageIndex, dpi / 72f, imageType); - } - - /** - * Returns the given page as an RGB or ARGB image at the given scale. - * @param pageIndex the zero-based index of the page to be converted - * @param scale the scaling factor, where 1 = 72 DPI - * @param imageType the type of image to return - * @return the rendered page image - * @throws IOException if the PDF cannot be read - */ - public BufferedImage renderImage(int pageIndex, float scale, ImageType imageType) - throws IOException - { - PDPage page = document.getPage(pageIndex); - - PDRectangle cropbBox = page.getCropBox(); - float widthPt = cropbBox.getWidth(); - float heightPt = cropbBox.getHeight(); - int widthPx = Math.round(widthPt * scale); - int heightPx = Math.round(heightPt * scale); - int rotationAngle = page.getRotation(); - - int bimType = imageType.toBufferedImageType(); - if (imageType != ImageType.ARGB && hasBlendMode(page)) - { - // PDFBOX-4095: if the PDF has blending on the top level, draw on transparent background - // Inpired from PDF.js: if a PDF page uses any blend modes other than Normal, - // PDF.js renders everything on a fully transparent RGBA canvas. - // Finally when the page has been rendered, PDF.js draws the RGBA canvas on a white canvas. - bimType = BufferedImage.TYPE_INT_ARGB; - } - - // swap width and height - BufferedImage image; - if (rotationAngle == 90 || rotationAngle == 270) - { - image = new BufferedImage(heightPx, widthPx, bimType); - } - else - { - image = new BufferedImage(widthPx, heightPx, bimType); - } - - // use a transparent background if the image type supports alpha - Graphics2D g = image.createGraphics(); - if (image.getType() == BufferedImage.TYPE_INT_ARGB) - { - g.setBackground(new Color(0, 0, 0, 0)); - } - else - { - g.setBackground(Color.WHITE); - } - g.clearRect(0, 0, image.getWidth(), image.getHeight()); - - transform(g, page, scale); - - // the end-user may provide a custom PageDrawer - PageDrawerParameters parameters = new PageDrawerParameters(this, page, subsamplingAllowed); - PageDrawer drawer = createPageDrawer(parameters); - drawer.drawPage(g, page.getCropBox()); - - g.dispose(); - - if (image.getType() != imageType.toBufferedImageType()) - { - // PDFBOX-4095: draw temporary transparent image on white background - BufferedImage newImage = - new BufferedImage(image.getWidth(), image.getHeight(), imageType.toBufferedImageType()); - Graphics2D dstGraphics = newImage.createGraphics(); - dstGraphics.setBackground(Color.WHITE); - dstGraphics.clearRect(0, 0, image.getWidth(), image.getHeight()); - dstGraphics.drawImage(image, 0, 0, null); - dstGraphics.dispose(); - image = newImage; - } - - return image; - } - - /** - * Renders a given page to an AWT Graphics2D instance. - * @param pageIndex the zero-based index of the page to be converted - * @param graphics the Graphics2D on which to draw the page - * @throws IOException if the PDF cannot be read - */ - public void renderPageToGraphics(int pageIndex, Graphics2D graphics) throws IOException - { - renderPageToGraphics(pageIndex, graphics, 1); - } - - /** - * Renders a given page to an AWT Graphics2D instance. - * @param pageIndex the zero-based index of the page to be converted - * @param graphics the Graphics2D on which to draw the page - * @param scale the scale to draw the page at - * @throws IOException if the PDF cannot be read - */ - public void renderPageToGraphics(int pageIndex, Graphics2D graphics, float scale) - throws IOException - { - PDPage page = document.getPage(pageIndex); - // TODO need width/wight calculations? should these be in PageDrawer? - - transform(graphics, page, scale); - - PDRectangle cropBox = page.getCropBox(); - graphics.clearRect(0, 0, (int) cropBox.getWidth(), (int) cropBox.getHeight()); - - // the end-user may provide a custom PageDrawer - PageDrawerParameters parameters = new PageDrawerParameters(this, page, subsamplingAllowed); - PageDrawer drawer = createPageDrawer(parameters); - drawer.drawPage(graphics, cropBox); - } - - // scale rotate translate - private void transform(Graphics2D graphics, PDPage page, float scale) - { - graphics.scale(scale, scale); - - // TODO should we be passing the scale to PageDrawer rather than messing with Graphics? - int rotationAngle = page.getRotation(); - PDRectangle cropBox = page.getCropBox(); - - if (rotationAngle != 0) - { - float translateX = 0; - float translateY = 0; - switch (rotationAngle) - { - case 90: - translateX = cropBox.getHeight(); - break; - case 270: - translateY = cropBox.getWidth(); - break; - case 180: - translateX = cropBox.getWidth(); - translateY = cropBox.getHeight(); - break; - default: - break; - } - graphics.translate(translateX, translateY); - graphics.rotate((float) Math.toRadians(rotationAngle)); - } - } - - /** - * Returns a new PageDrawer instance, using the given parameters. May be overridden. - */ - protected PageDrawer createPageDrawer(PageDrawerParameters parameters) throws IOException - { - PageDrawer pageDrawer = new PageDrawer(parameters); - pageDrawer.setAnnotationFilter(annotationFilter); - return pageDrawer; - } - - private boolean hasBlendMode(PDPage page) - { - // check the current resources for blend modes - PDResources resources = page.getResources(); - if (resources == null) - { - return false; - } - for (COSName name : resources.getExtGStateNames()) - { - PDExtendedGraphicsState extGState = resources.getExtGState(name); - if (extGState == null) - { - // can happen if key exists but no value - // see PDFBOX-3950-23EGDHXSBBYQLKYOKGZUOVYVNE675PRD.pdf - continue; - } - BlendMode blendMode = extGState.getBlendMode(); - if (blendMode != BlendMode.NORMAL) - { - return true; - } - } - return false; - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.rendering; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.StringTokenizer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode; +import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; +import org.apache.pdfbox.pdmodel.interactive.annotation.AnnotationFilter; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; + +/** + * Renders a PDF document to an AWT BufferedImage. + * This class may be overridden in order to perform custom rendering. + * + * @author John Hewson + */ +public class PDFRenderer +{ + private static final Log LOG = LogFactory.getLog(PDFRenderer.class); + + protected final PDDocument document; + // TODO keep rendering state such as caches here + + /** + * Default annotations filter, returns all annotations + */ + private AnnotationFilter annotationFilter = new AnnotationFilter() + { + @Override + public boolean accept(PDAnnotation annotation) + { + return true; + } + }; + + private boolean subsamplingAllowed = false; + + private BufferedImage pageImage; + + private static boolean kcmsLogged = false; + + /** + * Creates a new PDFRenderer. + * @param document the document to render + */ + public PDFRenderer(PDDocument document) + { + this.document = document; + + if (!kcmsLogged) + { + suggestKCMS(); + kcmsLogged = true; + } + } + + /** + * Return the AnnotationFilter. + * + * @return the AnnotationFilter + */ + public AnnotationFilter getAnnotationsFilter() + { + return annotationFilter; + } + + /** + * Set the AnnotationFilter. + * + *

Allows to only render annotation accepted by the filter. + * + * @param annotationsFilter the AnnotationFilter + */ + public void setAnnotationsFilter(AnnotationFilter annotationsFilter) + { + this.annotationFilter = annotationsFilter; + } + + /** + * Value indicating if the renderer is allowed to subsample images before drawing, according to + * image dimensions and requested scale. + * + * Subsampling may be faster and less memory-intensive in some cases, but it may also lead to + * loss of quality, especially in images with high spatial frequency. + * + * @return true if subsampling of images is allowed, false otherwise. + */ + public boolean isSubsamplingAllowed() + { + return subsamplingAllowed; + } + + /** + * Sets a value instructing the renderer whether it is allowed to subsample images before + * drawing. The subsampling frequency is determined according to image size and requested scale. + * + * Subsampling may be faster and less memory-intensive in some cases, but it may also lead to + * loss of quality, especially in images with high spatial frequency. + * + * @param subsamplingAllowed The new value indicating if subsampling is allowed. + */ + public void setSubsamplingAllowed(boolean subsamplingAllowed) + { + this.subsamplingAllowed = subsamplingAllowed; + } + + /** + * Returns the given page as an RGB image at 72 DPI + * @param pageIndex the zero-based index of the page to be converted. + * @return the rendered page image + * @throws IOException if the PDF cannot be read + */ + public BufferedImage renderImage(int pageIndex) throws IOException + { + return renderImage(pageIndex, 1); + } + + /** + * Returns the given page as an RGB image at the given scale. + * A scale of 1 will render at 72 DPI. + * @param pageIndex the zero-based index of the page to be converted + * @param scale the scaling factor, where 1 = 72 DPI + * @return the rendered page image + * @throws IOException if the PDF cannot be read + */ + public BufferedImage renderImage(int pageIndex, float scale) throws IOException + { + return renderImage(pageIndex, scale, ImageType.RGB); + } + + /** + * Returns the given page as an RGB image at the given DPI. + * @param pageIndex the zero-based index of the page to be converted + * @param dpi the DPI (dots per inch) to render at + * @return the rendered page image + * @throws IOException if the PDF cannot be read + */ + public BufferedImage renderImageWithDPI(int pageIndex, float dpi) throws IOException + { + return renderImage(pageIndex, dpi / 72f, ImageType.RGB); + } + + /** + * Returns the given page as an RGB image at the given DPI. + * @param pageIndex the zero-based index of the page to be converted + * @param dpi the DPI (dots per inch) to render at + * @param imageType the type of image to return + * @return the rendered page image + * @throws IOException if the PDF cannot be read + */ + public BufferedImage renderImageWithDPI(int pageIndex, float dpi, ImageType imageType) + throws IOException + { + return renderImage(pageIndex, dpi / 72f, imageType); + } + + /** + * Returns the given page as an RGB or ARGB image at the given scale. + * @param pageIndex the zero-based index of the page to be converted + * @param scale the scaling factor, where 1 = 72 DPI + * @param imageType the type of image to return + * @return the rendered page image + * @throws IOException if the PDF cannot be read + */ + public BufferedImage renderImage(int pageIndex, float scale, ImageType imageType) + throws IOException + { + PDPage page = document.getPage(pageIndex); + + PDRectangle cropbBox = page.getCropBox(); + float widthPt = cropbBox.getWidth(); + float heightPt = cropbBox.getHeight(); + + // PDFBOX-4306 avoid single blank pixel line on the right or on the bottom + int widthPx = (int) Math.max(Math.floor(widthPt * scale), 1); + int heightPx = (int) Math.max(Math.floor(heightPt * scale), 1); + + int rotationAngle = page.getRotation(); + + int bimType = imageType.toBufferedImageType(); + if (imageType != ImageType.ARGB && hasBlendMode(page)) + { + // PDFBOX-4095: if the PDF has blending on the top level, draw on transparent background + // Inpired from PDF.js: if a PDF page uses any blend modes other than Normal, + // PDF.js renders everything on a fully transparent RGBA canvas. + // Finally when the page has been rendered, PDF.js draws the RGBA canvas on a white canvas. + bimType = BufferedImage.TYPE_INT_ARGB; + } + + // swap width and height + BufferedImage image; + if (rotationAngle == 90 || rotationAngle == 270) + { + image = new BufferedImage(heightPx, widthPx, bimType); + } + else + { + image = new BufferedImage(widthPx, heightPx, bimType); + } + + pageImage = image; + + // use a transparent background if the image type supports alpha + Graphics2D g = image.createGraphics(); + if (image.getType() == BufferedImage.TYPE_INT_ARGB) + { + g.setBackground(new Color(0, 0, 0, 0)); + } + else + { + g.setBackground(Color.WHITE); + } + g.clearRect(0, 0, image.getWidth(), image.getHeight()); + + transform(g, page, scale, scale); + + // the end-user may provide a custom PageDrawer + PageDrawerParameters parameters = new PageDrawerParameters(this, page, subsamplingAllowed); + PageDrawer drawer = createPageDrawer(parameters); + drawer.drawPage(g, page.getCropBox()); + + g.dispose(); + + if (image.getType() != imageType.toBufferedImageType()) + { + // PDFBOX-4095: draw temporary transparent image on white background + BufferedImage newImage = + new BufferedImage(image.getWidth(), image.getHeight(), imageType.toBufferedImageType()); + Graphics2D dstGraphics = newImage.createGraphics(); + dstGraphics.setBackground(Color.WHITE); + dstGraphics.clearRect(0, 0, image.getWidth(), image.getHeight()); + dstGraphics.drawImage(image, 0, 0, null); + dstGraphics.dispose(); + image = newImage; + } + + return image; + } + + /** + * Renders a given page to an AWT Graphics2D instance. + * @param pageIndex the zero-based index of the page to be converted + * @param graphics the Graphics2D on which to draw the page + * @throws IOException if the PDF cannot be read + */ + public void renderPageToGraphics(int pageIndex, Graphics2D graphics) throws IOException + { + renderPageToGraphics(pageIndex, graphics, 1); + } + + /** + * Renders a given page to an AWT Graphics2D instance. + * @param pageIndex the zero-based index of the page to be converted + * @param graphics the Graphics2D on which to draw the page + * @param scale the scale to draw the page at + * @throws IOException if the PDF cannot be read + */ + public void renderPageToGraphics(int pageIndex, Graphics2D graphics, float scale) + throws IOException + { + renderPageToGraphics(pageIndex, graphics, scale, scale); + } + + /** + * Renders a given page to an AWT Graphics2D instance. + * + * @param pageIndex the zero-based index of the page to be converted + * @param graphics the Graphics2D on which to draw the page + * @param scaleX the scale to draw the page at for the x-axis + * @param scaleY the scale to draw the page at for the y-axis + * @throws IOException if the PDF cannot be read + */ + public void renderPageToGraphics(int pageIndex, Graphics2D graphics, float scaleX, float scaleY) + throws IOException + { + PDPage page = document.getPage(pageIndex); + // TODO need width/wight calculations? should these be in PageDrawer? + + transform(graphics, page, scaleX, scaleY); + + PDRectangle cropBox = page.getCropBox(); + graphics.clearRect(0, 0, (int) cropBox.getWidth(), (int) cropBox.getHeight()); + + // the end-user may provide a custom PageDrawer + PageDrawerParameters parameters = new PageDrawerParameters(this, page, subsamplingAllowed); + PageDrawer drawer = createPageDrawer(parameters); + drawer.drawPage(graphics, cropBox); + } + + // scale rotate translate + private void transform(Graphics2D graphics, PDPage page, float scaleX, float scaleY) + { + graphics.scale(scaleX, scaleY); + + // TODO should we be passing the scale to PageDrawer rather than messing with Graphics? + int rotationAngle = page.getRotation(); + PDRectangle cropBox = page.getCropBox(); + + if (rotationAngle != 0) + { + float translateX = 0; + float translateY = 0; + switch (rotationAngle) + { + case 90: + translateX = cropBox.getHeight(); + break; + case 270: + translateY = cropBox.getWidth(); + break; + case 180: + translateX = cropBox.getWidth(); + translateY = cropBox.getHeight(); + break; + default: + break; + } + graphics.translate(translateX, translateY); + graphics.rotate((float) Math.toRadians(rotationAngle)); + } + } + + /** + * Returns a new PageDrawer instance, using the given parameters. May be overridden. + */ + protected PageDrawer createPageDrawer(PageDrawerParameters parameters) throws IOException + { + PageDrawer pageDrawer = new PageDrawer(parameters); + pageDrawer.setAnnotationFilter(annotationFilter); + return pageDrawer; + } + + private boolean hasBlendMode(PDPage page) + { + // check the current resources for blend modes + PDResources resources = page.getResources(); + if (resources == null) + { + return false; + } + for (COSName name : resources.getExtGStateNames()) + { + PDExtendedGraphicsState extGState = resources.getExtGState(name); + if (extGState == null) + { + // can happen if key exists but no value + // see PDFBOX-3950-23EGDHXSBBYQLKYOKGZUOVYVNE675PRD.pdf + continue; + } + BlendMode blendMode = extGState.getBlendMode(); + if (blendMode != BlendMode.NORMAL) + { + return true; + } + } + return false; + } + + /** + * Returns the image to which the current page is being rendered. + * May be null if the page is rendered to a Graphics2D object + * instead of a BufferedImage. + */ + BufferedImage getPageImage() + { + return pageImage; + } + + private static void suggestKCMS() + { + String cmmProperty = System.getProperty("sun.java2d.cmm"); + if (isMinJdk8() && !"sun.java2d.cmm.kcms.KcmsServiceProvider".equals(cmmProperty)) + { + try + { + // Make sure that class exists + Class.forName("sun.java2d.cmm.kcms.KcmsServiceProvider"); + + LOG.info("To get higher rendering speed on java 8 or 9,"); + LOG.info(" use the option -Dsun.java2d.cmm=sun.java2d.cmm.kcms.KcmsServiceProvider"); + LOG.info(" or call System.setProperty(\"sun.java2d.cmm\", \"sun.java2d.cmm.kcms.KcmsServiceProvider\")"); + } + catch (ClassNotFoundException e) + { + // jdk 10 and higher + LOG.debug("KCMS doesn't exist anymore. SO SAD!"); + } + } + } + + private static boolean isMinJdk8() + { + // strategy from lucene-solr/lucene/core/src/java/org/apache/lucene/util/Constants.java + String version = System.getProperty("java.specification.version"); + final StringTokenizer st = new StringTokenizer(version, "."); + try + { + int major = Integer.parseInt(st.nextToken()); + int minor = 0; + if (st.hasMoreTokens()) + { + minor = Integer.parseInt(st.nextToken()); + } + return major > 1 || (major == 1 && minor >= 8); + } + catch (NumberFormatException nfe) + { + // maybe some new numbering scheme in the 22nd century + return true; + } + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/rendering/TilingPaintFactory.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/rendering/TilingPaintFactory.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/rendering/TilingPaintFactory.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/rendering/TilingPaintFactory.java 2018-11-28 17:18:38.000000000 +0000 @@ -15,6 +15,7 @@ */ package org.apache.pdfbox.rendering; +import java.awt.Paint; import java.awt.geom.AffineTransform; import java.io.IOException; import java.lang.ref.WeakReference; @@ -34,21 +35,21 @@ class TilingPaintFactory { private final PageDrawer drawer; - private final Map> weakCache - = new WeakHashMap>(); + private final Map> weakCache + = new WeakHashMap>(); TilingPaintFactory(PageDrawer drawer) { this.drawer = drawer; } - TilingPaint create(PDTilingPattern pattern, PDColorSpace colorSpace, + Paint create(PDTilingPattern pattern, PDColorSpace colorSpace, PDColor color, AffineTransform xform) throws IOException { - TilingPaint paint = null; + Paint paint = null; TilingPaintParameter tilingPaintParameter = new TilingPaintParameter(drawer.getInitialMatrix(), pattern.getCOSObject(), colorSpace, color, xform); - WeakReference weakRef = weakCache.get(tilingPaintParameter); + WeakReference weakRef = weakCache.get(tilingPaintParameter); if (weakRef != null) { // PDFBOX-4058: additional WeakReference makes gc work better @@ -57,7 +58,7 @@ if (paint == null) { paint = new TilingPaint(drawer, pattern, colorSpace, color, xform); - weakCache.put(tilingPaintParameter, new WeakReference(paint)); + weakCache.put(tilingPaintParameter, new WeakReference(paint)); } return paint; } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/rendering/TilingPaint.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/rendering/TilingPaint.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/rendering/TilingPaint.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/rendering/TilingPaint.java 2018-11-28 17:18:38.000000000 +0000 @@ -46,7 +46,7 @@ class TilingPaint implements Paint { private static final Log LOG = LogFactory.getLog(TilingPaint.class); - private final TexturePaint paint; + private final Paint paint; private final Matrix patternMatrix; private static final int MAXEDGE; private static final String DEFAULTMAXEDGE = "3000"; diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/text/LegacyPDFStreamEngine.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/text/LegacyPDFStreamEngine.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/text/LegacyPDFStreamEngine.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/text/LegacyPDFStreamEngine.java 2018-11-28 17:18:38.000000000 +0000 @@ -116,7 +116,7 @@ } /** - * This will initialise and process the contents of the stream. + * This will initialize and process the contents of the stream. * * @param page the page to process * @throws java.io.IOException if there is an error accessing the stream. diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/text/PDFTextStripper.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/text/PDFTextStripper.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/text/PDFTextStripper.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/text/PDFTextStripper.java 2018-11-28 17:18:38.000000000 +0000 @@ -638,14 +638,7 @@ float expectedStartOfNextWordX = EXPECTED_START_OF_NEXT_WORD_X_RESET_VALUE; if (endOfLastTextX != END_OF_LAST_TEXT_X_RESET_VALUE) { - if (deltaCharWidth > deltaSpace) - { - expectedStartOfNextWordX = endOfLastTextX + deltaSpace; - } - else - { - expectedStartOfNextWordX = endOfLastTextX + deltaCharWidth; - } + expectedStartOfNextWordX = endOfLastTextX + Math.min(deltaSpace, deltaCharWidth); } if (lastPosition != null) diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/util/filetypedetector/FileTypeDetector.java libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/util/filetypedetector/FileTypeDetector.java --- libpdfbox2-java-2.0.9/pdfbox/src/main/java/org/apache/pdfbox/util/filetypedetector/FileTypeDetector.java 2018-03-20 16:19:48.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/main/java/org/apache/pdfbox/util/filetypedetector/FileTypeDetector.java 2018-11-28 17:18:38.000000000 +0000 @@ -60,7 +60,8 @@ root.addPath(FileType.PCX, new byte[]{0x0A, 0x05, 0x01}); root.addPath(FileType.RIFF, "RIFF".getBytes(Charsets.ISO_8859_1)); - root.addPath(FileType.ARW, "II".getBytes(Charsets.ISO_8859_1), new byte[]{0x2a, 0x00, 0x08, 0x00}); + // https://github.com/drewnoakes/metadata-extractor/issues/217 + //root.addPath(FileType.ARW, "II".getBytes(Charsets.ISO_8859_1), new byte[]{0x2a, 0x00, 0x08, 0x00}) root.addPath(FileType.CRW, "II".getBytes(Charsets.ISO_8859_1), new byte[]{0x1a, 0x00, 0x00, 0x00}, "HEAPCCDR".getBytes(Charsets.ISO_8859_1)); root.addPath(FileType.CR2, "II".getBytes(Charsets.ISO_8859_1), new byte[]{0x2a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x43, 0x52}); root.addPath(FileType.NEF, "MM".getBytes(Charsets.ISO_8859_1), new byte[]{0x00, 0x2a, 0x00, 0x00, 0x00, (byte)0x80, 0x00}); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/cos/COSObjectKeyTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/cos/COSObjectKeyTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/cos/COSObjectKeyTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/cos/COSObjectKeyTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.cos; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class COSObjectKeyTest +{ + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void compareToInputNotNullOutputZero() + { + // Arrange + final COSObjectKey objectUnderTest = new COSObjectKey(0L, 0); + final COSObjectKey other = new COSObjectKey(0L, 0); + + // Act + final int retval = objectUnderTest.compareTo(other); + + // Assert result + Assert.assertEquals(0, retval); + } + + @Test + public void compareToInputNotNullOutputPositive() + { + // Arrange + final COSObjectKey objectUnderTest = new COSObjectKey(0L, 0); + final COSObjectKey other = new COSObjectKey(-9223372036854775808L, 0); + + // Act + final int retval = objectUnderTest.compareTo(other); + + // Assert result + Assert.assertEquals(1, retval); + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSFloat.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSFloat.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSFloat.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSFloat.java 2018-11-28 17:18:34.000000000 +0000 @@ -300,6 +300,13 @@ writePDFTester.runTest(0.000000000000000000000000000000001f); } + public void testDoubleNegative() throws IOException + { + // PDFBOX-4289 + COSFloat cosFloat = new COSFloat("--16.33"); + assertEquals(-16.33f, cosFloat.floatValue()); + } + private String floatToString(float value) { // use a BigDecimal as intermediate state to avoid diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSStream.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSStream.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSStream.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSStream.java 2018-11-28 17:18:34.000000000 +0000 @@ -129,6 +129,25 @@ validateDecoded(stream, testString); } + /** + * Tests tests that encoding is done correctly even if the the stream is closed twice. + * Closeable.close() allows streams to be closed multiple times. The second and subsequent + * close() calls should have no effect. + * + * @throws IOException + */ + public void testCompressedStreamDoubleClose() throws IOException + { + byte[] testString = "This is a test string to be used as input for TestCOSStream".getBytes("ASCII"); + byte[] testStringEncoded = encodeData(testString, COSName.FLATE_DECODE); + COSStream stream = new COSStream(); + OutputStream output = stream.createOutputStream(COSName.FLATE_DECODE); + output.write(testString); + output.close(); + output.close(); + validateEncoded(stream, testStringEncoded); + } + private byte[] encodeData(byte[] original, COSName filter) throws IOException { Filter encodingFilter = FilterFactory.INSTANCE.getFilter(filter); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/encryption/TestSymmetricKeyEncryption.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/encryption/TestSymmetricKeyEncryption.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/encryption/TestSymmetricKeyEncryption.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/encryption/TestSymmetricKeyEncryption.java 2018-11-28 17:18:34.000000000 +0000 @@ -210,6 +210,28 @@ } /** + * PDFBOX-4308: test that index colorspace table string doesn't get + * corrupted when encrypting. This happened because the colorspace was + * referenced twice, once in the resources dictionary and once in an image + * in the resources dictionary, and when saving the PDF the string was saved + * twice, once as a direct object and once as an indirect object (both from + * the same java object). Encryption used the wrong object number and/or the + * object was encrypted twice. + * + * @throws IOException + */ + public void testPDFBox4308() throws IOException + { + InputStream is = new FileInputStream("target/pdfs/PDFBOX-4308.pdf"); + byte[] inputFileAsByteArray = IOUtils.toByteArray(is); + is.close(); + int sizePriorToEncryption = inputFileAsByteArray.length; + + testSymmEncrForKeySize(40, false, sizePriorToEncryption, inputFileAsByteArray, + USERPASSWORD, OWNERPASSWORD, permission); + } + + /** * Protect a document with an embedded PDF with a key and try to reopen it * with that key and compare. * diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/multipdf/PDFMergerUtilityTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/multipdf/PDFMergerUtilityTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/multipdf/PDFMergerUtilityTest.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/multipdf/PDFMergerUtilityTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -17,7 +17,11 @@ import java.awt.image.BufferedImage; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.util.HashSet; +import java.util.Set; import junit.framework.TestCase; import org.apache.pdfbox.cos.COSArray; @@ -174,10 +178,10 @@ PDDocument dst = PDDocument.load(new File(TARGETPDFDIR, "PDFBOX-3999-GeneralForbearance.pdf")); pdfMergerUtility.appendDocument(dst, src); src.close(); - dst.save(new File(TARGETTESTDIR, "PDFBOX-3999-GovFormPreFlattened-merged.pdf")); + dst.save(new File(TARGETTESTDIR, "PDFBOX-3999-GeneralForbearance-merged.pdf")); dst.close(); - PDDocument doc = PDDocument.load(new File(TARGETTESTDIR, "PDFBOX-3999-GovFormPreFlattened-merged.pdf")); + PDDocument doc = PDDocument.load(new File(TARGETTESTDIR, "PDFBOX-3999-GeneralForbearance-merged.pdf")); PDPageTree pageTree = doc.getPages(); // check for orphan pages in the StructTreeRoot/K and StructTreeRoot/ParentTree trees. @@ -186,9 +190,126 @@ checkElement(pageTree, structureTreeRoot.getK()); } + /** + * PDFBOX-3999: check that no streams are kept from the source document by the destination + * document, despite orphan annotations remaining in the structure tree. + * + * @throws IOException + */ + public void testStructureTreeMerge2() throws IOException + { + PDFMergerUtility pdfMergerUtility = new PDFMergerUtility(); + PDDocument doc = PDDocument.load(new File(TARGETPDFDIR, "PDFBOX-3999-GeneralForbearance.pdf")); + doc.getDocumentCatalog().getAcroForm().flatten(); + doc.save(new File(TARGETTESTDIR, "PDFBOX-3999-GeneralForbearance-flattened.pdf")); + + ElementCounter elementCounter = new ElementCounter(); + elementCounter.walk(doc.getDocumentCatalog().getStructureTreeRoot().getK()); + int singleCnt = elementCounter.cnt; + int singleSetSize = elementCounter.set.size(); + + doc.close(); + + PDDocument src = PDDocument.load(new File(TARGETTESTDIR, "PDFBOX-3999-GeneralForbearance-flattened.pdf")); + PDDocument dst = PDDocument.load(new File(TARGETTESTDIR, "PDFBOX-3999-GeneralForbearance-flattened.pdf")); + pdfMergerUtility.appendDocument(dst, src); + // before solving PDFBOX-3999, the close() below brought + // IOException: COSStream has been closed and cannot be read. + src.close(); + dst.save(new File(TARGETTESTDIR, "PDFBOX-3999-GeneralForbearance-flattened-merged.pdf")); + dst.close(); + + doc = PDDocument.load(new File(TARGETTESTDIR, "PDFBOX-3999-GeneralForbearance-flattened-merged.pdf")); + PDPageTree pageTree = doc.getPages(); + + // check for orphan pages in the StructTreeRoot/K and StructTreeRoot/ParentTree trees. + PDStructureTreeRoot structureTreeRoot = doc.getDocumentCatalog().getStructureTreeRoot(); + checkElement(pageTree, structureTreeRoot.getParentTree().getCOSObject()); + checkElement(pageTree, structureTreeRoot.getK()); + + // Assume that the merged tree has double element count + elementCounter = new ElementCounter(); + elementCounter.walk(structureTreeRoot.getK()); + assertEquals(singleCnt * 2, elementCounter.cnt); + assertEquals(singleSetSize * 2, elementCounter.set.size()); + + doc.close(); + } + + /** + * PDFBOX-4383: Test that file can be deleted after merge. + * + * @throws IOException + */ + public void testFileDeletion() throws IOException + { + File outFile = new File(TARGETTESTDIR, "PDFBOX-4383-result.pdf"); + + File inFile1 = new File(TARGETTESTDIR, "PDFBOX-4383-src1.pdf"); + File inFile2 = new File(TARGETTESTDIR, "PDFBOX-4383-src2.pdf"); + + createSimpleFile(inFile1); + createSimpleFile(inFile2); + + OutputStream out = new FileOutputStream(outFile); + PDFMergerUtility merger = new PDFMergerUtility(); + merger.setDestinationStream(out); + merger.addSource(inFile1); + merger.addSource(inFile2); + merger.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly()); + out.close(); + + assertTrue(inFile1.delete()); + assertTrue(inFile2.delete()); + assertTrue(outFile.delete()); + } + + private void createSimpleFile(File file) throws IOException + { + PDDocument doc = new PDDocument(); + doc.addPage(new PDPage()); + doc.save(file); + doc.close(); + } + + private class ElementCounter + { + int cnt = 0; + Set set = new HashSet(); + + void walk(COSBase base) + { + if (base instanceof COSArray) + { + for (COSBase base2 : (COSArray) base) + { + if (base2 instanceof COSObject) + { + base2 = ((COSObject) base2).getObject(); + } + walk(base2); + } + } + else if (base instanceof COSDictionary) + { + COSDictionary kdict = (COSDictionary) base; + if (kdict.containsKey(COSName.PG)) + { + ++cnt; + set.add(kdict); + } + if (kdict.containsKey(COSName.K)) + { + walk(kdict.getDictionaryObject(COSName.K)); + } + } + } + } + // Each element can be an array, a dictionary or a number. - // See PDF specification Table 37 – Entries in a number tree node dictionary - // See PDF specification Table 322 – Entries in the structure tree root + // See PDF specification Table 37 - Entries in a number tree node dictionary + // See PDF specification Table 322 - Entries in the structure tree root + // See PDF specification Table 323 - Entries in a structure element dictionary // example of file with /Kids: 000153.pdf 000208.pdf 000314.pdf 000359.pdf 000671.pdf // from digitalcorpora site private void checkElement(PDPageTree pageTree, COSBase base) diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdfparser/TestPDFParser.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdfparser/TestPDFParser.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdfparser/TestPDFParser.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdfparser/TestPDFParser.java 2018-11-28 17:18:34.000000000 +0000 @@ -308,6 +308,30 @@ PDDocument.load(new File(TARGETPDFDIR, "genko_oc_shiryo1.pdf")).close(); } + /** + * Test parsing the file from PDFBOX-4338, which brought an + * ArrayIndexOutOfBoundsException before the bug was fixed. + * + * @throws IOException + */ + @Test + public void testPDFBox4338() throws IOException + { + PDDocument.load(new File(TARGETPDFDIR, "PDFBOX-4338.pdf")).close(); + } + + /** + * Test parsing the file from PDFBOX-4339, which brought a + * NullPointerException before the bug was fixed. + * + * @throws IOException + */ + @Test + public void testPDFBox4339() throws IOException + { + PDDocument.load(new File(TARGETPDFDIR, "PDFBOX-4339.pdf")).close(); + } + private void executeParserTest(RandomAccessRead source, MemoryUsageSetting memUsageSetting) throws IOException { ScratchFile scratchFile = new ScratchFile(memUsageSetting); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdfwriter/COSWriterTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdfwriter/COSWriterTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdfwriter/COSWriterTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdfwriter/COSWriterTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdfwriter; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.junit.Test; + +public class COSWriterTest +{ + /** + * PDFBOX-4241: check whether the output stream is closed twice. + * + * @throws IOException + */ + @Test + public void testPDFBox4241() throws IOException + { + PDDocument doc = new PDDocument(); + PDPage page = new PDPage(); + doc.addPage(page); + doc.save(new BufferedOutputStream(new ByteArrayOutputStream(1024) + { + private boolean open = true; + + @Override + public void close() throws IOException + { + //Thread.dumpStack(); + + open = false; + super.close(); + } + + @Override + public void flush() throws IOException + { + if (!open) + { + throw new IOException("Stream already closed"); + } + + //Thread.dumpStack(); + } + })); + doc.close(); + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdfwriter/COSWriterXRefEntryTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdfwriter/COSWriterXRefEntryTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdfwriter/COSWriterXRefEntryTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdfwriter/COSWriterXRefEntryTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdfwriter; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class COSWriterXRefEntryTest +{ + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void compareToInputNullOutputNegative() + { + // Arrange + final COSWriterXRefEntry objectUnderTest = new COSWriterXRefEntry(0L, null, null); + final COSWriterXRefEntry obj = null; + + // Act + final int retval = objectUnderTest.compareTo(obj); + + // Assert result + Assert.assertEquals(-1, retval); + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestEmbeddedFiles.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestEmbeddedFiles.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestEmbeddedFiles.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestEmbeddedFiles.java 2018-11-28 17:18:34.000000000 +0000 @@ -84,7 +84,7 @@ PDDocumentNameDictionary names = catalog.getNames(); PDEmbeddedFilesNameTreeNode treeNode = names.getEmbeddedFiles(); List> kids = treeNode.getKids(); - for (PDNameTreeNode kid : kids) + for (PDNameTreeNode kid : kids) { Map tmpNames = kid.getNames(); COSObjectable obj = tmpNames.get("My first attachment"); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestPDNameTreeNode.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestPDNameTreeNode.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestPDNameTreeNode.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestPDNameTreeNode.java 2018-11-28 17:18:34.000000000 +0000 @@ -18,7 +18,7 @@ import java.io.IOException; import java.util.List; -import java.util.SortedMap; +import java.util.Map; import java.util.TreeMap; import junit.framework.TestCase; import org.apache.pdfbox.cos.COSInteger; @@ -31,18 +31,17 @@ */ public class TestPDNameTreeNode extends TestCase { - - private PDNameTreeNode node1; - private PDNameTreeNode node2; - private PDNameTreeNode node4; - private PDNameTreeNode node5; - private PDNameTreeNode node24; + private PDNameTreeNode node1; + private PDNameTreeNode node2; + private PDNameTreeNode node4; + private PDNameTreeNode node5; + private PDNameTreeNode node24; @Override protected void setUp() throws Exception { this.node5 = new PDIntegerNameTreeNode(); - SortedMap names = new TreeMap(); + Map names = new TreeMap(); names.put("Actinium", COSInteger.get(89)); names.put("Aluminum", COSInteger.get(13)); names.put("Americium", COSInteger.get(95)); @@ -53,7 +52,7 @@ this.node5.setNames(names); this.node24 = new PDIntegerNameTreeNode(); - names = new TreeMap(); + names = new TreeMap(); names.put("Xenon", COSInteger.get(54)); names.put("Ytterbium", COSInteger.get(70)); names.put("Yttrium", COSInteger.get(39)); @@ -62,10 +61,10 @@ this.node24.setNames(names); this.node2 = new PDIntegerNameTreeNode(); - List kids = this.node2.getKids(); + List> kids = this.node2.getKids(); if (kids == null) { - kids = new COSArrayList(); + kids = new COSArrayList>(); } kids.add(this.node5); this.node2.setKids(kids); @@ -74,7 +73,7 @@ kids = this.node4.getKids(); if (kids == null) { - kids = new COSArrayList(); + kids = new COSArrayList>(); } kids.add(this.node24); this.node4.setKids(kids); @@ -83,14 +82,13 @@ kids = this.node1.getKids(); if (kids == null) { - kids = new COSArrayList(); + kids = new COSArrayList>(); } kids.add(this.node2); kids.add(this.node4); this.node1.setKids(kids); } - public void testUpperLimit() throws IOException { Assert.assertEquals("Astatine", this.node5.getUpperLimit()); @@ -112,5 +110,4 @@ Assert.assertEquals(null, this.node1.getLowerLimit()); } - } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElementTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElementTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElementTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElementTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSObject; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.Assert; +import org.junit.Test; + +/** + * + * @author Tilman Hausherr + */ +public class PDStructureElementTest +{ + private static final File TARGETPDFDIR = new File("target/pdfs"); + + /** + * PDFBOX-4197: test that object references in array attributes of a PDStructureElement are caught. + * + * @throws IOException + */ + @Test + public void testPDFBox4197() throws IOException + { + PDDocument doc = PDDocument.load(new File(TARGETPDFDIR, "PDFBOX-4197.pdf")); + PDStructureTreeRoot structureTreeRoot = doc.getDocumentCatalog().getStructureTreeRoot(); + Set> attributeSet = new HashSet>(); + checkElement(structureTreeRoot.getK(), attributeSet); + doc.close(); + + // collect attributes and check their count. + Assert.assertEquals(117, attributeSet.size()); + int cnt = 0; + for (Revisions attributes : attributeSet) + { + cnt += attributes.size(); + } + Assert.assertEquals(111, cnt); // this one was 105 before PDFBOX-4197 was fixed + } + + // Each element can be an array, a dictionary or a number. + // See PDF specification Table 323 - Entries in a structure element dictionary + private void checkElement(COSBase base, Set>attributeSet) + { + if (base instanceof COSArray) + { + for (COSBase base2 : (COSArray) base) + { + if (base2 instanceof COSObject) + { + base2 = ((COSObject) base2).getObject(); + } + checkElement(base2, attributeSet); + } + } + else if (base instanceof COSDictionary) + { + COSDictionary kdict = (COSDictionary) base; + if (kdict.containsKey(COSName.PG)) + { + PDStructureElement structureElement = new PDStructureElement(kdict); + Revisions attributes = structureElement.getAttributes(); + attributeSet.add(attributes); + Revisions classNames = structureElement.getClassNames(); + //TODO: modify the test to also check for class names, if we ever have a file. + } + if (kdict.containsKey(COSName.K)) + { + checkElement(kdict.getDictionaryObject(COSName.K), attributeSet); + } + } + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotationTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotationTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotationTest.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotationTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -23,6 +23,7 @@ import java.net.URISyntaxException; import java.util.List; +import org.junit.Assert; import org.junit.Test; /** @@ -39,7 +40,30 @@ File f = new File(FDFAnnotationTest.class.getResource("xfdf-test-document-annotations.xml").toURI()); FDFDocument fdfDoc = FDFDocument.loadXFDF(f); List fdfAnnots = fdfDoc.getCatalog().getFDF().getAnnotations(); - assertEquals(17, fdfAnnots.size()); + assertEquals(18, fdfAnnots.size()); + + // test PDFBOX-4345 and PDFBOX-3646 + boolean testedPDFBox4345andPDFBox3646 = false; + for (FDFAnnotation ann : fdfAnnots) + { + if (ann instanceof FDFAnnotationFreeText) + { + FDFAnnotationFreeText annotationFreeText = (FDFAnnotationFreeText) ann; + if ("P&1 P&2 P&3".equals(annotationFreeText.getContents())) + { + testedPDFBox4345andPDFBox3646 = true; + Assert.assertEquals("\n" + + "

P&1 P&2 " + + "P&3

\n" + + " ", annotationFreeText.getRichContents().trim()); + } + } + } + Assert.assertTrue(testedPDFBox4345andPDFBox3646); fdfDoc.close(); } } \ No newline at end of file diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDFontTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDFontTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDFontTest.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDFontTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -127,8 +127,8 @@ @Test public void testPDFBox3826() throws IOException, URISyntaxException { - URL url = PDFontTest.class.getClassLoader().getResource( - "org/apache/pdfbox/ttf/LiberationSans-Regular.ttf"); + URL url = PDFont.class.getClassLoader().getResource( + "org/apache/pdfbox/resources/ttf/LiberationSans-Regular.ttf"); File fontFile = new File(url.toURI()); TrueTypeFont ttf1 = new TTFParser().parse(fontFile); @@ -190,6 +190,32 @@ doc.close(); } + /** + * Test whether bug from PDFBOX-4318 is fixed, which had the wrong cache key. + * @throws java.io.IOException + */ + @Test + public void testPDFox4318() throws IOException + { + try + { + PDType1Font.HELVETICA_BOLD.encode("\u0080"); + Assert.fail("should have thrown IllegalArgumentException"); + } + catch (IllegalArgumentException ex) + { + } + PDType1Font.HELVETICA_BOLD.encode("€"); + try + { + PDType1Font.HELVETICA_BOLD.encode("\u0080"); + Assert.fail("should have thrown IllegalArgumentException"); + } + catch (IllegalArgumentException ex) + { + } + } + private void testPDFBox3826checkFonts(byte[] byteArray, File fontFile) throws IOException { PDDocument doc = PDDocument.load(byteArray); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/TestFontEmbedding.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/TestFontEmbedding.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/TestFontEmbedding.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/TestFontEmbedding.java 2018-11-28 17:18:34.000000000 +0000 @@ -20,6 +20,8 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; import junit.framework.TestCase; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSDictionary; @@ -170,14 +172,56 @@ assertEquals(expectedExtractedtext, extracted.replaceAll("\r", "").trim()); } + /** + * Test corner case of PDFBOX-4302. + * + * @throws java.io.IOException + */ + public void testMaxEntries() throws IOException + { + File file; + String text; + text = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん" + + "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン" + + "12345678"; + + // The test must have MAX_ENTRIES_PER_OPERATOR unique characters + Set set = new HashSet(ToUnicodeWriter.MAX_ENTRIES_PER_OPERATOR); + for (int i = 0; i < text.length(); ++i) + { + set.add(text.charAt(i)); + } + assertEquals(ToUnicodeWriter.MAX_ENTRIES_PER_OPERATOR, set.size()); + + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A0); + document.addPage(page); + File ipafont = new File("target/fonts/ipag00303", "ipag.ttf"); + PDType0Font font = PDType0Font.load(document, ipafont); + PDPageContentStream contentStream = new PDPageContentStream(document, page); + contentStream.beginText(); + contentStream.setFont(font, 20); + contentStream.newLineAtOffset(50, 3000); + contentStream.showText(text); + contentStream.endText(); + contentStream.close(); + file = new File(OUT_DIR, "PDFBOX-4302-test.pdf"); + document.save(file); + document.close(); + + // check that the extracted text matches what we wrote + String extracted = getUnicodeText(file); + assertEquals(text, extracted.trim()); + } + private void validateCIDFontType2(boolean useSubset) throws Exception { PDDocument document = new PDDocument(); PDPage page = new PDPage(PDRectangle.A4); document.addPage(page); - InputStream input = TestFontEmbedding.class.getClassLoader().getResourceAsStream( - "org/apache/pdfbox/ttf/LiberationSans-Regular.ttf"); + InputStream input = PDFont.class.getClassLoader().getResourceAsStream( + "org/apache/pdfbox/resources/ttf/LiberationSans-Regular.ttf"); PDType0Font font = PDType0Font.load(document, input, useSubset); PDPageContentStream stream = new PDPageContentStream(document, page); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/TestTTFParser.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/TestTTFParser.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/TestTTFParser.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/TestTTFParser.java 2018-11-28 17:18:34.000000000 +0000 @@ -41,8 +41,8 @@ @Test public void testPostTable() throws IOException { - InputStream input = TestTTFParser.class.getClassLoader().getResourceAsStream( - "org/apache/pdfbox/ttf/LiberationSans-Regular.ttf"); + InputStream input = PDFont.class.getClassLoader().getResourceAsStream( + "org/apache/pdfbox/resources/ttf/LiberationSans-Regular.ttf"); Assert.assertNotNull(input); TTFParser parser = new TTFParser(); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendModeTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendModeTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendModeTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/blend/BlendModeTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License")); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdmodel.graphics.blend; + +import static junit.framework.TestCase.assertEquals; +import org.apache.pdfbox.cos.COSName; +import org.junit.Test; + +/** + * + * @author Tilman Hausherr + */ +public class BlendModeTest +{ + public BlendModeTest() + { + } + + /** + * Check that BlendMode.* constant instances are not null. This could happen if the declaration + * sequence is changed. + */ + @Test + public void testInstances() + { + assertEquals(BlendMode.NORMAL, BlendMode.getInstance(COSName.NORMAL)); + assertEquals(BlendMode.NORMAL, BlendMode.getInstance(COSName.COMPATIBLE)); + assertEquals(BlendMode.MULTIPLY, BlendMode.getInstance(COSName.MULTIPLY)); + assertEquals(BlendMode.SCREEN, BlendMode.getInstance(COSName.SCREEN)); + assertEquals(BlendMode.OVERLAY, BlendMode.getInstance(COSName.OVERLAY)); + assertEquals(BlendMode.DARKEN, BlendMode.getInstance(COSName.DARKEN)); + assertEquals(BlendMode.LIGHTEN, BlendMode.getInstance(COSName.LIGHTEN)); + assertEquals(BlendMode.COLOR_DODGE, BlendMode.getInstance(COSName.COLOR_DODGE)); + assertEquals(BlendMode.COLOR_BURN, BlendMode.getInstance(COSName.COLOR_BURN)); + assertEquals(BlendMode.HARD_LIGHT, BlendMode.getInstance(COSName.HARD_LIGHT)); + assertEquals(BlendMode.SOFT_LIGHT, BlendMode.getInstance(COSName.SOFT_LIGHT)); + assertEquals(BlendMode.DIFFERENCE, BlendMode.getInstance(COSName.DIFFERENCE)); + assertEquals(BlendMode.EXCLUSION, BlendMode.getInstance(COSName.EXCLUSION)); + assertEquals(BlendMode.HUE, BlendMode.getInstance(COSName.HUE)); + assertEquals(BlendMode.SATURATION, BlendMode.getInstance(COSName.SATURATION)); + assertEquals(BlendMode.LUMINOSITY, BlendMode.getInstance(COSName.LUMINOSITY)); + assertEquals(BlendMode.COLOR, BlendMode.getInstance(COSName.COLOR)); + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/LosslessFactoryTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/LosslessFactoryTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/LosslessFactoryTest.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/LosslessFactoryTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -19,17 +19,31 @@ import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; +import java.awt.Point; import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; import java.awt.image.BufferedImage; +import java.awt.image.ColorConvertOp; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; import java.io.File; import java.io.IOException; +import java.util.Hashtable; import java.util.Random; import javax.imageio.ImageIO; import junit.framework.TestCase; +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceCMYK; import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray; import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB; import static org.apache.pdfbox.pdmodel.graphics.image.ValidateXImage.checkIdent; @@ -158,7 +172,7 @@ */ public void testCreateLosslessFromImageBITMASK_INT_ARGB() throws IOException { - doBitmaskTransparencyTest(BufferedImage.TYPE_INT_ARGB, "bitmaskintargb.pdf"); + doBitmaskTransparencyTest(BufferedImage.TYPE_4BYTE_ABGR, "bitmaskintargb.pdf"); } /** @@ -250,6 +264,22 @@ } /** + * Test file that had a predictor encoding bug in PDFBOX-4184. + * + * @throws java.io.IOException + */ + public void testCreateLosslessFromGovdocs032163() throws IOException + { + PDDocument document = new PDDocument(); + BufferedImage image = ImageIO.read(new File("target/imgs", "PDFBOX-4184-032163.jpg")); + PDImageXObject ximage = LosslessFactory.createFromImage(document, image); + validate(ximage, 8, image.getWidth(), image.getHeight(), "png", PDDeviceRGB.INSTANCE.getName()); + checkIdent(image, ximage.getImage()); + + doWritePDF(document, ximage, testResultsDir, "PDFBOX-4184-032163.pdf"); + } + + /** * Check whether the RGB part of images are identical. * * @param expectedImage @@ -385,4 +415,114 @@ document.close(); } + + /** + * Test lossless encoding of CMYK images + */ + public void testCreateLosslessFromImageCMYK() throws IOException + { + PDDocument document = new PDDocument(); + BufferedImage image = ImageIO.read(this.getClass().getResourceAsStream("png.png")); + + final ColorSpace targetCS = new ICC_ColorSpace(ICC_Profile + .getInstance(this.getClass().getResourceAsStream("/org/apache/pdfbox/resources/icc/ISOcoated_v2_300_bas.icc"))); + ColorConvertOp op = new ColorConvertOp(image.getColorModel().getColorSpace(), targetCS, null); + BufferedImage imageCMYK = op.filter(image, null); + + PDImageXObject ximage = LosslessFactory.createFromImage(document, imageCMYK); + validate(ximage, 8, imageCMYK.getWidth(), imageCMYK.getHeight(), "png", "ICCBased"); + + doWritePDF(document, ximage, testResultsDir, "cmyk.pdf"); + + // still slight difference of 1 color level + //checkIdent(imageCMYK, ximage.getImage()); + } + + public void testCreateLosslessFrom16Bit() throws IOException + { + PDDocument document = new PDDocument(); + BufferedImage image = ImageIO.read(this.getClass().getResourceAsStream("png.png")); + + ColorSpace targetCS = ColorSpace.getInstance(ColorSpace.CS_sRGB); + int dataBufferType = DataBuffer.TYPE_USHORT; + final ColorModel colorModel = new ComponentColorModel(targetCS, false, false, + ColorModel.OPAQUE, dataBufferType); + WritableRaster targetRaster = Raster.createInterleavedRaster(dataBufferType, image.getWidth(), image.getHeight(), + targetCS.getNumComponents(), new Point(0, 0)); + BufferedImage img16Bit = new BufferedImage(colorModel, targetRaster, false, new Hashtable()); + ColorConvertOp op = new ColorConvertOp(image.getColorModel().getColorSpace(), targetCS, null); + op.filter(image, img16Bit); + + PDImageXObject ximage = LosslessFactory.createFromImage(document, img16Bit); + validate(ximage, 16, img16Bit.getWidth(), img16Bit.getHeight(), "png", PDDeviceRGB.INSTANCE.getName()); + checkIdent(image, ximage.getImage()); + doWritePDF(document, ximage, testResultsDir, "misc-16bit.pdf"); + } + + public void testCreateLosslessFromImageINT_BGR() throws IOException + { + PDDocument document = new PDDocument(); + BufferedImage image = ImageIO.read(this.getClass().getResourceAsStream("png.png")); + + BufferedImage imgBgr = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_BGR); + Graphics2D graphics = imgBgr.createGraphics(); + graphics.drawImage(image, 0, 0, null); + + PDImageXObject ximage = LosslessFactory.createFromImage(document, imgBgr); + validate(ximage, 8, imgBgr.getWidth(), imgBgr.getHeight(), "png", PDDeviceRGB.INSTANCE.getName()); + checkIdent(image, ximage.getImage()); + } + + public void testCreateLosslessFromImageINT_RGB() throws IOException + { + PDDocument document = new PDDocument(); + BufferedImage image = ImageIO.read(this.getClass().getResourceAsStream("png.png")); + + BufferedImage imgRgb = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = imgRgb.createGraphics(); + graphics.drawImage(image, 0, 0, null); + + PDImageXObject ximage = LosslessFactory.createFromImage(document, imgRgb); + validate(ximage, 8, imgRgb.getWidth(), imgRgb.getHeight(), "png", PDDeviceRGB.INSTANCE.getName()); + checkIdent(image, ximage.getImage()); + } + + public void testCreateLosslessFromImageBYTE_3BGR() throws IOException + { + PDDocument document = new PDDocument(); + BufferedImage image = ImageIO.read(this.getClass().getResourceAsStream("png.png")); + + BufferedImage imgRgb = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_3BYTE_BGR); + Graphics2D graphics = imgRgb.createGraphics(); + graphics.drawImage(image, 0, 0, null); + + PDImageXObject ximage = LosslessFactory.createFromImage(document, imgRgb); + validate(ximage, 8, imgRgb.getWidth(), imgRgb.getHeight(), "png", PDDeviceRGB.INSTANCE.getName()); + checkIdent(image, ximage.getImage()); + } + + public void testCreateLosslessFrom16BitPNG() throws IOException + { + PDDocument document = new PDDocument(); + BufferedImage image = ImageIO.read(new File("target/imgs", "PDFBOX-4184-16bit.png")); + + assertEquals(64, image.getColorModel().getPixelSize()); + assertEquals(Transparency.TRANSLUCENT, image.getColorModel().getTransparency()); + assertEquals(4, image.getRaster().getNumDataElements()); + assertEquals(java.awt.image.DataBuffer.TYPE_USHORT, image.getRaster().getDataBuffer().getDataType()); + + PDImageXObject ximage = LosslessFactory.createFromImage(document, image); + + int w = image.getWidth(); + int h = image.getHeight(); + validate(ximage, 16, w, h, "png", PDDeviceRGB.INSTANCE.getName()); + checkIdent(image, ximage.getImage()); + checkIdentRGB(image, ximage.getOpaqueImage()); + + assertNotNull(ximage.getSoftMask()); + validate(ximage.getSoftMask(), 16, w, h, "png", PDDeviceGray.INSTANCE.getName()); + assertEquals(35, colorCount(ximage.getSoftMask().getImage())); + + doWritePDF(document, ximage, testResultsDir, "png16bit.pdf"); + } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/ValidateXImage.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/ValidateXImage.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/ValidateXImage.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/ValidateXImage.java 2018-11-28 17:18:34.000000000 +0000 @@ -16,12 +16,14 @@ package org.apache.pdfbox.pdmodel.graphics.image; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.OutputStream; import java.util.HashSet; import java.util.Set; import javax.imageio.ImageIO; +import javax.imageio.ImageWriter; +import javax.imageio.spi.ImageWriterSpi; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.pdmodel.PDDocument; @@ -61,13 +63,33 @@ assertEquals(ximage.getWidth(), ximage.getImage().getWidth()); assertEquals(ximage.getHeight(), ximage.getImage().getHeight()); - boolean writeOk = ImageIO.write(ximage.getImage(), - format, new ByteArrayOutputStream()); - assertTrue(writeOk); - writeOk = ImageIO.write(ximage.getOpaqueImage(), - format, new ByteArrayOutputStream()); + boolean canEncode = true; + boolean writeOk; + // jdk11+ no longer encodes ARGB jpg + // https://bugs.openjdk.java.net/browse/JDK-8211748 + if ("jpg".equals(format) && + ximage.getImage().getType() == BufferedImage.TYPE_INT_ARGB) + { + ImageWriter writer = ImageIO.getImageWritersBySuffix(format).next(); + ImageWriterSpi originatingProvider = writer.getOriginatingProvider(); + canEncode = originatingProvider.canEncodeImage(ximage.getImage()); + } + if (canEncode) + { + writeOk = ImageIO.write(ximage.getImage(), format, new NullOutputStream()); + assertTrue(writeOk); + } + writeOk = ImageIO.write(ximage.getOpaqueImage(), format, new NullOutputStream()); assertTrue(writeOk); } + + private static class NullOutputStream extends OutputStream + { + @Override + public void write(int b) throws IOException + { + } + } static int colorCount(BufferedImage bim) { @@ -143,7 +165,7 @@ { if (expectedImage.getRGB(x, y) != actualImage.getRGB(x, y)) { - errMsg = String.format("(%d,%d) %08X != %08X", x, y, expectedImage.getRGB(x, y), actualImage.getRGB(x, y)); + errMsg = String.format("(%d,%d) expected: <%08X> but was: <%08X>; ", x, y, expectedImage.getRGB(x, y), actualImage.getRGB(x, y)); } assertEquals(errMsg, expectedImage.getRGB(x, y), actualImage.getRGB(x, y)); } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/state/RenderingIntentTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/state/RenderingIntentTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/state/RenderingIntentTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/state/RenderingIntentTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdmodel.graphics.state; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class RenderingIntentTest +{ + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void fromStringInputNotNullOutputNotNull() + { + // Arrange + final String value = "AbsoluteColorimetric"; + + // Act + final RenderingIntent retval = RenderingIntent.fromString(value); + + // Assert result + Assert.assertEquals(RenderingIntent.ABSOLUTE_COLORIMETRIC, retval); + } + + @Test + public void fromStringInputNotNullOutputNotNull2() + { + // Arrange + final String value = "RelativeColorimetric"; + + // Act + final RenderingIntent retval = RenderingIntent.fromString(value); + + // Assert result + Assert.assertEquals(RenderingIntent.RELATIVE_COLORIMETRIC, retval); + } + + @Test + public void fromStringInputNotNullOutputNotNull3() + { + // Arrange + final String value = "Perceptual"; + + // Act + final RenderingIntent retval = RenderingIntent.fromString(value); + + // Assert result + Assert.assertEquals(RenderingIntent.PERCEPTUAL, retval); + } + + @Test + public void fromStringInputNotNullOutputNotNull4() + { + // Arrange + final String value = "Saturation"; + + // Act + final RenderingIntent retval = RenderingIntent.fromString(value); + + // Assert result + Assert.assertEquals(RenderingIntent.SATURATION, retval); + } + + @Test + public void fromStringInputNotNullOutputNotNull5() + { + // Arrange + final String value = ""; + + // Act + final RenderingIntent retval = RenderingIntent.fromString(value); + + // Assert result + Assert.assertEquals(RenderingIntent.RELATIVE_COLORIMETRIC, retval); + } + + @Test + public void stringValueOutputNotNull() + { + // Arrange + final RenderingIntent objectUnderTest = RenderingIntent.ABSOLUTE_COLORIMETRIC; + + // Act + final String retval = objectUnderTest.stringValue(); + + // Assert result + Assert.assertEquals("AbsoluteColorimetric", retval); + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormTest.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -24,17 +24,22 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import org.apache.pdfbox.rendering.TestPDFToImage; import org.junit.After; +import static org.junit.Assert.fail; import org.junit.Before; import org.junit.Test; @@ -135,6 +140,41 @@ System.out.println("Rendering of " + file + " failed or is not identical to expected rendering in " + IN_DIR + " directory"); } } + + @Test + public void testFlattenSpecificFieldsOnly() throws IOException + { + File file = new File(OUT_DIR, "AlignmentTests-flattened-specificFields.pdf"); + + List fieldsToFlatten = new ArrayList(); + + PDDocument testPdf = null; + try + { + testPdf = PDDocument.load(new File(IN_DIR, "AlignmentTests.pdf")); + PDAcroForm acroFormToFlatten = testPdf.getDocumentCatalog().getAcroForm(); + int numFieldsBeforeFlatten = acroFormToFlatten.getFields().size(); + int numWidgetsBeforeFlatten = countWidgets(testPdf); + + fieldsToFlatten.add(acroFormToFlatten.getField("AlignLeft-Border_Small-Filled")); + fieldsToFlatten.add(acroFormToFlatten.getField("AlignLeft-Border_Medium-Filled")); + fieldsToFlatten.add(acroFormToFlatten.getField("AlignLeft-Border_Wide-Filled")); + fieldsToFlatten.add(acroFormToFlatten.getField("AlignLeft-Border_Wide_Clipped-Filled")); + + acroFormToFlatten.flatten(fieldsToFlatten, true); + int numFieldsAfterFlatten = acroFormToFlatten.getFields().size(); + int numWidgetsAfterFlatten = countWidgets(testPdf); + + assertEquals(numFieldsBeforeFlatten, numFieldsAfterFlatten + fieldsToFlatten.size()); + assertEquals(numWidgetsBeforeFlatten, numWidgetsAfterFlatten + fieldsToFlatten.size()); + + testPdf.save(file); + } + finally + { + IOUtils.closeQuietly(testPdf); + } + } /* * Test that we do not modify an AcroForm with missing resource information @@ -204,7 +244,54 @@ return; } } - + + /** + * PDFBOX-4235: a bad /DA string should not result in an NPE. + * + * @throws IOException + */ + @Test + public void testBadDA() throws IOException + { + PDDocument doc = new PDDocument(); + + PDPage page = new PDPage(); + doc.addPage(page); + + PDAcroForm acroForm = new PDAcroForm(document); + doc.getDocumentCatalog().setAcroForm(acroForm); + acroForm.setDefaultResources(new PDResources()); + + PDTextField textBox = new PDTextField(acroForm); + textBox.setPartialName("SampleField"); + + // https://stackoverflow.com/questions/50609478/ + // "tf" is a typo, should have been "Tf" and this results that no font is chosen + textBox.setDefaultAppearance("/Helv 0 tf 0 g"); + acroForm.getFields().add(textBox); + + PDAnnotationWidget widget = textBox.getWidgets().get(0); + PDRectangle rect = new PDRectangle(50, 750, 200, 20); + widget.setRectangle(rect); + widget.setPage(page); + + page.getAnnotations().add(widget); + + try + { + textBox.setValue("huhu"); + } + catch (IllegalArgumentException ex) + { + return; + } + finally + { + doc.close(); + } + fail("IllegalArgumentException should have been thrown"); + } + @After public void tearDown() throws IOException { @@ -239,6 +326,28 @@ document.close(); return baos.toByteArray(); } - + + private int countWidgets(PDDocument documentToTest) + { + int count = 0; + for (PDPage page : documentToTest.getPages()) + { + try + { + for (PDAnnotation annotation : page.getAnnotations()) + { + if (annotation instanceof PDAnnotationWidget) + { + count ++; + } + } + } + catch (IOException e) + { + // ignoring + } + } + return count; + } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDButtonTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDButtonTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDButtonTest.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDButtonTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -216,7 +216,7 @@ public void testAcrobatCheckBoxProperties() throws IOException { PDCheckBox checkbox = (PDCheckBox) acrobatAcroForm.getField("Checkbox"); - assertEquals(checkbox.getValue(), ""); + assertEquals(checkbox.getValue(), "Off"); assertEquals(checkbox.isChecked(), false); checkbox.check(); @@ -260,7 +260,7 @@ public void testAcrobatCheckBoxGroupProperties() throws IOException { PDCheckBox checkbox = (PDCheckBox) acrobatAcroForm.getField("CheckboxGroup"); - assertEquals(checkbox.getValue(), ""); + assertEquals(checkbox.getValue(), "Off"); assertEquals(checkbox.isChecked(), false); checkbox.check(); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDChoiceTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDChoiceTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDChoiceTest.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDChoiceTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -20,7 +20,12 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSString; import org.apache.pdfbox.pdmodel.PDDocument; import org.junit.Before; import org.junit.Test; @@ -33,12 +38,18 @@ { private PDDocument document; private PDAcroForm acroForm; + private List options; + @Before public void setUp() { document = new PDDocument(); acroForm = new PDAcroForm(document); + options = new ArrayList(); + options.add(" "); + options.add("A"); + options.add("B"); } @Test @@ -61,5 +72,78 @@ assertTrue(choiceField.isCombo()); } + @Test + public void getOptionsFromStrings() + { + PDChoice choiceField = new PDComboBox(acroForm); + COSArray choiceFieldOptions = new COSArray(); + choiceFieldOptions.add(new COSString(" ")); + choiceFieldOptions.add(new COSString("A")); + choiceFieldOptions.add(new COSString("B")); + + // add the options using the low level COS model as the PD model will + // abstract the COSArray + choiceField.getCOSObject().setItem(COSName.OPT, choiceFieldOptions); + + assertEquals(options, choiceField.getOptions()); + } + + @Test + public void getOptionsFromCOSArray() + { + PDChoice choiceField = new PDComboBox(acroForm); + COSArray choiceFieldOptions = new COSArray(); + + // add entry to options + COSArray entry = new COSArray(); + entry.add(new COSString(" ")); + choiceFieldOptions.add(entry); + + // add entry to options + entry = new COSArray(); + entry.add(new COSString("A")); + choiceFieldOptions.add(entry); + + // add entry to options + entry = new COSArray(); + entry.add(new COSString("B")); + choiceFieldOptions.add(entry); + + // add the options using the low level COS model as the PD model will + // abstract the COSArray + choiceField.getCOSObject().setItem(COSName.OPT, choiceFieldOptions); + + assertEquals(options, choiceField.getOptions()); + } + + /* + * Get the entries form a moxed values array. See PDFBOX-4185 + */ + @Test + public void getOptionsFromMixed() + { + PDChoice choiceField = new PDComboBox(acroForm); + COSArray choiceFieldOptions = new COSArray(); + + // add string entry to options + choiceFieldOptions.add(new COSString(" ")); + + // add array entry to options + COSArray entry = new COSArray(); + entry.add(new COSString("A")); + choiceFieldOptions.add(entry); + + // add array entry to options + entry = new COSArray(); + entry.add(new COSString("B")); + choiceFieldOptions.add(entry); + + // add the options using the low level COS model as the PD model will + // abstract the COSArray + choiceField.getCOSObject().setItem(COSName.OPT, choiceFieldOptions); + + assertEquals(options, choiceField.getOptions()); + } + } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestCheckBox.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestCheckBox.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestCheckBox.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestCheckBox.java 2018-11-28 17:18:34.000000000 +0000 @@ -25,11 +25,19 @@ import junit.framework.TestSuite; import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceCharacteristicsDictionary; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; /** - * This will test the functionality of Radio Buttons in PDFBox. + * This will test the functionality of checkboxes in PDFBox. */ public class TestCheckBox extends TestCase { @@ -66,7 +74,7 @@ } /** - * This will test the radio button PDModel. + * This will test the checkbox PDModel. * * @throws IOException If there is an error creating the field. */ @@ -106,7 +114,7 @@ checkBox.setExportValues(null); assertNull(checkBox.getCOSObject().getItem(COSName.OPT)); // if there is no Opt entry an empty List shall be returned - assertEquals(checkBox.getExportValues(), new ArrayList()); + assertTrue(checkBox.getExportValues().isEmpty()); } finally { @@ -116,4 +124,40 @@ } } } + + /** + * PDFBOX-4366: Create and test a checkbox with no /AP. The created file works with Adobe Reader! + * + * @throws IOException + */ + public void testCheckBoxNoAppearance() throws IOException + { + PDDocument doc = new PDDocument(); + PDPage page = new PDPage(); + doc.addPage(page); + PDAcroForm acroForm = new PDAcroForm(doc); + acroForm.setNeedAppearances(true); // need this or it won't appear on Adobe Reader + doc.getDocumentCatalog().setAcroForm(acroForm); + List fields = new ArrayList(); + PDCheckBox checkBox = new PDCheckBox(acroForm); + checkBox.setPartialName("checkbox"); + PDAnnotationWidget widget = checkBox.getWidgets().get(0); + widget.setRectangle(new PDRectangle(50, 600, 100, 100)); + PDBorderStyleDictionary bs = new PDBorderStyleDictionary(); + bs.setStyle(PDBorderStyleDictionary.STYLE_SOLID); + bs.setWidth(1); + COSDictionary acd = new COSDictionary(); + PDAppearanceCharacteristicsDictionary ac = new PDAppearanceCharacteristicsDictionary(acd); + ac.setBackground(new PDColor(new float[] { 1, 1, 0 }, PDDeviceRGB.INSTANCE)); + ac.setBorderColour(new PDColor(new float[] { 1, 0, 0 }, PDDeviceRGB.INSTANCE)); + ac.setNormalCaption("4"); // 4 is checkmark, 8 is cross + widget.setAppearanceCharacteristics(ac); + widget.setBorderStyle(bs); + checkBox.setValue("Off"); + fields.add(checkBox); + page.getAnnotations().add(widget); + acroForm.setFields(fields); + assertEquals("Off", checkBox.getValue()); + doc.close(); + } } \ No newline at end of file diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestListBox.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestListBox.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestListBox.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestListBox.java 2018-11-28 17:18:34.000000000 +0000 @@ -67,15 +67,15 @@ } /** - * This will test the radio button PDModel. + * This will test the list box PDModel. * * @throws IOException If there is an error creating the field. */ - public void testChoicePDModel() throws IOException + public void testListboxPDModel() throws IOException { /* - * Set up two data list which will be used for the tests + * Set up two data lists which will be used for the tests */ // export values @@ -96,7 +96,7 @@ { doc = new PDDocument(); PDAcroForm form = new PDAcroForm( doc ); - PDChoice choice = new PDListBox(form); + PDListBox choice = new PDListBox(form); // appearance construction is not implemented, so turn on NeedAppearances form.setNeedAppearances(true); @@ -115,6 +115,12 @@ assertEquals(exportValues,choice.getOptionsDisplayValues()); assertEquals(exportValues,choice.getOptionsExportValues()); + // Test bug 1 of PDFBOX-4252 when top index is not null + choice.setTopIndex(1); + choice.setValue(exportValues.get(2)); + assertEquals(exportValues.get(2), choice.getValue().get(0)); + choice.setTopIndex(null); // reset + // assert that the option values have been correctly set COSArray optItem = (COSArray) choice.getCOSObject().getItem(COSName.OPT); assertNotNull(choice.getCOSObject().getItem(COSName.OPT)); diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/PageLayoutTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/PageLayoutTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/PageLayoutTest.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/PageLayoutTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -19,14 +19,15 @@ import java.util.HashSet; import java.util.Set; import static org.junit.Assert.assertEquals; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; -/** - * @author Tilman Hausherr - */ public class PageLayoutTest { /** + * @author Tilman Hausherr + * * Test for completeness (PDFBOX-3362). */ @Test @@ -43,4 +44,23 @@ assertEquals(PageLayout.values().length, pageLayoutSet.size()); assertEquals(PageLayout.values().length, stringSet.size()); } + + /** + * @author John Bergqvist + */ + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void fromStringInputNotNullOutputIllegalArgumentException() + { + // Arrange + final String value = "SinglePag"; + + // Act + thrown.expect(IllegalArgumentException.class); + PageLayout.fromString(value); + + // Method is not expected to return due to exception thrown + } } diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/PageModeTest.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/PageModeTest.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/PageModeTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/PageModeTest.java 2018-11-28 17:18:34.000000000 +0000 @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdmodel; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class PageModeTest +{ + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void fromStringInputNotNullOutputNotNull() + { + // Arrange + final String value = "FullScreen"; + + // Act + final PageMode retval = PageMode.fromString(value); + + // Assert result + Assert.assertEquals(PageMode.FULL_SCREEN, retval); + } + + @Test + public void fromStringInputNotNullOutputNotNull2() + { + // Arrange + final String value = "UseThumbs"; + + // Act + final PageMode retval = PageMode.fromString(value); + + // Assert result + Assert.assertEquals(PageMode.USE_THUMBS, retval); + } + + @Test + public void fromStringInputNotNullOutputNotNull3() + { + // Arrange + final String value = "UseOC"; + + // Act + final PageMode retval = PageMode.fromString(value); + + // Assert result + Assert.assertEquals(PageMode.USE_OPTIONAL_CONTENT, retval); + } + + @Test + public void fromStringInputNotNullOutputNotNull4() + { + // Arrange + final String value = "UseNone"; + + // Act + final PageMode retval = PageMode.fromString(value); + + // Assert result + Assert.assertEquals(PageMode.USE_NONE, retval); + } + + @Test + public void fromStringInputNotNullOutputNotNull5() + { + // Arrange + final String value = "UseAttachments"; + + // Act + final PageMode retval = PageMode.fromString(value); + + // Assert result + Assert.assertEquals(PageMode.USE_ATTACHMENTS, retval); + } + + @Test + public void fromStringInputNotNullOutputNotNull6() + { + // Arrange + final String value = "UseOutlines"; + + // Act + final PageMode retval = PageMode.fromString(value); + + // Assert result + Assert.assertEquals(PageMode.USE_OUTLINES, retval); + } + + @Test + public void fromStringInputNotNullOutputIllegalArgumentException() + { + // Arrange + final String value = ""; + + // Act + thrown.expect(IllegalArgumentException.class); + PageMode.fromString(value); + + // Method is not expected to return due to exception thrown + } + + @Test + public void fromStringInputNotNullOutputIllegalArgumentException2() + { + // Arrange + final String value = "Dulacb`ecj"; + + // Act + thrown.expect(IllegalArgumentException.class); + PageMode.fromString(value); + + // Method is not expected to return due to exception thrown + } + + @Test + public void stringValueOutputNotNull() + { + // Arrange + final PageMode objectUnderTest = PageMode.USE_OPTIONAL_CONTENT; + + // Act + final String retval = objectUnderTest.stringValue(); + + // Assert result + Assert.assertEquals("UseOC", retval); + } +} diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/TestPDPageContentStream.java libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/TestPDPageContentStream.java --- libpdfbox2-java-2.0.9/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/TestPDPageContentStream.java 2018-03-20 16:19:50.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/TestPDPageContentStream.java 2018-11-28 17:18:34.000000000 +0000 @@ -96,4 +96,20 @@ List tokens = parser.getTokens(); assertEquals(0, tokens.size()); } + + /** + * Check that close() can be called twice. + * + * @throws IOException + */ + public void testCloseContract() throws IOException + { + PDDocument doc = new PDDocument(); + PDPage page = new PDPage(); + doc.addPage(page); + PDPageContentStream contentStream = new PDPageContentStream(doc, page, AppendMode.OVERWRITE, true); + contentStream.close(); + contentStream.close(); + doc.close(); + } } Binary files /tmp/tmpAhqVVB/umSeuAuTn3/libpdfbox2-java-2.0.9/pdfbox/src/test/resources/input/PDFBOX-4322-Empty-ToUnicode-reduced.pdf and /tmp/tmpAhqVVB/Y0XihoDyIA/libpdfbox2-java-2.0.13/pdfbox/src/test/resources/input/PDFBOX-4322-Empty-ToUnicode-reduced.pdf differ diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/resources/input/PDFBOX-4322-Empty-ToUnicode-reduced.pdf-sorted.txt libpdfbox2-java-2.0.13/pdfbox/src/test/resources/input/PDFBOX-4322-Empty-ToUnicode-reduced.pdf-sorted.txt --- libpdfbox2-java-2.0.9/pdfbox/src/test/resources/input/PDFBOX-4322-Empty-ToUnicode-reduced.pdf-sorted.txt 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/resources/input/PDFBOX-4322-Empty-ToUnicode-reduced.pdf-sorted.txt 2018-11-28 17:18:36.000000000 +0000 @@ -0,0 +1 @@ +Justin diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/resources/input/PDFBOX-4322-Empty-ToUnicode-reduced.pdf.txt libpdfbox2-java-2.0.13/pdfbox/src/test/resources/input/PDFBOX-4322-Empty-ToUnicode-reduced.pdf.txt --- libpdfbox2-java-2.0.9/pdfbox/src/test/resources/input/PDFBOX-4322-Empty-ToUnicode-reduced.pdf.txt 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/resources/input/PDFBOX-4322-Empty-ToUnicode-reduced.pdf.txt 2018-11-28 17:18:34.000000000 +0000 @@ -0,0 +1 @@ +Justin diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/resources/input/rendering/PDFBOX-4372-2DAYCLVOFG3FTVO4RMAJJL3VTPNYDFRO-p4_reduced.pdf libpdfbox2-java-2.0.13/pdfbox/src/test/resources/input/rendering/PDFBOX-4372-2DAYCLVOFG3FTVO4RMAJJL3VTPNYDFRO-p4_reduced.pdf --- libpdfbox2-java-2.0.9/pdfbox/src/test/resources/input/rendering/PDFBOX-4372-2DAYCLVOFG3FTVO4RMAJJL3VTPNYDFRO-p4_reduced.pdf 1970-01-01 00:00:00.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/resources/input/rendering/PDFBOX-4372-2DAYCLVOFG3FTVO4RMAJJL3VTPNYDFRO-p4_reduced.pdf 2018-11-28 17:18:34.000000000 +0000 @@ -0,0 +1,82 @@ +%PDF-1.7 +% +1 0 obj +<< +/Type /Catalog +/Version /1.4 +/Pages 2 0 R +/ViewerPreferences 3 0 R +>> +endobj +2 0 obj +<< +/Type /Pages +/Kids [4 0 R] +/Count 1 +>> +endobj +3 0 obj +<< +/Direction /L2R +>> +endobj +4 0 obj +<< +/Contents 5 0 R +/MediaBox [0.0 0.0 595.276 595.276] +/Parent 2 0 R +/Resources 6 0 R +/Type /Page +>> +endobj +5 0 obj +<< +/Length 9 +>> +stream +/Fm0 Do + +endstream +endobj +6 0 obj +<< +/XObject 7 0 R +>> +endobj +7 0 obj +<< +/Fm0 8 0 R +>> +endobj +8 0 obj +<< +/Length 7 +/BBox [-16.4768 477.771 616.049 439.202] +/Matrix [1 0 0 1 0 0] +/Resources null +/Subtype /Form +>> +stream +/Fm0 Do +endstream +endobj +xref +0 9 +0000000000 65535 f +0000000015 00000 n +0000000103 00000 n +0000000160 00000 n +0000000197 00000 n +0000000313 00000 n +0000000373 00000 n +0000000409 00000 n +0000000441 00000 n +trailer +<< +/Root 1 0 R +/ID [ ] +/Size 9 +>> +startxref +593 +%%EOF Binary files /tmp/tmpAhqVVB/umSeuAuTn3/libpdfbox2-java-2.0.9/pdfbox/src/test/resources/input/rendering/PDFBOX-4372-2DAYCLVOFG3FTVO4RMAJJL3VTPNYDFRO-p4_reduced.pdf-1.png and /tmp/tmpAhqVVB/Y0XihoDyIA/libpdfbox2-java-2.0.13/pdfbox/src/test/resources/input/rendering/PDFBOX-4372-2DAYCLVOFG3FTVO4RMAJJL3VTPNYDFRO-p4_reduced.pdf-1.png differ diff -Nru libpdfbox2-java-2.0.9/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/fdf/xfdf-test-document-annotations.xml libpdfbox2-java-2.0.13/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/fdf/xfdf-test-document-annotations.xml --- libpdfbox2-java-2.0.9/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/fdf/xfdf-test-document-annotations.xml 2018-03-20 16:19:52.000000000 +0000 +++ libpdfbox2-java-2.0.13/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/fdf/xfdf-test-document-annotations.xml 2018-11-28 17:18:36.000000000 +0000 @@ -65,5 +65,16 @@ + + + + +

P&1 P&2 P&3

+ +
+ /Helvetica 12 Tf 0.842 0.424 0.000 rg +
\ No newline at end of file Binary files /tmp/tmpAhqVVB/umSeuAuTn3/libpdfbox2-java-2.0.9/pdfbox/src/test/resources/org/apache/pdfbox/ttf/LiberationSans-Regular.ttf and /tmp/tmpAhqVVB/Y0XihoDyIA/libpdfbox2-java-2.0.13/pdfbox/src/test/resources/org/apache/pdfbox/ttf/LiberationSans-Regular.ttf differ diff -Nru libpdfbox2-java-2.0.9/pom.xml libpdfbox2-java-2.0.13/pom.xml --- libpdfbox2-java-2.0.9/pom.xml 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/pom.xml 2018-11-28 17:18:40.000000000 +0000 @@ -23,7 +23,7 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 parent/pom.xml @@ -34,12 +34,12 @@ - scm:svn:http://svn.apache.org/repos/asf/pdfbox/tags/2.0.9 + scm:svn:http://svn.apache.org/repos/asf/pdfbox/tags/2.0.13 - scm:svn:https://svn.apache.org/repos/asf/pdfbox/tags/2.0.9 + scm:svn:https://svn.apache.org/repos/asf/pdfbox/tags/2.0.13 - http://svn.apache.org/viewvc/pdfbox/tags/2.0.9 + http://svn.apache.org/viewvc/pdfbox/tags/2.0.13 diff -Nru libpdfbox2-java-2.0.9/preflight/pom.xml libpdfbox2-java-2.0.13/preflight/pom.xml --- libpdfbox2-java-2.0.9/preflight/pom.xml 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/pom.xml 2018-11-28 17:18:40.000000000 +0000 @@ -26,7 +26,7 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 ../parent/pom.xml @@ -35,6 +35,26 @@ true + + + + [11,) + + + + javax.xml.bind + jaxb-api + provided + + + javax.activation + activation + provided + + + + + @@ -107,9 +127,7 @@ com.googlecode.maven-download-plugin - - maven-download-plugin - 1.1.0 + download-maven-plugin ${skipTests} @@ -124,7 +142,7 @@ https://www.pdfa.org/wp-content/until2016_uploads/2011/08/isartor-pdfa-2008-08-13.zip true ${project.build.directory}/pdfs - 9f129c834bc6f9f8dabad4491c4c10ec + 66bf4ad470b36079c1e0ceca4438053f32649f964fb1de5cd88babce36c5afc0ba6fa7880bc1c9aac791df872cdfc8dc9851bfd3c75ae96786edd8fac61193ae @@ -135,10 +153,10 @@ ${skip-bavaria} - http://www.pdflib.com/fileadmin/pdflib/Bavaria/2009-04-03-Bavaria-pdfa.zip + https://web.archive.org/web/20160305185745if_/http://www.pdflib.com/fileadmin/pdflib/Bavaria/2009-04-03-Bavaria-pdfa.zip true ${project.build.directory}/pdfs - d8fccb2fea540ab49bef237f3579546b + a6efe70574dcde3628271fc1d7aa32cc00095334aa9415e5ebfb96cc20e0f79edd040c0290d5a76b4ced4c6a4343ba4af9567bf12eb7cfe3ec70f1a43202c231 @@ -158,7 +176,6 @@ com.googlecode.maven-download-plugin download-maven-plugin - [1.3.0,) wget @@ -195,11 +212,6 @@ junit junit - - log4j - log4j - test - diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/metadata/SynchronizedMetaDataValidation.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/metadata/SynchronizedMetaDataValidation.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/metadata/SynchronizedMetaDataValidation.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/metadata/SynchronizedMetaDataValidation.java 2018-11-28 17:18:38.000000000 +0000 @@ -35,7 +35,6 @@ import org.apache.pdfbox.preflight.PreflightConstants; import org.apache.pdfbox.preflight.ValidationResult.ValidationError; import org.apache.pdfbox.preflight.exception.ValidationException; -import org.apache.xmpbox.DateConverter; import org.apache.xmpbox.XMPMetadata; import org.apache.xmpbox.schema.AdobePDFSchema; import org.apache.xmpbox.schema.DublinCoreSchema; @@ -352,7 +351,7 @@ } else { - if (!DateConverter.toISO8601(xmpCreationDate).equals(DateConverter.toISO8601(creationDate))) + if (xmpCreationDate.compareTo(creationDate) != 0) { ve.add(unsynchronizedMetaDataError("CreationDate")); } @@ -395,7 +394,7 @@ } else { - if (!DateConverter.toISO8601(xmpModifyDate).equals(DateConverter.toISO8601(modifyDate))) + if (xmpModifyDate.compareTo(modifyDate) != 0) { ve.add(unsynchronizedMetaDataError("ModificationDate")); } diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/parser/XmlResultParser.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/parser/XmlResultParser.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/parser/XmlResultParser.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/parser/XmlResultParser.java 2018-11-28 17:18:38.000000000 +0000 @@ -43,12 +43,12 @@ { - public Element validate(DataSource source) throws IOException + public Element validate(DataSource dataSource) throws IOException { try { Document rdocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); - return validate(rdocument,source); + return validate(rdocument,dataSource); } catch (ParserConfigurationException e) { @@ -57,14 +57,14 @@ } - public Element validate(Document rdocument, DataSource source) throws IOException + public Element validate(Document rdocument, DataSource dataSource) throws IOException { String pdfType = null; ValidationResult result; long before = System.currentTimeMillis(); try { - PreflightParser parser = new PreflightParser(source); + PreflightParser parser = new PreflightParser(dataSource); try { parser.parse(); @@ -82,13 +82,13 @@ catch(Exception e) { long after = System.currentTimeMillis(); - return generateFailureResponse(rdocument, source.getName(), after-before, pdfType, e); + return generateFailureResponse(rdocument, dataSource.getName(), after-before, pdfType, e); } long after = System.currentTimeMillis(); if (result.isValid()) { - Element preflight = generateResponseSkeleton(rdocument, source.getName(), after-before); + Element preflight = generateResponseSkeleton(rdocument, dataSource.getName(), after-before); // valid ? Element valid = rdocument.createElement("isValid"); valid.setAttribute("type", pdfType); @@ -98,7 +98,7 @@ } else { - Element preflight = generateResponseSkeleton(rdocument, source.getName(), after-before); + Element preflight = generateResponseSkeleton(rdocument, dataSource.getName(), after-before); // valid ? createResponseWithError(rdocument, pdfType, result, preflight); return preflight; diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightConfiguration.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightConfiguration.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightConfiguration.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightConfiguration.java 2018-11-28 17:18:40.000000000 +0000 @@ -114,6 +114,11 @@ */ private ColorSpaceHelperFactory colorSpaceHelperFact; + /** + * Define the maximum number of errors. + */ + private int maxErrors = 10000; + public static PreflightConfiguration createPdfA1BConfiguration() { PreflightConfiguration configuration = new PreflightConfiguration(); @@ -300,4 +305,23 @@ this.colorSpaceHelperFact = colorSpaceHelperFact; } + /** + * Get the maximum number of errors after which to abort when possible. + * + * @return + */ + public int getMaxErrors() + { + return maxErrors; + } + + /** + * Set the maximum number of errors after which to abort when possible. + * + * @param maxErrors + */ + public void setMaxErrors(int maxErrors) + { + this.maxErrors = maxErrors; + } } diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightConstants.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightConstants.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightConstants.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightConstants.java 2018-11-28 17:18:40.000000000 +0000 @@ -66,8 +66,8 @@ String OUTPUT_INTENT_DICTIONARY_KEY_OUTPUT_CONDITION_IDENTIFIER = "OutputConditionIdentifier"; String OUTPUT_INTENT_DICTIONARY_VALUE_OUTPUT_CONDITION_IDENTIFIER_CUSTOM = "Custom"; - String TRANPARENCY_DICTIONARY_KEY_EXTGSTATE = "ExtGState"; - String TRANPARENCY_DICTIONARY_KEY_EXTGSTATE_ENTRY_REGEX = "(GS|gs)([0-9])+"; + String TRANSPARENCY_DICTIONARY_KEY_EXTGSTATE = "ExtGState"; + String TRANSPARENCY_DICTIONARY_KEY_EXTGSTATE_ENTRY_REGEX = "(GS|gs)([0-9])+"; String TRANSPARENCY_DICTIONARY_KEY_BLEND_MODE = "BM"; String TRANSPARENCY_DICTIONARY_KEY_UPPER_CA = "CA"; diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightContext.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightContext.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightContext.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightContext.java 2018-11-28 17:18:40.000000000 +0000 @@ -51,7 +51,7 @@ /** * The datasource to load the document from. Needed by StreamValidationProcess. */ - private DataSource source = null; + private DataSource dataSource = null; /** * Contains all Xref/trailer objects and resolves them into single object using startxref reference. @@ -84,16 +84,16 @@ /** * Create the DocumentHandler using the DataSource which represent the PDF file to check. * - * @param source + * @param dataSource */ - public PreflightContext(DataSource source) + public PreflightContext(DataSource dataSource) { - this.source = source; + this.dataSource = dataSource; } - public PreflightContext(DataSource source, PreflightConfiguration configuration) + public PreflightContext(DataSource dataSource, PreflightConfiguration configuration) { - this.source = source; + this.dataSource = dataSource; this.config = configuration; } @@ -148,12 +148,12 @@ */ public DataSource getSource() { - return source; + return dataSource; } public boolean isComplete() { - return (document != null) && (source != null); + return (document != null) && (dataSource != null); } /** diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightDocument.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightDocument.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightDocument.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/PreflightDocument.java 2018-11-28 17:18:40.000000000 +0000 @@ -156,6 +156,9 @@ */ public void validate() throws ValidationException { + // force early class loading to check if people forgot to use --add-modules javax.xml.bind + // on java 9 & 10, or to add jaxb-api on java 11 and later + javax.xml.bind.DatatypeConverter.parseInt("0"); context.setConfig(config); Collection processes = config.getProcessNames(); for (String name : processes) diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/process/PageTreeValidationProcess.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/process/PageTreeValidationProcess.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/process/PageTreeValidationProcess.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/process/PageTreeValidationProcess.java 2018-11-28 17:18:40.000000000 +0000 @@ -28,6 +28,7 @@ import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.preflight.PreflightConstants; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_PDF_PROCESSING_MISSING; import org.apache.pdfbox.preflight.PreflightContext; import org.apache.pdfbox.preflight.ValidationResult.ValidationError; @@ -50,12 +51,21 @@ "/Pages dictionary entry is missing in document catalog")); return; } - int numPages = context.getDocument().getNumberOfPages(); - for (int i = 0; i < numPages; i++) + int p = 0; + for (PDPage page : context.getDocument().getPages()) { - context.setCurrentPageNumber(i); - validatePage(context, context.getDocument().getPage(i)); + context.setCurrentPageNumber(p); + validatePage(context, page); + + if (context.getDocument().getResult().getErrorsList().size() > context.getConfig().getMaxErrors()) + { + context.addValidationError(new ValidationError(PreflightConstants.ERROR_UNKOWN_ERROR, + "Over " + context.getConfig().getMaxErrors() + + " errors, page tree validation process aborted")); + break; + } context.setCurrentPageNumber(null); + ++p; } } else diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ExtGStateValidationProcess.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ExtGStateValidationProcess.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ExtGStateValidationProcess.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ExtGStateValidationProcess.java 2018-11-28 17:18:40.000000000 +0000 @@ -31,7 +31,7 @@ import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_TRANSPARENCY_EXT_GS_BLEND_MODE; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_TRANSPARENCY_EXT_GS_CA; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_TRANSPARENCY_EXT_GS_SOFT_MASK; -import static org.apache.pdfbox.preflight.PreflightConstants.TRANPARENCY_DICTIONARY_KEY_EXTGSTATE_ENTRY_REGEX; +import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_EXTGSTATE_ENTRY_REGEX; import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_BLEND_MODE; import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_LOWER_CA; import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_UPPER_CA; @@ -69,7 +69,7 @@ * Validate the ExtGState dictionaries. * * @param context the context which contains the Resource dictionary. - * @throws ValidationException thrown if a the Extended Graphic State isn't valid. + * @throws ValidationException thrown if an Extended Graphic State isn't valid. */ @Override public void validate(PreflightContext context) throws ValidationException @@ -99,7 +99,7 @@ * @param context the context which contains the Resource dictionary. * @param egsEntry a resource COSDictionary. * @return the list of ExtGState dictionaries. - * @throws ValidationException thrown if a the Extended Graphic State isn't valid. + * @throws ValidationException thrown if an Extended Graphic State isn't valid. */ public List extractExtGStateDictionaries(PreflightContext context, COSDictionary egsEntry) throws ValidationException @@ -113,16 +113,13 @@ for (Object object : extGStates.keySet()) { COSName key = (COSName) object; - if (key.getName().matches(TRANPARENCY_DICTIONARY_KEY_EXTGSTATE_ENTRY_REGEX)) + COSBase gsBase = extGStates.getItem(key); + COSDictionary gsDict = COSUtils.getAsDictionary(gsBase, cosDocument); + if (gsDict == null) { - COSBase gsBase = extGStates.getItem(key); - COSDictionary gsDict = COSUtils.getAsDictionary(gsBase, cosDocument); - if (gsDict == null) - { - throw new ValidationException("The Extended Graphics State dictionary is invalid"); - } - listOfExtGState.add(gsDict); + throw new ValidationException("The Extended Graphics State dictionary is invalid"); } + listOfExtGState.add(gsDict); } } return listOfExtGState; diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ResourcesValidationProcess.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ResourcesValidationProcess.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ResourcesValidationProcess.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ResourcesValidationProcess.java 2018-11-28 17:18:40.000000000 +0000 @@ -54,7 +54,7 @@ import static org.apache.pdfbox.preflight.PreflightConfiguration.TILING_PATTERN_PROCESS; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_GRAPHIC_INVALID_PATTERN_DEFINITION; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_GRAPHIC_MAIN; -import static org.apache.pdfbox.preflight.PreflightConstants.TRANPARENCY_DICTIONARY_KEY_EXTGSTATE; +import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_EXTGSTATE; public class ResourcesValidationProcess extends AbstractProcess { @@ -145,7 +145,7 @@ */ protected void validateExtGStates(PreflightContext context, PDResources resources) throws ValidationException { - COSBase egsEntry = resources.getCOSObject().getItem(TRANPARENCY_DICTIONARY_KEY_EXTGSTATE); + COSBase egsEntry = resources.getCOSObject().getItem(TRANSPARENCY_DICTIONARY_KEY_EXTGSTATE); COSDocument cosDocument = context.getDocument().getDocument(); COSDictionary extGState = COSUtils.getAsDictionary(egsEntry, cosDocument); if (egsEntry != null) diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ShadingPatternValidationProcess.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ShadingPatternValidationProcess.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ShadingPatternValidationProcess.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ShadingPatternValidationProcess.java 2018-11-28 17:18:40.000000000 +0000 @@ -25,7 +25,7 @@ import static org.apache.pdfbox.preflight.PreflightConfiguration.EXTGSTATE_PROCESS; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_GRAPHIC_INVALID_UNKNOWN_COLOR_SPACE; -import static org.apache.pdfbox.preflight.PreflightConstants.TRANPARENCY_DICTIONARY_KEY_EXTGSTATE; +import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_EXTGSTATE; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.pdmodel.PDPage; @@ -109,7 +109,7 @@ throws ValidationException { COSDictionary resources = (COSDictionary) shadingRes.getCOSObject().getDictionaryObject( - TRANPARENCY_DICTIONARY_KEY_EXTGSTATE); + TRANSPARENCY_DICTIONARY_KEY_EXTGSTATE); if (resources != null) { ContextHelper.validateElement(context, resources, EXTGSTATE_PROCESS); diff -Nru libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/Validator_A1b.java libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/Validator_A1b.java --- libpdfbox2-java-2.0.9/preflight/src/main/java/org/apache/pdfbox/preflight/Validator_A1b.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/main/java/org/apache/pdfbox/preflight/Validator_A1b.java 2018-11-28 17:18:38.000000000 +0000 @@ -68,6 +68,17 @@ System.exit(1); } + try + { + // force KCMS (faster than LCMS) if available + Class.forName("sun.java2d.cmm.kcms.KcmsServiceProvider"); + System.setProperty("sun.java2d.cmm", "sun.java2d.cmm.kcms.KcmsServiceProvider"); + } + catch (ClassNotFoundException e) + { + // ignore + } + // is output xml ? int posFile = 0; boolean outputXml = "xml".equals(args[posFile]); diff -Nru libpdfbox2-java-2.0.9/preflight/src/test/java/org/apache/pdfbox/preflight/integration/TestValidFiles.java libpdfbox2-java-2.0.13/preflight/src/test/java/org/apache/pdfbox/preflight/integration/TestValidFiles.java --- libpdfbox2-java-2.0.9/preflight/src/test/java/org/apache/pdfbox/preflight/integration/TestValidFiles.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/test/java/org/apache/pdfbox/preflight/integration/TestValidFiles.java 2018-11-28 17:18:38.000000000 +0000 @@ -79,7 +79,7 @@ { // find isartor files String isartor = System.getProperty(ISARTOR_FILES); - if (isartor == null) + if (isartor == null || isartor.isEmpty()) { staticLogger.warn(ISARTOR_FILES + " (where are isartor pdf files) is not defined."); return stopIfExpected(); diff -Nru libpdfbox2-java-2.0.9/preflight/src/test/java/org/apache/pdfbox/preflight/metadata/TestSynchronizedMetadataValidation.java libpdfbox2-java-2.0.13/preflight/src/test/java/org/apache/pdfbox/preflight/metadata/TestSynchronizedMetadataValidation.java --- libpdfbox2-java-2.0.9/preflight/src/test/java/org/apache/pdfbox/preflight/metadata/TestSynchronizedMetadataValidation.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/test/java/org/apache/pdfbox/preflight/metadata/TestSynchronizedMetadataValidation.java 2018-11-28 17:18:38.000000000 +0000 @@ -123,14 +123,7 @@ @Test public void testEmptyXMP() throws Exception { - title = "TITLE"; - author = "AUTHOR(S)"; - subject = "SUBJECTS"; - keywords = "KEYWORD(S)"; - creator = "CREATOR"; - producer = "PRODUCER"; - creationDate = Calendar.getInstance(); - modifyDate = Calendar.getInstance(); + initValues(); // Writing info in Document Information dictionary // TITLE @@ -173,14 +166,7 @@ @Test public void testEmptyXMPSchemas() throws Exception { - title = "TITLE"; - author = "AUTHOR(S)"; - subject = "SUBJECTS"; - keywords = "KEYWORD(S)"; - creator = "CREATOR"; - producer = "PRODUCER"; - creationDate = Calendar.getInstance(); - modifyDate = Calendar.getInstance(); + initValues(); // building temporary XMP metadata (but empty) metadata.createAndAddDublinCoreSchema(); @@ -373,16 +359,9 @@ * @throws Exception */ @Test - public void testAllInfoSynhcronized() throws Exception + public void testAllInfoSynchronized() throws Exception { - title = "TITLE"; - author = "AUTHOR(S)"; - subject = "SUBJECTS"; - keywords = "KEYWORD(S)"; - creator = "CREATOR"; - producer = "PRODUCER"; - creationDate = Calendar.getInstance(); - modifyDate = Calendar.getInstance(); + initValues(); // building temporary XMP metadata DublinCoreSchema dc = metadata.createAndAddDublinCoreSchema(); @@ -447,14 +426,7 @@ @Test public void testBadPrefixSchemas() throws Exception { - title = "TITLE"; - author = "AUTHOR(S)"; - subject = "SUBJECTS"; - keywords = "KEYWORD(S)"; - creator = "CREATOR"; - producer = "PRODUCER"; - creationDate = Calendar.getInstance(); - modifyDate = Calendar.getInstance(); + initValues(); // building temporary XMP metadata DublinCoreSchema dc = new DublinCoreSchema(metadata, "dctest"); @@ -514,14 +486,7 @@ @Test public void testdoublePrefixSchemas() throws Exception { - title = "TITLE"; - author = "AUTHOR(S)"; - subject = "SUBJECTS"; - keywords = "KEYWORD(S)"; - creator = "CREATOR"; - producer = "PRODUCER"; - creationDate = Calendar.getInstance(); - modifyDate = Calendar.getInstance(); + initValues(); // building temporary XMP metadata DublinCoreSchema dc = metadata.createAndAddDublinCoreSchema(); @@ -575,7 +540,40 @@ { throw new Exception(e.getMessage()); } + } + + /** + * Tests that two date values, which are from different time zones but + * really identical, are detected as such. + * + * @throws Exception + */ + @Test + public void testPDFBox4292() throws Exception + { + initValues(); + + Calendar cal1 = org.apache.pdfbox.util.DateConverter.toCalendar("20180817115837+02'00'"); + Calendar cal2 = org.apache.xmpbox.DateConverter.toCalendar("2018-08-17T09:58:37Z"); + + XMPBasicSchema xmp = metadata.createAndAddXMPBasicSchema(); + + dico.setCreationDate(cal1); + xmp.setCreateDate(cal2); + dico.setModificationDate(cal1); + xmp.setModifyDate(cal2); + // Launching synchronization test + try + { + ve = sync.validateMetadataSynchronization(doc, metadata); + // Test unsychronized value + Assert.assertEquals(0, ve.size()); + } + catch (ValidationException e) + { + throw new Exception(e.getMessage()); + } } @After @@ -595,4 +593,21 @@ */ } + private void initValues() + { + title = "TITLE"; + author = "AUTHOR(S)"; + subject = "SUBJECTS"; + keywords = "KEYWORD(S)"; + creator = "CREATOR"; + producer = "PRODUCER"; + creationDate = Calendar.getInstance(); + modifyDate = Calendar.getInstance(); + + // PDFBOX-4292: because xmp keeps the milliseconds before writing to XML, + // but COS doesn't, tests would fail when calendar values are compared + // so reset the milliseconds. + creationDate.set(Calendar.MILLISECOND, 0); + modifyDate.set(Calendar.MILLISECOND, 0); + } } diff -Nru libpdfbox2-java-2.0.9/preflight/src/test/resources/log4j.xml libpdfbox2-java-2.0.13/preflight/src/test/resources/log4j.xml --- libpdfbox2-java-2.0.9/preflight/src/test/resources/log4j.xml 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight/src/test/resources/log4j.xml 1970-01-01 00:00:00.000000000 +0000 @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff -Nru libpdfbox2-java-2.0.9/preflight-app/pom.xml libpdfbox2-java-2.0.13/preflight-app/pom.xml --- libpdfbox2-java-2.0.9/preflight-app/pom.xml 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/preflight-app/pom.xml 2018-11-28 17:18:40.000000000 +0000 @@ -23,7 +23,7 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 ../parent/pom.xml @@ -80,7 +80,7 @@ true *;scope=provided;inline=org/apache/**|org/bouncycastle/**|com/ibm/icu/**|META-INF/services/** ${project.url} - !junit.framework,!junit.textui,javax.*;resolution:=optional,org.apache.avalon.framework.logger;resolution:=optional,org.apache.log;resolution:=optional,org.apache.log4j;resolution:=optional,* + !junit.framework,!junit.textui,javax.*;resolution:=optional,org.apache.avalon.framework.logger;resolution:=optional,org.apache.log;resolution:=optional,* org.apache.pdfbox.preflight.Validator_A1b diff -Nru libpdfbox2-java-2.0.9/RELEASE-NOTES.txt libpdfbox2-java-2.0.13/RELEASE-NOTES.txt --- libpdfbox2-java-2.0.9/RELEASE-NOTES.txt 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/RELEASE-NOTES.txt 2018-11-28 17:18:38.000000000 +0000 @@ -1,11 +1,11 @@ -Release Notes -- Apache PDFBox -- Version 2.0.9 +Release Notes -- Apache PDFBox -- Version 2.0.13 Introduction ------------ The Apache PDFBox library is an open source Java tool for working with PDF documents. -This is an incremental bugfix release based on the earlier 2.0.8 release. It contains +This is an incremental bugfix release based on the earlier 2.0.12 release. It contains a couple of fixes and small improvements. For more details on these changes and all the other fixes and improvements @@ -14,98 +14,59 @@ Bug -[PDFBOX-2142] some /ICCBased colorspaces not rendered correctly -[PDFBOX-2558] Image missing -[PDFBOX-2917] PDF to Image, faint/dim Images -[PDFBOX-3377] font caching never stops in AIX -[PDFBOX-3401] PDObjectReference getReferencedObject() returns null if referenced obj is not a COSStream -[PDFBOX-3457] Glyphs rendered in wrong width -[PDFBOX-3956] Truncated pdf can't be repaired anymore -[PDFBOX-3990] Rendering will never complete -[PDFBOX-3994] ClassCastException in COSParser.bfSearchForTrailer -[PDFBOX-3997] Cannot encode strings with of surrogate pairs -[PDFBOX-4001] Rendering LineFeed (No Unicode mapping for .notdef (10)) -[PDFBOX-4002] Layer Utility - Text not being displayed when overlaying -[PDFBOX-4005] Incorrect use of PDNumberTreeNode in PDPageLabels -[PDFBOX-4006] PDFBox throws NullPointerException when parsing PDF -[PDFBOX-4011] BBox in signature forms has wrong order -[PDFBOX-4012] PDF with incremental save is shown blank -[PDFBOX-4015] java.awt.color.CMMException: LCMS error 13: Couldn't link the profiles -[PDFBOX-4018] NPE in sanitizeType in org.apache.pdfbox.pdmodel.PDPageTree -[PDFBOX-4021] Font missing when building from source makes build fail -[PDFBOX-4027] IndexOutOfBoundsException when XObject form matrix has only 5 elements -[PDFBOX-4030] ClassCastException when matrix array has indirect objects -[PDFBOX-4038] CFF font Blue values and other delta encoded lists read incorrectly -[PDFBOX-4043] ExtractImages doesn't extract images from PDPatterns -[PDFBOX-4044] Unable to process overlay on Cognos PDF documents -[PDFBOX-4052] Number '------------06836305' is getting too long, stop reading at offset 36 -[PDFBOX-4053] build test fails on jdk9 -[PDFBOX-4058] High memory consumption when extracting image from PDF file -[PDFBOX-4060] Slow rendering of PDF file with DeviceN jpeg file -[PDFBOX-4061] ClassCastException PDActionJavaScript cannot be cast to PDDestination -[PDFBOX-4064] cm operator has 7 numbers -[PDFBOX-4066] Merging documents with nested fields duplicates child fields -[PDFBOX-4083] Line annotation /LL, /LLE and /LLO have wrong default values -[PDFBOX-4084] Can't draw PDFs while ANNOTS is COSStream instead of COSArray -[PDFBOX-4085] COSString cannot be cast to COSDictionary error -[PDFBOX-4088] Root/StructTreeRoot/K/S must be name, not string (merge) -[PDFBOX-4091] Cannot analyze signatures : Wrong type of referenced length object COSObject -[PDFBOX-4093] illegible characters in rendered image -[PDFBOX-4103] Optional Content Groups with same names can't have different visibility -[PDFBOX-4105] Copyright 2011 adam -[PDFBOX-4107] NPE at PDFMergerUtility -[PDFBOX-4108] /Length1 not needed for /CIDToGIDMap -[PDFBOX-4113] Debugger file open dialog has incorrect filter on Mac -[PDFBOX-4114] ICCBased color spaces wrong color output -[PDFBOX-4115] Problem creating PDF with German text using embedded Type1 (PFB) font -[PDFBOX-4125] FDFField.writeXML KO with String -[PDFBOX-4129] Deleted fonts not detected when checking cache -[PDFBOX-4140] Crash when repeating flag is outside of range. -[PDFBOX-4146] Patch: Fix for appearance of visible signature -[PDFBOX-4153] Outlines missing in some versions, not in others - -New Feature - -[PDFBOX-3198] Visible Signature N2 layer / Support signature with text -[PDFBOX-4106] Vertical text creation -[PDFBOX-4117] Implement GoToE action-type +[PDFBOX-3646] - Annotations parsed from XFDF containing ampersand characters are not properly imported +[PDFBOX-4163] - Java 11 compile error +[PDFBOX-4326] - PDF with JPEG2000 image can't be rendered +[PDFBOX-4327] - NullPointerException in PDFStreamEngine.processSoftMask() when running ExtractImages +[PDFBOX-4330] - NumberFormatException in CFFParser.readRealNumber() +[PDFBOX-4331] - Make jdk9 profile activation automatic +[PDFBOX-4333] - ClassCastException when loading PDF +[PDFBOX-4336] - "CMap is invalid" exception thrown +[PDFBOX-4338] - ArrayIndexOutOfBoundsException in COSParser +[PDFBOX-4339] - NullPointerException in COSParser +[PDFBOX-4343] - Prevent calling addSignature twice +[PDFBOX-4345] - FDFAnnotation.richContentsToString does not evaluate text nodes which have siblings in the XML +[PDFBOX-4347] - ArrayIndexOutOfBoundsException in PDFXrefStreamParser +[PDFBOX-4348] - ClassCastException in COSParser +[PDFBOX-4349] - ClassCastException in COSParser +[PDFBOX-4350] - IllegalArgumentException in PDFObjectStreamParser +[PDFBOX-4351] - IndexOutOfBoundsException when reading from InputStreamSource +[PDFBOX-4352] - NullPointerException in COSParser +[PDFBOX-4353] - NullPointerException in PDFXrefStreamParser +[PDFBOX-4354] - NumberFormatException in COSParser +[PDFBOX-4355] - PDFTextStripperByArea dies on Chinese/Japanese files +[PDFBOX-4357] - IllegalArgumentException "root cannot be null" +[PDFBOX-4359] - Bad sizing of signature field inside rotated page +[PDFBOX-4360] - ArrayIndexOutOfBoundsException in ASCIIHexFilter +[PDFBOX-4361] - ArrayIndexOutOfBoundsException in COSParser +[PDFBOX-4364] - example AddValidationInformation fails with scratchfile error +[PDFBOX-4365] - PDFDebugger: JComboBox does not take generic parameters in Java 1.6 +[PDFBOX-4366] - NullPointerException in PDButton.updateByValue() when appearance missing +[PDFBOX-4367] - Error expected floating point number actual='18-5' +[PDFBOX-4369] - unsupported ExtractText -force option still appears in online 2.0 docs +[PDFBOX-4372] - Stack overflow around PDFStreamEngine.processStream +[PDFBOX-4374] - Switch from log4j to slf4j +[PDFBOX-4377] - Verify CRL in AddValidation example +[PDFBOX-4381] - Revocation CRL check should be done at signing time in AddValidation example +[PDFBOX-4383] - PDFMergerUtility seems to leave source file open +[PDFBOX-4384] - PDF/A Document Validation out of memory Improvement -[PDFBOX-1848] Time Stamp Document Level Sigature -[PDFBOX-2092] Very slow rendering of scanned document -[PDFBOX-3340] Image decoded twice without a real need -[PDFBOX-3984] Add validation data of signer to document -[PDFBOX-3992] Implement show text with positioning operator (TJ) -[PDFBOX-3998] Inform the user when not using KCMS with jdk8 or higher + set KCMS in cli -[PDFBOX-4020] Into existing signature embedded signed timestamp for validation -[PDFBOX-4022] Cache ColorSpace instances in PDColorSpace.java -[PDFBOX-4024] YCbCr JPEGs not implemented -[PDFBOX-4025] Other page sizes than US Letter should be selectable in TextToPDF -[PDFBOX-4040] Get/set Viewports in PDPage -[PDFBOX-4119] KCMS takes too much time -[PDFBOX-4121] (-Dorg.apache.pdfbox.rendering.UsePureJavaCMYKConversion=true) takes much time -[PDFBOX-4137] Allow subsampled/downscaled rendering of images, and rendering subimages -[PDFBOX-4139] Optimize memory footprint of CID mappings within CMaps -[PDFBOX-4142] Don't use md5 checksum due to changes to the release distribuition policy -[PDFBOX-4150] Optimize clipping text rendering modes +[PDFBOX-4335] - Overlay should implement Closeable +[PDFBOX-4363] - [Patch] Add a common interface PDShadingPaint for all shading paints +[PDFBOX-4371] - Improve ExtractText utility so that it can extract rotated text automatically +[PDFBOX-4375] - Change visibility of Overlay#loadPDF to protected -Wish +Test -[PDFBOX-4094] Add support for a flag disabling the rendering of PDF annotations in PDFRenderer +[PDFBOX-4373] - Add additional unit tests Task -[PDFBOX-2852] Improve code quality (2) -[PDFBOX-3991] PDPageContentStream has sometimes float, sometimes double parameters -[PDFBOX-4050] Check user password when decrypting with owner password in build test -[PDFBOX-4055] Output info when PDFBox JBIG2 ImageIO is released -[PDFBOX-4135] Modify PDFBox builds für Apache JBIG2 plugin -[PDFBOX-4143] repository-cached download of fontbox test files +[PDFBOX-4358] - Prevent stack overflow in COSDictionary.toString() +[PDFBOX-4362] - Create simple text extraction example -Sub-task - -[PDFBOX-4029] Rendering transparency groups in patterns Release Contents ---------------- @@ -114,10 +75,10 @@ The archive can be unpacked with the jar tool from your JDK installation. See the README.txt file for instructions on how to build this release. -The source archive is accompanied by SHA512 checksums and a PGP signature +The source archive is accompanied by a SHA512 checksum and a PGP signature that you can use to verify the authenticity of your download. The public key used for the PGP signature can be found at -https://svn.apache.org/repos/asf/pdfbox/KEYS. +https://www.apache.org/dist/pdfbox/KEYS. About Apache PDFBox ------------------- diff -Nru libpdfbox2-java-2.0.9/tools/pom.xml libpdfbox2-java-2.0.13/tools/pom.xml --- libpdfbox2-java-2.0.9/tools/pom.xml 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/tools/pom.xml 2018-11-28 17:18:40.000000000 +0000 @@ -23,7 +23,7 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 ../parent/pom.xml diff -Nru libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/ExtractImages.java libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/ExtractImages.java --- libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/ExtractImages.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/ExtractImages.java 2018-11-28 17:18:40.000000000 +0000 @@ -178,9 +178,8 @@ throw new IOException("You do not have permission to extract images"); } - for (int i = 0; i < document.getNumberOfPages(); i++) // todo: ITERATOR would be much better + for (PDPage page : document.getPages()) { - PDPage page = document.getPage(i); ImageGraphicsEngine extractor = new ImageGraphicsEngine(page); extractor.run(); } @@ -214,6 +213,9 @@ PDTransparencyGroup group = softMask.getGroup(); if (group != null) { + // PDFBOX-4327: without this line NPEs will occur + res.getExtGState(name).copyIntoGraphicsState(getGraphicsState()); + processSoftMask(group); } } diff -Nru libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/ExtractText.java libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/ExtractText.java --- libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/ExtractText.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/ExtractText.java 2018-11-28 17:18:40.000000000 +0000 @@ -23,24 +23,37 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; import org.apache.pdfbox.pdmodel.encryption.AccessPermission; import org.apache.pdfbox.text.PDFTextStripper; +import org.apache.pdfbox.text.TextPosition; +import org.apache.pdfbox.util.Matrix; /** * This is the main program that simply parses the pdf document and transforms it * into text. * * @author Ben Litchfield + * @author Tilman Hausherr */ public final class ExtractText { + private static final Log LOG = LogFactory.getLog(ExtractText.class); + private static final String PASSWORD = "-password"; private static final String ENCODING = "-encoding"; private static final String CONSOLE = "-console"; @@ -50,7 +63,8 @@ private static final String IGNORE_BEADS = "-ignoreBeads"; private static final String DEBUG = "-debug"; private static final String HTML = "-html"; - + private static final String ALWAYSNEXT = "-alwaysNext"; + private static final String ROTATION_MAGIC = "-rotationMagic"; private static final String STD_ENCODING = "UTF-8"; /* @@ -93,6 +107,8 @@ boolean toHTML = false; boolean sort = false; boolean separateBeads = true; + boolean alwaysNext = false; + boolean rotationMagic = false; String password = ""; String encoding = STD_ENCODING; String pdfFile = null; @@ -143,6 +159,14 @@ { separateBeads = false; } + else if (args[i].equals(ALWAYSNEXT)) + { + alwaysNext = true; + } + else if (args[i].equals(ROTATION_MAGIC)) + { + rotationMagic = true; + } else if( args[i].equals( DEBUG ) ) { debug = true; @@ -212,30 +236,43 @@ } output = new OutputStreamWriter( new FileOutputStream( outputFile ), encoding ); } + startTime = startProcessing("Starting text extraction"); + if (debug) + { + System.err.println("Writing to " + outputFile); + } PDFTextStripper stripper; if(toHTML) { + // HTML stripper can't work page by page because of startDocument() callback stripper = new PDFText2HTML(); + stripper.setSortByPosition(sort); + stripper.setShouldSeparateByBeads(separateBeads); + stripper.setStartPage(startPage); + stripper.setEndPage(endPage); + + // Extract text for main document: + stripper.writeText(document, output); } else { - stripper = new PDFTextStripper(); - } - stripper.setSortByPosition( sort ); - stripper.setShouldSeparateByBeads( separateBeads ); - stripper.setStartPage( startPage ); - stripper.setEndPage( endPage ); + if (rotationMagic) + { + stripper = new FilteredTextStripper(); + } + else + { + stripper = new PDFTextStripper(); + } + stripper.setSortByPosition(sort); + stripper.setShouldSeparateByBeads(separateBeads); - startTime = startProcessing("Starting text extraction"); - if (debug) - { - System.err.println("Writing to "+outputFile); + // Extract text for main document: + extractPages(startPage, Math.min(endPage, document.getNumberOfPages()), + stripper, document, output, rotationMagic, alwaysNext); } - - // Extract text for main document: - stripper.writeText( document, output ); - + // ... also for any embedded PDFs: PDDocumentCatalog catalog = document.getDocumentCatalog(); PDDocumentNameDictionary names = catalog.getNames(); @@ -266,17 +303,20 @@ try { subDoc = PDDocument.load(fis); + if (toHTML) + { + // will not really work because of HTML header + footer + stripper.writeText( subDoc, output ); + } + else + { + extractPages(1, subDoc.getNumberOfPages(), + stripper, subDoc, output, rotationMagic, alwaysNext); + } } finally { fis.close(); - } - try - { - stripper.writeText( subDoc, output ); - } - finally - { IOUtils.closeQuietly(subDoc); } } @@ -294,6 +334,58 @@ } } + private void extractPages(int startPage, int endPage, + PDFTextStripper stripper, PDDocument document, Writer output, + boolean rotationMagic, boolean alwaysNext) throws IOException + { + for (int p = startPage; p <= endPage; ++p) + { + stripper.setStartPage(p); + stripper.setEndPage(p); + try + { + if (rotationMagic) + { + PDPage page = document.getPage(p - 1); + int rotation = page.getRotation(); + page.setRotation(0); + AngleCollector angleCollector = new AngleCollector(); + angleCollector.setStartPage(p); + angleCollector.setEndPage(p); + angleCollector.writeText(document, new NullWriter()); + // rotation magic + for (int angle : angleCollector.getAngles()) + { + // prepend a transformation + // (we could skip these parts for angle 0, but it doesn't matter much) + PDPageContentStream cs = new PDPageContentStream(document, page, + PDPageContentStream.AppendMode.PREPEND, false); + cs.transform(Matrix.getRotateInstance(-Math.toRadians(angle), 0, 0)); + cs.close(); + + stripper.writeText(document, output); + + // remove prepended transformation + ((COSArray) page.getCOSObject().getItem(COSName.CONTENTS)).remove(0); + } + page.setRotation(rotation); + } + else + { + stripper.writeText(document, output); + } + } + catch (IOException ex) + { + if (!alwaysNext) + { + throw ex; + } + LOG.error("Failed to process page " + p, ex); + } + } + } + private long startProcessing(String message) { if (debug) @@ -320,19 +412,100 @@ { String message = "Usage: java -jar pdfbox-app-x.y.z.jar ExtractText [options] [output-text-file]\n" + "\nOptions:\n" - + " -password : Password to decrypt document\n" - + " -encoding : UTF-8 (default) or ISO-8859-1, UTF-16BE, UTF-16LE, etc.\n" - + " -console : Send text to console instead of file\n" - + " -html : Output in HTML format instead of raw text\n" - + " -sort : Sort the text before writing\n" - + " -ignoreBeads : Disables the separation by beads\n" - + " -debug : Enables debug output about the time consumption of every stage\n" - + " -startPage : The first page to start extraction(1 based)\n" - + " -endPage : The last page to extract(inclusive)\n" - + " : The PDF document to use\n" - + " [output-text-file] : The file to write the text to"; + + " -password : Password to decrypt document\n" + + " -encoding : UTF-8 (default) or ISO-8859-1, UTF-16BE,\n" + + " UTF-16LE, etc.\n" + + " -console : Send text to console instead of file\n" + + " -html : Output in HTML format instead of raw text\n" + + " -sort : Sort the text before writing\n" + + " -ignoreBeads : Disables the separation by beads\n" + + " -debug : Enables debug output about the time consumption\n" + + " of every stage\n" + + " -alwaysNext : Process next page (if applicable) despite\n" + + " IOException (ignored when -html)\n" + + " -rotationMagic : Analyze each page for rotated/skewed text,\n" + + " rotate to 0° and extract separately\n" + + " (slower, and ignored when -html)\n" + + " -startPage : The first page to start extraction (1 based)\n" + + " -endPage : The last page to extract (1 based, inclusive)\n" + + " : The PDF document to use\n" + + " [output-text-file] : The file to write the text to"; System.err.println(message); System.exit( 1 ); } } + +/** + * Collect all angles while doing text extraction. Angles are in degrees and rounded to the closest + * integer (to avoid slight differences from floating point arithmethic resulting in similarly + * angled glyphs being treated separately). This class must be constructed for each page so that the + * angle set is initialized. + */ +class AngleCollector extends PDFTextStripper +{ + private final Set angles = new TreeSet(); + + AngleCollector() throws IOException + { + } + + Set getAngles() + { + return angles; + } + + @Override + protected void processTextPosition(TextPosition text) + { + Matrix m = text.getTextMatrix(); + int angle = (int) Math.round(Math.toDegrees(Math.atan2(m.getShearY(), m.getScaleY()))); + angle = (angle + 360) % 360; + angles.add(angle); + } +} + +/** + * TextStripper that only processes glyphs that have angle 0. + */ +class FilteredTextStripper extends PDFTextStripper +{ + FilteredTextStripper() throws IOException + { + } + + @Override + protected void processTextPosition(TextPosition text) + { + Matrix m = text.getTextMatrix(); + int angle = (int) Math.round(Math.toDegrees(Math.atan2(m.getShearY(), m.getScaleY()))); + if (angle == 0) + { + super.processTextPosition(text); + } + } +} + +/** + * Dummy output. + */ +class NullWriter extends Writer +{ + @Override + public void write(char[] cbuf, int off, int len) throws IOException + { + // do nothing + } + + @Override + public void flush() throws IOException + { + // do nothing + } + + @Override + public void close() throws IOException + { + // do nothing + } +} diff -Nru libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java --- libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java 2018-11-28 17:18:40.000000000 +0000 @@ -65,12 +65,31 @@ public static boolean writeImage(BufferedImage image, String filename, int dpi) throws IOException { + return writeImage(image, filename, dpi, 1.0f); + } + + /** + * Writes a buffered image to a file using the given image format. + * See {@link #writeImage(BufferedImage image, String formatName, + * OutputStream output, int dpi, float quality)} for more details. + * + * @param image the image to be written + * @param filename used to construct the filename for the individual image. Its suffix will be + * used as the image format. + * @param dpi the resolution in dpi (dots per inch) to be used in metadata + * @param quality quality to be used when compressing the image (0 < quality < 1.0f) + * @return true if the image file was produced, false if there was an error. + * @throws IOException if an I/O error occurs + */ + public static boolean writeImage(BufferedImage image, String filename, + int dpi, float quality) throws IOException + { File file = new File(filename); FileOutputStream output = new FileOutputStream(file); try { String formatName = filename.substring(filename.lastIndexOf('.') + 1); - return writeImage(image, formatName, output, dpi); + return writeImage(image, formatName, output, dpi, quality); } finally { @@ -151,8 +170,8 @@ * Compression is fixed for PNG, GIF, BMP and WBMP, dependent of the quality * parameter for JPG, and dependent of bit count for TIFF (a bitonal image * will be compressed with CCITT G4, a color image with LZW). Creating a - * TIFF image is only supported if the jai_imageio library is in the class - * path. + * TIFF image is only supported if the jai_imageio library (or equivalent) + * is in the class path. * * @param image the image to be written * @param formatName the target format (ex. "png") @@ -166,6 +185,33 @@ public static boolean writeImage(BufferedImage image, String formatName, OutputStream output, int dpi, float quality) throws IOException { + return writeImage(image, formatName, output, dpi, quality, ""); + } + + /** + * Writes a buffered image to a file using the given image format. + * Compression is fixed for PNG, GIF, BMP and WBMP, dependent of the quality + * parameter for JPG, and dependent of bit count for TIFF (a bitonal image + * will be compressed with CCITT G4, a color image with LZW). Creating a + * TIFF image is only supported if the jai_imageio library is in the class + * path. + * + * @param image the image to be written + * @param formatName the target format (ex. "png") + * @param output the output stream to be used for writing + * @param dpi the resolution in dpi (dots per inch) to be used in metadata + * @param compressionQuality quality to be used when compressing the image + * (0 < quality < 1.0f) + * @param compressionType Advanced users only, and only relevant for TIFF + * files: If null, save uncompressed; if empty string, use logic explained + * above; other valid values are found in the javadoc of + * TIFFImageWriteParam. + * @return true if the image file was produced, false if there was an error. + * @throws IOException if an I/O error occurs + */ + public static boolean writeImage(BufferedImage image, String formatName, OutputStream output, + int dpi, float compressionQuality, String compressionType) throws IOException + { ImageOutputStream imageOutput = null; ImageWriter writer = null; try @@ -217,13 +263,24 @@ param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); if (formatName.toLowerCase().startsWith("tif")) { - // TIFF compression - TIFFUtil.setCompressionType(param, image); + if ("".equals(compressionType)) + { + // default logic + TIFFUtil.setCompressionType(param, image); + } + else + { + param.setCompressionType(compressionType); + if (compressionType != null) + { + param.setCompressionQuality(compressionQuality); + } + } } else { param.setCompressionType(param.getCompressionTypes()[0]); - param.setCompressionQuality(quality); + param.setCompressionQuality(compressionQuality); } } diff -Nru libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/OverlayPDF.java libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/OverlayPDF.java --- libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/OverlayPDF.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/OverlayPDF.java 2018-11-28 17:18:40.000000000 +0000 @@ -137,20 +137,23 @@ usage(); } - try + try { PDDocument result = overlayer.overlay(specificPageOverlayFile); result.save(outputFilename); result.close(); - // close the input files AFTER saving the resulting file as some - // streams are shared among the input and the output files - overlayer.close(); } catch (IOException e) { LOG.error("Overlay failed: " + e.getMessage(), e); throw e; } + finally + { + // close the input files AFTER saving the resulting file as some + // streams are shared among the input and the output files + overlayer.close(); + } } private static void usage() diff -Nru libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/PDFBox.java libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/PDFBox.java --- libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/PDFBox.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/PDFBox.java 2018-11-28 17:18:40.000000000 +0000 @@ -116,7 +116,6 @@ String message = "PDFBox version: \""+ Version.getVersion()+ "\"" + "\nUsage: java -jar pdfbox-app-x.y.z.jar \n" + "\nPossible commands are:\n" - + " ConvertColorspace\n" + " Decrypt\n" + " Encrypt\n" + " ExtractText\n" diff -Nru libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/PDFToImage.java libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/PDFToImage.java --- libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/PDFToImage.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/PDFToImage.java 2018-11-28 17:18:40.000000000 +0000 @@ -49,8 +49,10 @@ private static final String COLOR = "-color"; private static final String RESOLUTION = "-resolution"; private static final String DPI = "-dpi"; + private static final String QUALITY = "-quality"; private static final String CROPBOX = "-cropbox"; private static final String TIME = "-time"; + private static final String SUBSAMPLING = "-subsampling"; /** * private constructor. @@ -91,11 +93,13 @@ int endPage = Integer.MAX_VALUE; String color = "rgb"; int dpi; + float quality = 1.0f; float cropBoxLowerLeftX = 0; float cropBoxLowerLeftY = 0; float cropBoxUpperRightX = 0; float cropBoxUpperRightY = 0; boolean showTime = false; + boolean subsampling = false; try { dpi = Toolkit.getDefaultToolkit().getScreenResolution(); @@ -163,6 +167,11 @@ i++; dpi = Integer.parseInt(args[i]); } + else if( args[i].equals( QUALITY ) ) + { + i++; + quality = Float.parseFloat(args[i]); + } else if( args[i].equals( CROPBOX ) ) { i++; @@ -178,6 +187,10 @@ { showTime = true; } + else if( args[i].equals( SUBSAMPLING ) ) + { + subsampling = true; + } else { if( pdfFile == null ) @@ -242,11 +255,12 @@ boolean success = true; endPage = Math.min(endPage, document.getNumberOfPages()); PDFRenderer renderer = new PDFRenderer(document); + renderer.setSubsamplingAllowed(subsampling); for (int i = startPage - 1; i < endPage; i++) { BufferedImage image = renderer.renderImageWithDPI(i, dpi, imageType); String fileName = outputPrefix + (i + 1) + "." + imageFormat; - success &= ImageIOUtil.writeImage(image, fileName, dpi); + success &= ImageIOUtil.writeImage(image, fileName, dpi, quality); } // performance stats @@ -289,10 +303,12 @@ + " -page : The only page to extract (1-based)\n" + " -startPage : The first page to start extraction (1-based)\n" + " -endPage : The last page to extract(inclusive)\n" - + " -color : The color depth (valid: bilevel, gray, rgb, rgba)\n" - + " -dpi : The DPI of the output image\n" + + " -color : The color depth (valid: bilevel, gray, rgb (default), rgba)\n" + + " -dpi : The DPI of the output image, default: screen resolution or 96 if unknown\n" + + " -quality : The quality to be used when compressing the image (0 < quality <= 1 (default))\n" + " -cropbox : The page area to export\n" + " -time : Prints timing information to stdout\n" + + " -subsampling : Activate subsampling (for PDFs with huge images)\n" + " : The PDF document to use\n"; System.err.println(message); @@ -328,7 +344,6 @@ rectangle.setUpperRightX(c); rectangle.setUpperRightY(d); page.setCropBox(rectangle); - } } } diff -Nru libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/TextToPDF.java libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/TextToPDF.java --- libpdfbox2-java-2.0.9/tools/src/main/java/org/apache/pdfbox/tools/TextToPDF.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/tools/src/main/java/org/apache/pdfbox/tools/TextToPDF.java 2018-11-28 17:18:40.000000000 +0000 @@ -328,9 +328,11 @@ throw new IOException( "Unknown argument: " + args[i] ); } } - - app.createPDFFromText( doc, new FileReader( args[args.length-1] ) ); - doc.save( args[args.length-2] ); + + FileReader fileReader = new FileReader(args[args.length - 1]); + app.createPDFFromText(doc, fileReader); + fileReader.close(); + doc.save(args[args.length - 2]); } } finally diff -Nru libpdfbox2-java-2.0.9/tools/src/test/java/org/apache/pdfbox/tools/imageio/TestImageIOUtils.java libpdfbox2-java-2.0.13/tools/src/test/java/org/apache/pdfbox/tools/imageio/TestImageIOUtils.java --- libpdfbox2-java-2.0.9/tools/src/test/java/org/apache/pdfbox/tools/imageio/TestImageIOUtils.java 2018-03-20 16:19:42.000000000 +0000 +++ libpdfbox2-java-2.0.13/tools/src/test/java/org/apache/pdfbox/tools/imageio/TestImageIOUtils.java 2018-11-28 17:18:40.000000000 +0000 @@ -24,8 +24,10 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; +import java.io.OutputStream; import java.util.HashSet; import java.util.Iterator; import java.util.Set; @@ -123,43 +125,54 @@ checkSaveResources(document.getPage(0).getResources()); // testing PNG - writeImage(document, imageType, outDir + file.getName() + "-", ImageType.RGB, dpi); + writeImage(document, imageType, outDir + file.getName() + "-", ImageType.RGB, dpi, 1, ""); checkResolution(outDir + file.getName() + "-1." + imageType, (int) dpi); checkFileTypeByContent(outDir + file.getName() + "-1." + imageType, FileType.PNG); // testing JPG/JPEG imageType = "jpg"; - writeImage(document, imageType, outDir + file.getName() + "-", ImageType.RGB, dpi); + writeImage(document, imageType, outDir + file.getName() + "-", ImageType.RGB, dpi, 0.5f, ""); checkResolution(outDir + file.getName() + "-1." + imageType, (int) dpi); checkFileTypeByContent(outDir + file.getName() + "-1." + imageType, FileType.JPEG); // testing BMP imageType = "bmp"; - writeImage(document, imageType, outDir + file.getName() + "-", ImageType.RGB, dpi); + writeImage(document, imageType, outDir + file.getName() + "-", ImageType.RGB, dpi, 1, ""); checkResolution(outDir + file.getName() + "-1." + imageType, (int) dpi); checkFileTypeByContent(outDir + file.getName() + "-1." + imageType, FileType.BMP); // testing GIF imageType = "gif"; - writeImage(document, imageType, outDir + file.getName() + "-", ImageType.RGB, dpi); + writeImage(document, imageType, outDir + file.getName() + "-", ImageType.RGB, dpi, 1, ""); // no META data posible for GIF, thus no dpi test checkFileTypeByContent(outDir + file.getName() + "-1." + imageType, FileType.GIF); // testing WBMP imageType = "wbmp"; - writeImage(document, imageType, outDir + file.getName() + "-", ImageType.BINARY, dpi); + writeImage(document, imageType, outDir + file.getName() + "-", ImageType.BINARY, dpi, 1, ""); // no META data posible for WBMP, thus no dpi test // testing TIFF imageType = "tif"; - writeImage(document, imageType, outDir + file.getName() + "-bw-", ImageType.BINARY, dpi); + writeImage(document, imageType, outDir + file.getName() + "-bw-", ImageType.BINARY, dpi, 1, ""); checkResolution(outDir + file.getName() + "-bw-1." + imageType, (int) dpi); checkTiffCompression(outDir + file.getName() + "-bw-1." + imageType, "CCITT T.6"); checkFileTypeByContent(outDir + file.getName() + "-bw-1." + imageType, FileType.TIFF); - writeImage(document, imageType, outDir + file.getName() + "-co-", ImageType.RGB, dpi); - checkResolution(outDir + file.getName() + "-co-1." + imageType, (int) dpi); - checkTiffCompression(outDir + file.getName() + "-co-1." + imageType, "LZW"); - checkFileTypeByContent(outDir + file.getName() + "-co-1." + imageType, FileType.TIFF); + + writeImage(document, imageType, outDir + file.getName() + "-coLZW-", ImageType.RGB, dpi, 1, ""); + checkResolution(outDir + file.getName() + "-coLZW-1." + imageType, (int) dpi); + checkTiffCompression(outDir + file.getName() + "-coLZW-1." + imageType, "LZW"); + checkFileTypeByContent(outDir + file.getName() + "-coLZW-1." + imageType, FileType.TIFF); + + writeImage(document, imageType, outDir + file.getName() + "-coJPEG-", ImageType.RGB, dpi, 0.5f, "JPEG"); + checkResolution(outDir + file.getName() + "-coJPEG-1." + imageType, (int) dpi); + checkTiffCompression(outDir + file.getName() + "-coJPEG-1." + imageType, "JPEG"); + checkFileTypeByContent(outDir + file.getName() + "-coJPEG-1." + imageType, FileType.TIFF); + + writeImage(document, imageType, outDir + file.getName() + "-coNone-", ImageType.RGB, dpi, 1, null); + checkResolution(outDir + file.getName() + "-coNone-1." + imageType, (int) dpi); + checkTiffCompression(outDir + file.getName() + "-coNone-1." + imageType, "None"); + checkFileTypeByContent(outDir + file.getName() + "-coNone-1." + imageType, FileType.TIFF); } finally { @@ -236,16 +249,19 @@ } private void writeImage(PDDocument document, String imageFormat, String outputPrefix, - ImageType imageType, float dpi) throws IOException + ImageType imageType, float dpi, float compressionQuality, + String compressionType) throws IOException { PDFRenderer renderer = new PDFRenderer(document); BufferedImage image = renderer.renderImageWithDPI(0, dpi, imageType); String fileName = outputPrefix + 1; LOG.info("Writing: " + fileName + "." + imageFormat); System.out.println(" " + fileName + "." + imageFormat); // for Maven (keep me!) - boolean res = ImageIOUtil.writeImage(image, fileName + "." + imageFormat, Math.round(dpi)); + OutputStream os = new FileOutputStream(fileName + "." + imageFormat); + boolean res = ImageIOUtil.writeImage(image, imageFormat, os, Math.round(dpi), compressionQuality, compressionType); + os.close(); assertTrue("ImageIOUtil.writeImage() failed for file " + fileName, res); - if ("jpg".equals(imageFormat) || "gif".equals(imageFormat)) + if ("jpg".equals(imageFormat) || "gif".equals(imageFormat) || "JPEG".equals(compressionType)) { // jpeg is lossy, gif has 256 colors, // so we can't check for content identity diff -Nru libpdfbox2-java-2.0.9/xmpbox/pom.xml libpdfbox2-java-2.0.13/xmpbox/pom.xml --- libpdfbox2-java-2.0.9/xmpbox/pom.xml 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/xmpbox/pom.xml 2018-11-28 17:18:40.000000000 +0000 @@ -27,10 +27,24 @@ org.apache.pdfbox pdfbox-parent - 2.0.9 + 2.0.13 ../parent/pom.xml + + + + [11,) + + + + javax.xml.bind + jaxb-api + provided + + + + diff -Nru libpdfbox2-java-2.0.9/xmpbox/src/main/java/org/apache/xmpbox/xml/DomXmpParser.java libpdfbox2-java-2.0.13/xmpbox/src/main/java/org/apache/xmpbox/xml/DomXmpParser.java --- libpdfbox2-java-2.0.9/xmpbox/src/main/java/org/apache/xmpbox/xml/DomXmpParser.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/xmpbox/src/main/java/org/apache/xmpbox/xml/DomXmpParser.java 2018-11-28 17:18:40.000000000 +0000 @@ -518,10 +518,28 @@ // no child String text = liElement.getTextContent(); TypeMapping tm = xmp.getTypeMapping(); - AbstractSimpleProperty sp = tm.instanciateSimpleProperty(descriptor.getNamespaceURI(), - descriptor.getPrefix(), descriptor.getLocalPart(), text, type); - loadAttributes(sp, liElement); - return sp; + if (type.isSimple()) + { + AbstractField af = tm.instanciateSimpleProperty(descriptor.getNamespaceURI(), + descriptor.getPrefix(), descriptor.getLocalPart(), text, type); + loadAttributes(af, liElement); + return af; + } + else + { + // PDFBOX-4325: assume it is structured + AbstractField af; + try + { + af = tm.instanciateStructuredType(type, descriptor.getLocalPart()); + } + catch (BadFieldValueException ex) + { + throw new XmpParsingException(ErrorType.InvalidType, "Parsing of structured type failed", ex); + } + loadAttributes(af, liElement); + return af; + } } } diff -Nru libpdfbox2-java-2.0.9/xmpbox/src/test/java/org/apache/xmpbox/DateConverterTest.java libpdfbox2-java-2.0.13/xmpbox/src/test/java/org/apache/xmpbox/DateConverterTest.java --- libpdfbox2-java-2.0.9/xmpbox/src/test/java/org/apache/xmpbox/DateConverterTest.java 2018-03-20 16:19:44.000000000 +0000 +++ libpdfbox2-java-2.0.13/xmpbox/src/test/java/org/apache/xmpbox/DateConverterTest.java 2018-11-28 17:18:40.000000000 +0000 @@ -46,14 +46,13 @@ @Test public void testDateConversion() throws Exception { - final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - Calendar jaxbCal = null, - convDate = null; + Calendar jaxbCal; + // Test partial dates - convDate = DateConverter.toCalendar("2015-02-02"); + Calendar convDate = DateConverter.toCalendar("2015-02-02"); assertEquals(2015, convDate.get(Calendar.YEAR)); - + //Test missing seconds assertEquals(DateConverter.toCalendar("2015-12-08T12:07:00-05:00"), DateConverter.toCalendar("2015-12-08T12:07-05:00"));