diff -Nru libsambox-java-1.1.19/debian/changelog libsambox-java-1.1.46/debian/changelog --- libsambox-java-1.1.19/debian/changelog 2017-11-26 21:30:13.000000000 +0000 +++ libsambox-java-1.1.46/debian/changelog 2019-02-27 12:47:40.000000000 +0000 @@ -1,3 +1,23 @@ +libsambox-java (1.1.46-1~18.04) bionic; urgency=medium + + * Backport for OpenJDK 11 (dependency of pdfsam) . LP: #1814133. + + -- Matthias Klose Wed, 27 Feb 2019 13:47:40 +0100 + +libsambox-java (1.1.46-1) unstable; urgency=medium + + * New upstream version 1.1.46. + + -- Markus Koschany Sat, 22 Dec 2018 12:22:05 +0100 + +libsambox-java (1.1.41-1) unstable; urgency=medium + + * New upstream version 1.1.41. + * Switch to compat level 11. + * Declare compliance with Debian Policy 4.2.1. + + -- Markus Koschany Fri, 12 Oct 2018 12:55:01 +0200 + libsambox-java (1.1.19-1) unstable; urgency=medium * New upstream version 1.1.19. diff -Nru libsambox-java-1.1.19/debian/compat libsambox-java-1.1.46/debian/compat --- libsambox-java-1.1.19/debian/compat 2017-11-26 21:30:13.000000000 +0000 +++ libsambox-java-1.1.46/debian/compat 2018-12-22 11:22:05.000000000 +0000 @@ -1 +1 @@ -10 +11 diff -Nru libsambox-java-1.1.19/debian/control libsambox-java-1.1.46/debian/control --- libsambox-java-1.1.19/debian/control 2017-11-26 21:30:13.000000000 +0000 +++ libsambox-java-1.1.46/debian/control 2018-12-22 11:22:05.000000000 +0000 @@ -5,7 +5,7 @@ Uploaders: Markus Koschany Build-Depends: - debhelper (>= 10), + debhelper (>= 11), default-jdk, junit4, libbcmail-java, @@ -15,7 +15,7 @@ libsejda-io-java (>= 1.1.3-2), libslf4j-java (>= 1.7.25), maven-debian-helper (>= 2.1) -Standards-Version: 4.1.1 +Standards-Version: 4.2.1 Vcs-Git: https://anonscm.debian.org/git/pkg-java/libsambox-java.git Vcs-Browser: https://anonscm.debian.org/git/pkg-java/libsambox-java.git Homepage: https://github.com/torakiki/sambox diff -Nru libsambox-java-1.1.19/debian/copyright libsambox-java-1.1.46/debian/copyright --- libsambox-java-1.1.19/debian/copyright 2017-11-26 21:30:13.000000000 +0000 +++ libsambox-java-1.1.46/debian/copyright 2018-12-22 11:22:05.000000000 +0000 @@ -5,7 +5,7 @@ src/test/* Files: * -Copyright: 2016-2017, Andrea Vacondio +Copyright: 2016-2018, Andrea Vacondio 2010-2017, The Apache Software Foundation License: Apache-2.0 @@ -40,7 +40,7 @@ License: zlib Files: debian/* -Copyright: 2017, Markus Koschany +Copyright: 2017-2018, Markus Koschany License: Apache-2.0 License: Apache-2.0 diff -Nru libsambox-java-1.1.19/debian/maven.ignoreRules libsambox-java-1.1.46/debian/maven.ignoreRules --- libsambox-java-1.1.19/debian/maven.ignoreRules 2017-11-26 21:30:13.000000000 +0000 +++ libsambox-java-1.1.46/debian/maven.ignoreRules 2018-12-22 11:22:05.000000000 +0000 @@ -7,3 +7,5 @@ org.apache.maven.plugins maven-surefire-plugin * * * * org.hamcrest hamcrest-core * * * * org.mockito mockito-core * * * * +com.googlecode.maven-download-plugin download-maven-plugin * * * * +org.apache.pdfbox jbig2-imageio * * * * diff -Nru libsambox-java-1.1.19/LICENSE libsambox-java-1.1.46/LICENSE --- libsambox-java-1.1.19/LICENSE 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/LICENSE 2018-12-03 16:18:13.000000000 +0000 @@ -1,3 +1,6 @@ +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -175,28 +178,126 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. +EXTERNAL COMPONENTS - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - 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. +SAMBox includes a number of components with separate copyright notices +and license terms. Your use of these components is subject to the terms and +conditions of the following licenses. + +Contributions made to the original PDFBox and FontBox projects: + + Copyright (c) 2002-2007, www.pdfbox.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of pdfbox; nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + 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. + +Adobe Font Metrics (AFM) for PDF Core 14 Fonts + + This file and the 14 PostScript(R) AFM files it accompanies may be used, + copied, and distributed for any purpose and without charge, with or without + modification, provided that all copyright notices are retained; that the + AFM files are not distributed without this file; that all modifications + to this file or any of the AFM files are prominently noted in the modified + file(s); and that this paragraph is not modified. Adobe Systems has no + responsibility or obligation to support the use of the AFM files. + +CMaps for PDF Fonts (http://opensource.adobe.com/wiki/display/cmap/Downloads) + + Copyright 1990-2009 Adobe Systems Incorporated. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT 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. + +OSXAdapter + + Version: 2.0 + + Disclaimer: IMPORTANT: This Apple software is supplied to you by + Apple Inc. ("Apple") in consideration of your agreement to the + following terms, and your use, installation, modification or + redistribution of this Apple software constitutes acceptance of these + terms. If you do not agree with these terms, please do not use, + install, modify or redistribute this Apple software. + + In consideration of your agreement to abide by the following terms, and + subject to these terms, Apple grants you a personal, non-exclusive + license, under Apple's copyrights in this original Apple software (the + "Apple Software"), to use, reproduce, modify and redistribute the Apple + Software, with or without modifications, in source and/or binary forms; + provided that if you redistribute the Apple Software in its entirety and + without modifications, you must retain this notice and the following + text and disclaimers in all such redistributions of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. + may be used to endorse or promote products derived from the Apple + Software without specific prior written permission from Apple. Except + as expressly stated in this notice, no other rights or licenses, express + or implied, are granted by Apple herein, including but not limited to + any patent rights that may be infringed by your derivative works or by + other works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE + MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION + THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND + OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, + MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED + AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), + STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +Copyright (C) 2003-2007 Apple, Inc., All Rights Reserved diff -Nru libsambox-java-1.1.19/pom.xml libsambox-java-1.1.46/pom.xml --- libsambox-java-1.1.19/pom.xml 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/pom.xml 2018-12-03 16:18:13.000000000 +0000 @@ -5,7 +5,7 @@ sambox jar sambox - 1.1.19 + 1.1.46 An Apache PDFBox fork intended to be used as PDF processor for Sejda and PDFsam related projects http://www.sejda.org @@ -33,7 +33,7 @@ scm:git:git@github.com:torakiki/sambox.git scm:git:git@github.com:torakiki/sambox.git scm:git:git@github.com:torakiki/sambox.git - v1.1.19 + v1.1.46 @@ -138,6 +138,47 @@ + + private-release + + + sejda-pro-snapshot + http://mvn.sejda.com/artifactory/libs-snapshot + + + sejda-pro + http://mvn.sejda.com/artifactory/libs-release + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.2 + + v@{project.version} + clean install + true + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + + @@ -173,6 +214,13 @@ org.apache.maven.plugins maven-jar-plugin 2.6 + + + + org.sejda.sambox + + + org.apache.maven.plugins @@ -192,7 +240,8 @@ maven-surefire-plugin 2.19.1 - -Xmx768m + -Xmx768m + -Dsun.java2d.cmm=sun.java2d.cmm.kcms.KcmsServiceProvider org/sejda/sambox/rendering/TestPDFToImage.java @@ -200,6 +249,188 @@ false + + + com.googlecode.maven-download-plugin + download-maven-plugin + 1.3.0 + + + PDFBOX-3703 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12854913/966635-p12.pdf + ${project.build.directory}/pdfs + PDFBOX-3703-966635-p12.pdf + 28fcb3be0bd3aa983a05107912b7c75ec8203b1ab14e7e76fa2b542d9d2dec9c96921d4220610dff96a299d935d9fffb3be2b552421b516a93344b14aed0ce0d + + + + PDFBOX-3747 + generate-test-resources + + wget + + + https://github.com/jondot/dotfiles/blob/master/.fonts/calibri.ttf?raw=true + ${project.build.directory}/fonts + PDFBOX-3747-calibri.ttf + b7eb8e6f2a4549eb68280d0d8834b2a14f711f2d15ffe1420fde654f05dd939181c617bf51e11c44aededaa729966b49288b0a07a35b79aa73a08b8c48b72de0 + + + + PDFBOX-3948 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12890034/EUWO6SQS5TM4VGOMRD3FLXZHU35V2CP2.pdf + ${project.build.directory}/pdfs + PDFBOX-3948-EUWO6SQS5TM4VGOMRD3FLXZHU35V2CP2.pdf + f8a9b0b9ea6132f24e54136a40ad99d67df2402f3849a5cb0b7d80cd72298737fe4701e0e77ddd602a06e3ea0a7e107ca40d8d29389eea5834ff37245829c2d2 + + + + PDFBOX-3949 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12890037/MKFYUGZWS3OPXLLVU2Z4LWCTVA5WNOGF.pdf + ${project.build.directory}/pdfs + PDFBOX-3949-MKFYUGZWS3OPXLLVU2Z4LWCTVA5WNOGF.pdf + f450fb40ed5589ce0f390eb110d78bc721b766c34b753770b0cb00b2e40ffe15878f54df2423ab99d7df80dd91512858bf56a7cdc392d5c179b4440176fdd2fb + + + + PDFBOX-3950 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12890042/23EGDHXSBBYQLKYOKGZUOVYVNE675PRD.pdf + ${project.build.directory}/pdfs + PDFBOX-3950-23EGDHXSBBYQLKYOKGZUOVYVNE675PRD.pdf + ee1d464c3ed2ad91a4cafbc474b38e5c961282f53ef599d6d10e02058da5a67064550ddc54774dfa843a8b45f34b7e6e8ab4f9a445ba459fdcd858e8dce65b25 + + + + PDFBOX-3951 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12890047/FIHUZWDDL2VGPOE34N6YHWSIGSH5LVGZ.pdf + ${project.build.directory}/pdfs + PDFBOX-3951-FIHUZWDDL2VGPOE34N6YHWSIGSH5LVGZ.pdf + 2c0b91beb4a2b098738512fefdd40135bf66286cd350ac4e155a5a0150d649acb1da819c817ee9822e8686f526af6b7862fc63a0dae6dc7f1407c7f8b271c65e + + + + PDFBOX-3964 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12892097/c687766d68ac766be3f02aaec5e0d713_2.pdf + ${project.build.directory}/pdfs + PDFBOX-3964-c687766d68ac766be3f02aaec5e0d713_2.pdf + 0457fd291a7f83f531fef205128929c8fa8147dd781ea7b7cd49d4d1287941989e72739329a7b172c6f53df0b54d991b514b9baa6145effa8ec7705ef273877b + + + + PDFBOX-4022 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12899008/selection.pdf + ${project.build.directory}/pdfs + PDFBOX-4022-selection.pdf + d08af71bc8e3911ee3ed7c9ce9d4acc0562488981bc83a9c612de9d5f0640fd2d9805f600810f1cad5293fa4acda12444a0dcefa2543125c95d06059feb2c4f0 + + + + PDFBOX-4106 + generate-test-resources + + wget + + + https://ipafont.ipa.go.jp/old/ipafont/ipag00303.php + ${project.build.directory}/fonts + ipag00303.zip + true + 59535137c649a2f8bdbb463cd716426811a6003a65883ca6e45bb0af1d526b3889af0fba3a353e90bc8d373cd32b90a27ff9ff6916ecbccb42e922c09e9b046a + + + + PDFBOX-4106b + generate-test-resources + + wget + + + https://ipafont.ipa.go.jp/old/ipafont/ipagp00303.php + ${project.build.directory}/fonts + ipagp00303.zip + true + 26d0a9bfba7f5457a98b0bf45a4a6b081bca4140047a0886625691231459f8c81a6cdbe523e9abcbd45fd7caed21d78f1baf3a2cf9167320f6b79be3d697cb5b + + + + PDFBOX-4115 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12911053/n019003l.pfb + ${project.build.directory}/fonts + n019003l.pfb + 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-2 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/12929821/16bit.png + + ${project.build.directory}/imgs + PDFBOX-4184-16bit.png + 45f148913590ea1a94c3ac17080969b74e579fe51967a5bf535caa3f7104ea81ee222b99deb8ee528b0a53640f97d87cf668633a1bdd61a62092246df1807471 + + + + @@ -217,17 +448,17 @@ org.sejda sejda-io - 1.1.3.RELEASE + 1.1.4 commons-io commons-io - 2.5 + 2.6 org.apache.pdfbox fontbox - 2.0.8 + 2.0.12 commons-logging @@ -239,13 +470,19 @@ org.bouncycastle bcmail-jdk15on true - 1.56 + 1.60 org.bouncycastle bcprov-jdk15on true - 1.56 + 1.60 + + + org.apache.pdfbox + jbig2-imageio + 3.0.2 + test ch.qos.logback diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/contentstream/PDFStreamEngine.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/contentstream/PDFStreamEngine.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/contentstream/PDFStreamEngine.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/contentstream/PDFStreamEngine.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,6 +16,7 @@ */ package org.sejda.sambox.contentstream; +import static java.util.Objects.isNull; import static java.util.Optional.ofNullable; import java.awt.geom.GeneralPath; @@ -105,6 +106,22 @@ } /** + * Adds an operator processor to the engine if there isn't an operator already associated with the PDF operator. + * + * @param op operator processor + * @return true if the operator is added, false if not (there's already an operator associated) + */ + public final boolean addOperatorIfAbsent(OperatorProcessor op) + { + if (isNull(operators.putIfAbsent(op.getName(), op))) + { + op.setContext(this); + return true; + } + return false; + } + + /** * Initialises the stream engine for the given page. */ private void initPage(PDPage page) @@ -163,7 +180,10 @@ throw new IllegalStateException("No current page, call " + "#processChildStream(PDContentStream, PDPage) instead"); } - processStream(form); + if (!form.getCOSObject().isEmpty()) + { + processStream(form); + } } /** @@ -431,7 +451,7 @@ * @param contentStream the content stream * @throws IOException if there is an exception while processing the stream */ - protected void processStream(PDContentStream contentStream) throws IOException + public void processStream(PDContentStream contentStream) throws IOException { PDResources parent = pushResources(contentStream); Stack savedStack = saveGraphicsStack(); @@ -879,7 +899,7 @@ protected final Stack saveGraphicsStack() { Stack savedStack = graphicsStack; - graphicsStack = new Stack(); + graphicsStack = new Stack<>(); graphicsStack.add(savedStack.peek().clone()); return savedStack; } @@ -956,7 +976,7 @@ } /** - * Returns the stream' resources. + * @return the stream' resources. This is mainly to be used by the {@link OperatorProcessor} classes */ public PDResources getResources() { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/cos/COSArray.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/cos/COSArray.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/cos/COSArray.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/cos/COSArray.java 2018-12-03 16:18:13.000000000 +0000 @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.ListIterator; @@ -455,7 +454,8 @@ float[] retval = new float[size()]; for (int i = 0; i < size(); i++) { - retval[i] = ((COSNumber) getObject(i)).floatValue(); + retval[i] = ofNullable(getObject(i, COSNumber.class)).map(COSNumber::floatValue) + .orElse(0f); } return retval; } @@ -480,7 +480,7 @@ public List toList() { ArrayList retList = new ArrayList<>(size()); - Collections.copy(retList, objects); + retList.addAll(objects); return retList; } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/cos/COSDictionary.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/cos/COSDictionary.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/cos/COSDictionary.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/cos/COSDictionary.java 2018-12-03 16:18:13.000000000 +0000 @@ -134,6 +134,26 @@ } /** + * + * @param firstKey + * @param secondKey + * @param clazz + * @return + * @see #getDictionaryObject(COSName, COSName) + */ + public T getDictionaryObject(COSName firstKey, COSName secondKey, + Class clazz) + { + return ofNullable(getDictionaryObject(firstKey, clazz)).orElseGet(() -> { + if (nonNull(secondKey)) + { + return getDictionaryObject(secondKey, clazz); + } + return null; + }); + } + + /** * Get an object from this dictionary. If the object is a reference then it will dereference it. If the object is * COSNull then null will be returned. * @@ -592,7 +612,6 @@ /** * Convenience method that will get the dictionary object that is expected to be a name and convert it to a string. - * Null is returned if the entry does not exist in the dictionary. * * @param key The key to the item in the dictionary. * @param defaultValue The value to return if the dictionary item is null. @@ -605,7 +624,6 @@ /** * Convenience method that will get the dictionary object that is expected to be a name and convert it to a string. - * Null is returned if the entry does not exist in the dictionary. * * @param key The key to the item in the dictionary. * @param defaultValue The value to return if the dictionary item is null. @@ -643,7 +661,6 @@ /** * Convenience method that will get the dictionary object that is expected to be a name and convert it to a string. - * Null is returned if the entry does not exist in the dictionary. * * @param key The key to the item in the dictionary. * @param defaultValue The default value to return. @@ -656,7 +673,6 @@ /** * Convenience method that will get the dictionary object that is expected to be a name and convert it to a string. - * Null is returned if the entry does not exist in the dictionary. * * @param key The key to the item in the dictionary. * @param defaultValue The default value to return. @@ -709,7 +725,6 @@ /** * Convenience method that will get the dictionary object that is expected to be a name and convert it to a string. - * Null is returned if the entry does not exist in the dictionary. * * @param embedded The embedded dictionary. * @param key The key to the item in the dictionary. @@ -1168,6 +1183,26 @@ } /** + * This is a special case of getItem that takes multiple keys, it will handle the situation where multiple keys + * could get the same value, ie if either CS or ColorSpace is used to get the colorspace. This will get an object + * from this dictionary. + * + * @param firstKey The first key to try. + * @param secondKey The second key to try. + * + * @return The object that matches the key. + */ + public COSBase getItem(COSName firstKey, COSName secondKey) + { + COSBase retval = getItem(firstKey); + if (retval == null && secondKey != null) + { + retval = getItem(secondKey); + } + return retval; + } + + /** * @return names of the entries in this dictionary. The returned set is in the order the entries were added to the * dictionary. */ diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/cos/COSDocument.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/cos/COSDocument.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/cos/COSDocument.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/cos/COSDocument.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,6 +16,7 @@ */ package org.sejda.sambox.cos; +import static java.util.Objects.nonNull; import static java.util.Optional.ofNullable; import static org.sejda.util.RequireUtils.requireNotBlank; import static org.sejda.util.RequireUtils.requireNotNullArg; @@ -83,7 +84,8 @@ */ public boolean isEncrypted() { - return trailer.getCOSObject().getDictionaryObject(COSName.ENCRYPT) != null; + return nonNull( + trailer.getCOSObject().getDictionaryObject(COSName.ENCRYPT, COSDictionary.class)); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/cos/COSName.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/cos/COSName.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/cos/COSName.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/cos/COSName.java 2018-12-03 16:18:13.000000000 +0000 @@ -34,6 +34,7 @@ // A public static final COSName A = newCommonInstance("A"); public static final COSName AA = newCommonInstance("AA"); + public static final COSName AC = newCommonInstance("AC"); public static final COSName ACRO_FORM = newCommonInstance("AcroForm"); public static final COSName ACTUAL_TEXT = newCommonInstance("ActualText"); public static final COSName ADBE_PKCS7_DETACHED = newCommonInstance("adbe.pkcs7.detached"); @@ -124,6 +125,7 @@ public static final COSName CMAPNAME = newCommonInstance("CMapName"); public static final COSName CMYK = newCommonInstance("CMYK"); public static final COSName CO = newCommonInstance("CO"); + public static final COSName COLOR = new COSName("Color"); public static final COSName COLOR_BURN = newCommonInstance("ColorBurn"); public static final COSName COLOR_DODGE = newCommonInstance("ColorDodge"); public static final COSName COLORANTS = newCommonInstance("Colorants"); @@ -184,6 +186,7 @@ public static final COSName DOC_OPEN = newCommonInstance("DocOpen"); public static final COSName DOC_TIME_STAMP = newCommonInstance("DocTimeStamp"); public static final COSName DOCMDP = newCommonInstance("DocMDP"); + public static final COSName DOCUMENT = new COSName("Document"); public static final COSName DOMAIN = newCommonInstance("Domain"); public static final COSName DOS = newCommonInstance("DOS"); public static final COSName DP = newCommonInstance("DP"); @@ -264,6 +267,7 @@ public static final COSName HIDE_MENUBAR = newCommonInstance("HideMenubar"); public static final COSName HIDE_TOOLBAR = newCommonInstance("HideToolbar"); public static final COSName HIDE_WINDOWUI = newCommonInstance("HideWindowUI"); + public static final COSName HUE = new COSName("Hue"); // I public static final COSName I = newCommonInstance("I"); public static final COSName IC = newCommonInstance("IC"); @@ -284,6 +288,9 @@ public static final COSName INTERPOLATE = newCommonInstance("Interpolate"); public static final COSName IT = newCommonInstance("IT"); public static final COSName ITALIC_ANGLE = newCommonInstance("ItalicAngle"); + public static final COSName ISSUER = newCommonInstance("Issuer"); + public static final COSName IX = newCommonInstance("IX"); + // J public static final COSName JAVA_SCRIPT = newCommonInstance("JavaScript"); public static final COSName JBIG2_DECODE = newCommonInstance("JBIG2Decode"); @@ -334,6 +341,7 @@ public static final COSName MCID = newCommonInstance("MCID"); public static final COSName MDP = newCommonInstance("MDP"); public static final COSName MEDIA_BOX = newCommonInstance("MediaBox"); + public static final COSName MEASURE = new COSName("Measure"); public static final COSName METADATA = newCommonInstance("Metadata"); public static final COSName MISSING_WIDTH = newCommonInstance("MissingWidth"); public static final COSName MK = newCommonInstance("MK"); @@ -346,6 +354,7 @@ public static final COSName NAME = newCommonInstance("Name"); public static final COSName NAMES = newCommonInstance("Names"); public static final COSName NEED_APPEARANCES = newCommonInstance("NeedAppearances"); + public static final COSName NEW_WINDOW = new COSName("NewWindow"); public static final COSName NEXT = newCommonInstance("Next"); public static final COSName NM = newCommonInstance("NM"); public static final COSName NON_EFONT_NO_WARN = newCommonInstance("NonEFontNoWarn"); @@ -444,6 +453,7 @@ // S public static final COSName S = newCommonInstance("S"); public static final COSName SA = newCommonInstance("SA"); + public static final COSName SATURATION = new COSName("Saturation"); public static final COSName SCREEN = newCommonInstance("Screen"); public static final COSName SE = newCommonInstance("SE"); public static final COSName SEPARATION = newCommonInstance("Separation"); @@ -527,6 +537,8 @@ public static final COSName VIEW_AREA = newCommonInstance("ViewArea"); public static final COSName VIEW_CLIP = newCommonInstance("ViewClip"); public static final COSName VIEWER_PREFERENCES = newCommonInstance("ViewerPreferences"); + public static final COSName VOLUME = new COSName("Volume"); + public static final COSName VP = new COSName("VP"); // W public static final COSName W = newCommonInstance("W"); public static final COSName W2 = newCommonInstance("W2"); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/cos/COSStream.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/cos/COSStream.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/cos/COSStream.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/cos/COSStream.java 2018-12-03 16:18:13.000000000 +0000 @@ -401,7 +401,7 @@ /** * Sets the function to be used to encrypt this stream. * - * @param encrypted + * @param encryptor */ public void setEncryptor(Function encryptor) { @@ -471,28 +471,39 @@ /** * Adds Flate decode filter to the current filters list if possible + * + * @true if the FlateDecode filter has been added */ - public void addCompression() throws IOException + public boolean addCompression() { if (canCompress()) { - COSArray newFilters = new COSArray(COSName.FLATE_DECODE); - COSBase filters = getFilters(); - if (filters instanceof COSName) - { - newFilters.add(filters); - setFilters(newFilters); - } - else if (filters instanceof COSArray) + try { - newFilters.addAll((COSArray) filters); - setFilters(newFilters); + COSArray newFilters = new COSArray(COSName.FLATE_DECODE); + COSBase filters = getFilters(); + if (filters instanceof COSName) + { + newFilters.add(filters); + setFilters(newFilters); + } + else if (filters instanceof COSArray) + { + newFilters.addAll((COSArray) filters); + setFilters(newFilters); + } + else + { + setFilters(COSName.FLATE_DECODE); + } + return true; } - else + catch (IOException e) { - setFilters(COSName.FLATE_DECODE); + LOG.warn("Unable to add FlateDecode filter to the stream", e); } } + return false; } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/ASCIIHexFilter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/ASCIIHexFilter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/ASCIIHexFilter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/ASCIIHexFilter.java 2018-12-03 16:18:13.000000000 +0000 @@ -49,47 +49,57 @@ }; @Override - public DecodeResult decode(InputStream encoded, OutputStream decoded, - COSDictionary parameters, int index) throws IOException + public DecodeResult decode(InputStream encoded, OutputStream decoded, COSDictionary parameters, + int index) throws IOException { + // TODO iText and pdfjs both have similar impl which is different from what we have. Maybe we can replace this + // with the algorithm in pdfjs int value, firstByte, secondByte; - while ((firstByte = encoded.read()) != -1) + try { - // always after first char - while (isWhitespace(firstByte)) + while ((firstByte = encoded.read()) != -1) { - firstByte = encoded.read(); - } - if (firstByte == -1 || isEOD(firstByte)) - { - break; - } - - if (REVERSE_HEX[firstByte] == -1) - { - LOG.error("Invalid hex, int: " + firstByte + " char: " + (char)firstByte); - } - value = REVERSE_HEX[firstByte] * 16; - secondByte = encoded.read(); - - if (secondByte == -1 || isEOD(secondByte)) - { - // second value behaves like 0 in case of EOD - decoded.write(value); - break; - } - if (secondByte >= 0) - { - if (REVERSE_HEX[secondByte] == -1) + // always after first char + while (isWhitespace(firstByte)) + { + firstByte = encoded.read(); + } + if (firstByte == -1 || isEOD(firstByte)) { - LOG.error("Invalid hex, int: " + secondByte + " char: " + (char)secondByte); + break; } - value += REVERSE_HEX[secondByte]; + + if (REVERSE_HEX[firstByte] == -1) + { + LOG.error("Invalid hex, int: " + firstByte + " char: " + (char) firstByte); + } + value = REVERSE_HEX[firstByte] * 16; + secondByte = encoded.read(); + + if (secondByte == -1 || isEOD(secondByte)) + { + // second value behaves like 0 in case of EOD + decoded.write(value); + break; + } + if (secondByte >= 0) + { + if (REVERSE_HEX[secondByte] == -1) + { + LOG.error( + "Invalid hex, int: " + secondByte + " char: " + (char) secondByte); + } + value += REVERSE_HEX[secondByte]; + } + decoded.write(value); } - decoded.write(value); + decoded.flush(); + return new DecodeResult(parameters); + } + catch (ArrayIndexOutOfBoundsException e) + { + throw new IOException("Illegal character in ASCIIHexFilter", e); } - decoded.flush(); - return new DecodeResult(parameters); } // whitespace diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/CCITTFaxDecoderStream.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/CCITTFaxDecoderStream.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/CCITTFaxDecoderStream.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/CCITTFaxDecoderStream.java 2018-12-03 16:18:13.000000000 +0000 @@ -279,7 +279,7 @@ { if (optionByteAligned) { - bufferPos = -1; // Skip remaining bits and fetch the next byte at row start + resetBuffer(); } eof: while (true) { @@ -316,7 +316,7 @@ { if (optionByteAligned) { - bufferPos = -1; // Skip remaining bits and fetch the next byte at row start + resetBuffer(); } decode2D(); } @@ -421,30 +421,14 @@ { return total; } - else - { - n = tree.root; - } + n = tree.root; } } } - private void resetBuffer() throws IOException + private void resetBuffer() { - for (int i = 0; i < decodedRow.length; i++) - { - decodedRow[i] = 0; - } - - while (true) - { - if (bufferPos == -1) - { - return; - } - - readBit(); - } + bufferPos = -1; } int buffer = -1; @@ -688,9 +672,8 @@ } } - static final short[][] BLACK_CODES = { - { // 2 bits - 0x2, 0x3, }, + static final short[][] BLACK_CODES = { { // 2 bits + 0x2, 0x3, }, { // 3 bits 0x2, 0x3, }, { // 4 bits @@ -718,9 +701,8 @@ { // 13 bits 0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, } }; - static final short[][] BLACK_RUN_LENGTHS = { - { // 2 bits - 3, 2, }, + static final short[][] BLACK_RUN_LENGTHS = { { // 2 bits + 3, 2, }, { // 3 bits 1, 4, }, { // 4 bits @@ -748,9 +730,8 @@ 640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960, 1024, 1088, 1152, 1216, } }; - public static final short[][] WHITE_CODES = { - { // 4 bits - 0x7, 0x8, 0xb, 0xc, 0xe, 0xf, }, + public static final short[][] WHITE_CODES = { { // 4 bits + 0x7, 0x8, 0xb, 0xc, 0xe, 0xf, }, { // 5 bits 0x12, 0x13, 0x14, 0x1b, 0x7, 0x8, }, { // 6 bits @@ -771,9 +752,8 @@ { // 12 bits 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, } }; - public static final short[][] WHITE_RUN_LENGTHS = { - { // 4 bits - 2, 3, 4, 5, 6, 7, }, + public static final short[][] WHITE_RUN_LENGTHS = { { // 4 bits + 2, 3, 4, 5, 6, 7, }, { // 5 bits 128, 8, 9, 64, 10, 11, }, { // 6 bits diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/CCITTFaxFilter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/CCITTFaxFilter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/CCITTFaxFilter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/CCITTFaxFilter.java 2018-12-03 16:18:13.000000000 +0000 @@ -38,9 +38,6 @@ public DecodeResult decode(InputStream encoded, OutputStream decoded, COSDictionary parameters, int index) throws IOException { - DecodeResult result = new DecodeResult(new COSDictionary()); - result.getParameters().addAll(parameters); - // get decode parameters COSDictionary decodeParms = getDecodeParams(parameters, index); @@ -103,12 +100,6 @@ invertBitmap(decompressed); } - // repair missing color space - if (!parameters.containsKey(COSName.COLORSPACE)) - { - result.getParameters().setItem(COSName.COLORSPACE, COSName.DEVICEGRAY); - } - decoded.write(decompressed); return new DecodeResult(parameters); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/DCTFilter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/DCTFilter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/DCTFilter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/DCTFilter.java 2018-12-03 16:18:13.000000000 +0000 @@ -134,8 +134,7 @@ // already CMYK break; case 1: - // TODO YCbCr - LOG.warn("YCbCr JPEGs not implemented"); + raster = fromYCbCrtoCMYK(raster); break; case 2: raster = fromYCCKtoCMYK(raster); @@ -265,6 +264,44 @@ writableRaster.setPixel(x, y, value); } } + return writableRaster; + } + + private WritableRaster fromYCbCrtoCMYK(Raster raster) + { + WritableRaster writableRaster = raster.createCompatibleWritableRaster(); + + int[] value = new int[4]; + for (int y = 0, height = raster.getHeight(); y < height; y++) + { + for (int x = 0, width = raster.getWidth(); x < width; x++) + { + raster.getPixel(x, y, value); + + // 4-channels 0..255 + float Y = value[0]; + float Cb = value[1]; + float Cr = value[2]; + float K = value[3]; + + // YCbCr to RGB, see http://www.equasys.de/colorconversion.html + int r = clamp( (1.164f * (Y-16)) + (1.596f * (Cr - 128)) ); + int g = clamp( (1.164f * (Y-16)) + (-0.392f * (Cb-128)) + (-0.813f * (Cr-128))); + int b = clamp( (1.164f * (Y-16)) + (2.017f * (Cb-128))); + + // naive RGB to CMYK + int cyan = 255 - r; + int magenta = 255 - g; + int yellow = 255 - b; + + // update new raster + value[0] = cyan; + value[1] = magenta; + value[2] = yellow; + value[3] = (int)K; + writableRaster.setPixel(x, y, value); + } + } return writableRaster; } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/Filter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/Filter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/Filter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/Filter.java 2018-12-03 16:18:13.000000000 +0000 @@ -49,7 +49,7 @@ * used to compress /Flate streams. The default value is -1 which is {@link Deflater#DEFAULT_COMPRESSION}. To set * maximum compression, use {@code System.setProperty(Filter.SYSPROP_DEFLATELEVEL, "9");} */ - public static final String SYSPROP_DEFLATELEVEL = "org.apache.pdfbox.filter.deflatelevel"; + public static final String SYSPROP_DEFLATELEVEL = "org.sejda.sambox.filter.deflatelevel"; protected Filter() { @@ -112,7 +112,7 @@ return new COSDictionary(); } } - if (!(dp instanceof COSArray || dp instanceof COSArray)) + if (!(filter instanceof COSArray || dp instanceof COSArray)) { LOG.error("Ignoring invalid DecodeParams. Expected array or dictionary but found {}", dp.getClass().getName()); @@ -149,4 +149,21 @@ 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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/FlateFilter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/FlateFilter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/FlateFilter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/FlateFilter.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,7 +16,6 @@ */ package org.sejda.sambox.filter; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -25,9 +24,7 @@ import java.util.zip.DeflaterOutputStream; import java.util.zip.Inflater; -import org.sejda.io.FastByteArrayOutputStream; import org.sejda.sambox.cos.COSDictionary; -import org.sejda.sambox.cos.COSName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,39 +37,17 @@ final class FlateFilter extends Filter { private static final Logger LOG = LoggerFactory.getLogger(FlateFilter.class); - private static final int BUFFER_SIZE = 16348; + private static final int BUFFER_SIZE = 0x4000; @Override 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); - FastByteArrayOutputStream baos = new FastByteArrayOutputStream(); - 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) { @@ -142,30 +117,21 @@ public 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; - int mayRead = input.available(); - if (mayRead > 0) + try (DeflaterOutputStream out = new DeflaterOutputStream(encoded, deflater)) { - byte[] buffer = new byte[Math.min(mayRead, BUFFER_SIZE)]; - while ((amountRead = input.read(buffer, 0, Math.min(mayRead, BUFFER_SIZE))) != -1) + int amountRead; + int mayRead = input.available(); + if (mayRead > 0) { - out.write(buffer, 0, amountRead); + byte[] buffer = new byte[Math.min(mayRead, BUFFER_SIZE)]; + while ((amountRead = input.read(buffer, 0, Math.min(mayRead, BUFFER_SIZE))) != -1) + { + out.write(buffer, 0, amountRead); + } } } - out.close(); encoded.flush(); } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/IdentityFilter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/IdentityFilter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/IdentityFilter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/IdentityFilter.java 2018-12-03 16:18:13.000000000 +0000 @@ -20,6 +20,7 @@ import java.io.InputStream; import java.io.OutputStream; +import org.apache.commons.io.IOUtils; import org.sejda.sambox.cos.COSDictionary; /** @@ -30,19 +31,12 @@ */ final class IdentityFilter extends Filter { - private static final int BUFFER_SIZE = 1024; - @Override public DecodeResult decode(InputStream encoded, OutputStream decoded, COSDictionary parameters, int index) throws IOException { - byte[] buffer = new byte[BUFFER_SIZE]; - int amountRead; - while((amountRead = encoded.read(buffer, 0, BUFFER_SIZE)) != -1) - { - decoded.write(buffer, 0, amountRead); - } + IOUtils.copy(encoded, decoded); decoded.flush(); return new DecodeResult(parameters); } @@ -51,12 +45,7 @@ public void encode(InputStream input, OutputStream encoded, COSDictionary parameters) throws IOException { - byte[] buffer = new byte[BUFFER_SIZE]; - int amountRead; - while((amountRead = input.read(buffer, 0, BUFFER_SIZE)) != -1) - { - encoded.write(buffer, 0, amountRead); - } + IOUtils.copy(input, encoded); encoded.flush(); } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/JBIG2Filter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/JBIG2Filter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/JBIG2Filter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/JBIG2Filter.java 2018-12-03 16:18:13.000000000 +0000 @@ -40,8 +40,7 @@ * monochrome (1 bit per pixel) image data (or an approximation of that data). * * Requires a JBIG2 plugin for Java Image I/O to be installed. A known working - * plug-in is jbig2-imageio - * which is available under the GPL v3 license. + * plug-in is the Apache PDFBox JBIG2 plugin. * * @author Timo Boehme */ @@ -49,11 +48,29 @@ { private static final Logger LOG = LoggerFactory.getLogger(JBIG2Filter.class); + private static boolean levigoLogged = false; + + private static synchronized void logLevigoDonated() + { + if (!levigoLogged) + { + LOG.info("The Levigo JBIG2 plugin has been donated to the Apache Foundation"); + LOG.info("and an improved version is available for download at " + + "https://pdfbox.apache.org/download.cgi"); + levigoLogged = true; + } + } + @Override - public DecodeResult decode(InputStream encoded, OutputStream decoded, - COSDictionary parameters, int index) throws IOException + public DecodeResult decode(InputStream encoded, OutputStream decoded, COSDictionary + parameters, int index) throws IOException { ImageReader reader = findImageReader("JBIG2", "jbig2-imageio is not installed"); + if (reader.getClass().getName().contains("levigo")) + { + logLevigoDonated(); + } + DecodeResult result = new DecodeResult(new COSDictionary()); result.getParameters().addAll(parameters); @@ -127,12 +144,6 @@ reader.dispose(); } - // repair missing color space - if (!parameters.containsKey(COSName.COLORSPACE)) - { - result.getParameters().setName(COSName.COLORSPACE, COSName.DEVICEGRAY.getName()); - } - return new DecodeResult(parameters); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/JPXFilter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/JPXFilter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/JPXFilter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/JPXFilter.java 2018-12-03 16:18:13.000000000 +0000 @@ -20,14 +20,14 @@ import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferUShort; -import java.awt.image.WritableRaster; +import java.awt.image.Raster; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; import org.sejda.sambox.cos.COSDictionary; import org.sejda.sambox.cos.COSName; @@ -55,7 +55,7 @@ result.getParameters().addAll(parameters); BufferedImage image = readJPX(encoded, result); - WritableRaster raster = image.getRaster(); + Raster raster = image.getRaster(); switch (raster.getDataBuffer().getDataType()) { case DataBuffer.TYPE_BYTE: @@ -86,7 +86,9 @@ ImageInputStream iis = null; try { - iis = ImageIO.createImageInputStream(input); + // PDFBOX-4121: ImageIO.createImageInputStream() is much slower + iis = new MemoryCacheImageInputStream(input); + reader.setInput(iis, true, true); BufferedImage image; @@ -116,8 +118,8 @@ } // override dimensions, see PDFBOX-1735 - parameters.setInt(COSName.WIDTH, image.getWidth()); - parameters.setInt(COSName.HEIGHT, image.getHeight()); + parameters.setInt(COSName.WIDTH, reader.getWidth(0)); + parameters.setInt(COSName.HEIGHT, reader.getHeight(0)); // extract embedded color space if (!parameters.containsKey(COSName.COLORSPACE)) diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/LZWFilter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/LZWFilter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/LZWFilter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/LZWFilter.java 2018-12-03 16:18:13.000000000 +0000 @@ -15,8 +15,6 @@ */ package org.sejda.sambox.filter; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -53,60 +51,39 @@ * The LZW end of data code. */ public static final long EOD = 257; - - //BEWARE: codeTable must be local to each method, because there is only + + // BEWARE: codeTable must be local to each method, because there is only // one instance of each filter /** * {@inheritDoc} */ @Override - public DecodeResult decode(InputStream encoded, OutputStream decoded, - COSDictionary parameters, int index) throws IOException + 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); } + + doLZWDecode(encoded, Predictor.wrapPredictor(decoded, decodeParams), earlyChange); return new DecodeResult(parameters); } - private void doLZWDecode(InputStream encoded, OutputStream decoded, int earlyChange) throws IOException + private void doLZWDecode(InputStream encoded, OutputStream decoded, int earlyChange) + throws IOException { - List codeTable = new ArrayList(); + List codeTable = new ArrayList<>(); int chunk = 9; - final MemoryCacheImageInputStream in = new MemoryCacheImageInputStream(encoded); long nextCommand; long prevCommand = -1; - try + try (MemoryCacheImageInputStream in = new MemoryCacheImageInputStream(encoded)) { while ((nextCommand = in.readBits(chunk)) != EOD) { @@ -141,7 +118,7 @@ decoded.write(newData); codeTable.add(newData); } - + chunk = calculateChunk(codeTable.size(), earlyChange); prevCommand = nextCommand; } @@ -154,8 +131,8 @@ decoded.flush(); } - private void checkIndexBounds(List codeTable, long index, MemoryCacheImageInputStream in) - throws IOException + private static void checkIndexBounds(List codeTable, long index, + MemoryCacheImageInputStream in) throws IOException { if (index < 0) { @@ -177,68 +154,69 @@ int chunk = 9; byte[] inputPattern = null; - final MemoryCacheImageOutputStream out = new MemoryCacheImageOutputStream(encoded); - out.writeBits(CLEAR_TABLE, chunk); - int foundCode = -1; - int r; - while ((r = rawData.read()) != -1) + try (MemoryCacheImageOutputStream out = new MemoryCacheImageOutputStream(encoded)) { - byte by = (byte) r; - if (inputPattern == null) - { - inputPattern = new byte[] { by }; - foundCode = by & 0xff; - } - else + out.writeBits(CLEAR_TABLE, chunk); + int foundCode = -1; + int r; + while ((r = rawData.read()) != -1) { - inputPattern = Arrays.copyOf(inputPattern, inputPattern.length + 1); - inputPattern[inputPattern.length - 1] = by; - int newFoundCode = findPatternCode(codeTable, inputPattern); - if (newFoundCode == -1) + byte by = (byte) r; + if (inputPattern == null) { - // use previous - chunk = calculateChunk(codeTable.size() - 1, 1); - out.writeBits(foundCode, chunk); - // create new table entry - codeTable.add(inputPattern); - - if (codeTable.size() == 4096) - { - // code table is full - out.writeBits(CLEAR_TABLE, chunk); - codeTable = createCodeTable(); - } - inputPattern = new byte[] { by }; foundCode = by & 0xff; } else { - foundCode = newFoundCode; + inputPattern = Arrays.copyOf(inputPattern, inputPattern.length + 1); + inputPattern[inputPattern.length - 1] = by; + int newFoundCode = findPatternCode(codeTable, inputPattern); + if (newFoundCode == -1) + { + // use previous + chunk = calculateChunk(codeTable.size() - 1, 1); + out.writeBits(foundCode, chunk); + // create new table entry + codeTable.add(inputPattern); + + if (codeTable.size() == 4096) + { + // code table is full + out.writeBits(CLEAR_TABLE, chunk); + codeTable = createCodeTable(); + } + + inputPattern = new byte[] { by }; + foundCode = by & 0xff; + } + else + { + foundCode = newFoundCode; + } } } - } - if (foundCode != -1) - { - chunk = calculateChunk(codeTable.size() - 1, 1); - out.writeBits(foundCode, chunk); - } + if (foundCode != -1) + { + chunk = calculateChunk(codeTable.size() - 1, 1); + out.writeBits(foundCode, chunk); + } - // PPDFBOX-1977: the decoder wouldn't know that the encoder would output - // an EOD as code, so he would have increased his own code table and - // possibly adjusted the chunk. Therefore, the encoder must behave as - // if the code table had just grown and thus it must be checked it is - // needed to adjust the chunk, based on an increased table size parameter - chunk = calculateChunk(codeTable.size(), 1); - - out.writeBits(EOD, chunk); - - // pad with 0 - out.writeBits(0, 7); - - // must do or file will be empty :-( - out.flush(); - out.close(); + // PPDFBOX-1977: the decoder wouldn't know that the encoder would output + // an EOD as code, so he would have increased his own code table and + // possibly adjusted the chunk. Therefore, the encoder must behave as + // if the code table had just grown and thus it must be checked it is + // needed to adjust the chunk, based on an increased table size parameter + chunk = calculateChunk(codeTable.size(), 1); + + out.writeBits(EOD, chunk); + + // pad with 0 + out.writeBits(0, 7); + + // must do or file will be empty :-( + out.flush(); + } } /** @@ -246,8 +224,7 @@ * * @param codeTable The LZW code table. * @param pattern The pattern to be searched for. - * @return The index of the longest matching pattern or -1 if nothing is - * found. + * @return The index of the longest matching pattern or -1 if nothing is found. */ private int findPatternCode(List codeTable, byte[] pattern) { @@ -261,7 +238,7 @@ if (foundCode != -1) { // we already found pattern with size > 1 - return foundCode; + return foundCode; } else if (pattern.length > 1) { @@ -270,7 +247,8 @@ } } byte[] tryPattern = codeTable.get(i); - if ((foundCode != -1 || tryPattern.length > foundLen) && Arrays.equals(tryPattern, pattern)) + if ((foundCode != -1 || tryPattern.length > foundLen) + && Arrays.equals(tryPattern, pattern)) { foundCode = i; foundLen = tryPattern.length; @@ -280,12 +258,11 @@ } /** - * Init the code table with 1 byte entries and the EOD and CLEAR_TABLE - * markers. + * Init the code table with 1 byte entries and the EOD and CLEAR_TABLE markers. */ private List createCodeTable() { - List codeTable = new ArrayList(4096); + List codeTable = new ArrayList<>(4096); for (int i = 0; i < 256; ++i) { codeTable.add(new byte[] { (byte) (i & 0xFF) }); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/Predictor.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/Predictor.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/Predictor.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/Predictor.java 2018-12-03 16:18:13.000000000 +0000 @@ -15,15 +15,19 @@ */ package org.sejda.sambox.filter; +import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Arrays; import org.apache.commons.io.IOUtils; +import org.sejda.sambox.cos.COSDictionary; +import org.sejda.sambox.cos.COSName; /** - * Helper class to contain predictor decoding used by Flate and LZW filter. - * To see the history, look at the FlateFilter class. + * Helper class to contain predictor decoding used by Flate and LZW filter. To see the history, look at the FlateFilter + * class. */ public final class Predictor { @@ -31,9 +35,178 @@ private Predictor() { } - - static void decodePredictor(int predictor, int colors, int bitsPerComponent, int columns, InputStream in, OutputStream out) - throws IOException + + /** + * 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 { if (predictor == 1) { @@ -43,9 +216,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]; @@ -69,162 +240,26 @@ // read line int i, offset = 0; - while (offset < rowlength && ((i = in.read(actline, offset, rowlength - offset)) != -1)) + while (offset < rowlength + && ((i = in.read(actline, offset, rowlength - offset)) != -1)) { 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) { @@ -241,4 +276,141 @@ 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); + } + 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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/RunLengthDecodeFilter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/RunLengthDecodeFilter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/filter/RunLengthDecodeFilter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/filter/RunLengthDecodeFilter.java 2018-12-03 16:18:13.000000000 +0000 @@ -47,9 +47,14 @@ { int amountToCopy = dupAmount + 1; int compressedRead; - while(amountToCopy > 0) + while (amountToCopy > 0) { compressedRead = encoded.read(buffer, 0, amountToCopy); + // EOF reached? + if (compressedRead == -1) + { + break; + } decoded.write(buffer, 0, compressedRead); amountToCopy -= compressedRead; } @@ -57,6 +62,11 @@ else { int dupByte = encoded.read(); + // EOF reached? + if (dupByte == -1) + { + break; + } for (int i = 0; i < 257 - dupAmount; i++) { decoded.write(dupByte); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/input/AbstractXrefTableParser.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/input/AbstractXrefTableParser.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/input/AbstractXrefTableParser.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/input/AbstractXrefTableParser.java 2018-12-03 16:18:13.000000000 +0000 @@ -120,10 +120,12 @@ onEntryFound(inUseEntry(currentObjectNumber, Long.parseLong(splitString[0]), Integer.parseInt(splitString[1]))); } - catch (NumberFormatException e) + catch (IllegalArgumentException e) { - throw new IOException("Corrupted xref table entry.", e); + throw new IOException( + "Corrupted xref table entry. Invalid xref line: " + currentLine, e); } + } else if (!"f".equals(entryType)) { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/input/LazyIndirectObjectsProvider.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/input/LazyIndirectObjectsProvider.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/input/LazyIndirectObjectsProvider.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/input/LazyIndirectObjectsProvider.java 2018-12-03 16:18:13.000000000 +0000 @@ -207,6 +207,20 @@ { LOG.warn("Missing 'endobj' token for {}", xrefEntry); } + + if(found instanceof ExistingIndirectCOSObject) + { + ExistingIndirectCOSObject existingIndirectCOSObject = (ExistingIndirectCOSObject)found; + // does this point to itself? it would cause a StackOverflowError. Example: + // 9 0 obj + // 9 0 R + // endobj + if(existingIndirectCOSObject.id().objectIdentifier.equals(xrefEntry.key())) + { + LOG.warn("Found indirect object definition pointing to itself, for {}", xrefEntry); + found = COSNull.NULL; + } + } store.put(xrefEntry.key(), ofNullable(found).orElse(COSNull.NULL)); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/input/SourceReader.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/input/SourceReader.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/input/SourceReader.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/input/SourceReader.java 2018-12-03 16:18:13.000000000 +0000 @@ -476,16 +476,34 @@ public final String readNumber() throws IOException { StringBuilder builder = pool.borrow(); + int lastAppended = -1; try { int c = source.read(); if (c != -1 && (isDigit(c) || c == '+' || c == '-' || c == '.')) { builder.append((char) c); + lastAppended = c; + + // Ignore double negative (this is consistent with Adobe Reader) + if (c == '-' && source.peek() == c) + { + source.read(); + } + while ((c = source.read()) != -1 && (isDigit(c) || c == '.' || c == 'E' || c == 'e' || c == '+' || c == '-')) { - builder.append((char) c); + if (c == '-' && !(lastAppended == 'e' || lastAppended == 'E')) + { + // PDFBOX-4064: ignore "-" in the middle of a number + // but not if its a negative exponent 1e-23 + } + else + { + builder.append((char) c); + lastAppended = c; + } } } unreadIfValid(c); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/output/ContentStreamWriter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/output/ContentStreamWriter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/output/ContentStreamWriter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/output/ContentStreamWriter.java 2018-12-03 16:18:13.000000000 +0000 @@ -123,7 +123,10 @@ { key.accept(this); writeSpace(); - imageParams.getDictionaryObject(key).accept(this); + COSBase imageParamsDictionaryObject = imageParams.getDictionaryObject(key); + if(imageParamsDictionaryObject != null) { + imageParamsDictionaryObject.accept(this); + } writeEOL(); } writer().write(ID_OPERATOR.getBytes(StandardCharsets.US_ASCII)); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/output/DefaultCOSWriter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/output/DefaultCOSWriter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/output/DefaultCOSWriter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/output/DefaultCOSWriter.java 2018-12-03 16:18:13.000000000 +0000 @@ -139,7 +139,7 @@ public void visit(COSName value) throws IOException { writer.write(SOLIDUS); - byte[] bytes = value.getName().getBytes(StandardCharsets.US_ASCII); + byte[] bytes = value.getName().getBytes(StandardCharsets.UTF_8); for (int i = 0; i < bytes.length; i++) { int current = bytes[i] & 0xFF; diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/output/DefaultPDFWriter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/output/DefaultPDFWriter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/output/DefaultPDFWriter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/output/DefaultPDFWriter.java 2018-12-03 16:18:13.000000000 +0000 @@ -132,6 +132,10 @@ */ public void writeXrefStream(COSDictionary trailer) throws IOException { + if (nonNull(writer.context().addWritten(XrefEntry.DEFAULT_FREE_ENTRY))) + { + LOG.warn("Reserved object number 0 has been overwritten with the expected free entry"); + } writeXrefStream(trailer, -1); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/common/filespecification/PDFileSpecification.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/common/filespecification/PDFileSpecification.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/common/filespecification/PDFileSpecification.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/common/filespecification/PDFileSpecification.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,7 +16,12 @@ */ package org.sejda.sambox.pdmodel.common.filespecification; +import org.sejda.sambox.cos.COSBase; +import org.sejda.sambox.cos.COSDictionary; import org.sejda.sambox.cos.COSObjectable; +import org.sejda.sambox.cos.COSString; + +import java.io.IOException; /** * This represents a file specification. @@ -27,6 +32,38 @@ { /** + * A file specfication can either be a COSString or a COSDictionary. This + * will create the file specification either way. + * + * @param base The cos object that describes the fs. + * + * @return The file specification for the COSBase object. + * + * @throws IOException If there is an error creating the file spec. + */ + static PDFileSpecification createFS( COSBase base ) throws IOException + { + PDFileSpecification retval = null; + if( base == null ) + { + //then simply return null + } + else if( base instanceof COSString ) + { + retval = new PDSimpleFileSpecification( (COSString)base ); + } + else if( base instanceof COSDictionary) + { + retval = new PDComplexFileSpecification( (COSDictionary)base ); + } + else + { + throw new IOException( "Error: Unknown file specification " + base ); + } + return retval; + } + + /** * @return The file name. */ String getFile(); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/common/function/PDFunctionType3.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/common/function/PDFunctionType3.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/common/function/PDFunctionType3.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/common/function/PDFunctionType3.java 2018-12-03 16:18:13.000000000 +0000 @@ -34,7 +34,8 @@ private COSArray encode = null; private COSArray bounds = null; private PDFunction[] functionsArray = null; - + private float[] boundsValues = null; + /** * Constructor. * @@ -42,7 +43,7 @@ */ public PDFunctionType3(COSBase functionStream) { - super( functionStream ); + super(functionStream); } /** @@ -53,16 +54,17 @@ { return 3; } - + /** - * {@inheritDoc} - */ + * {@inheritDoc} + */ @Override public float[] eval(float[] input) throws IOException { - //This function is known as a "stitching" function. Based on the input, it decides which child function to call. + // This function is known as a "stitching" function. Based on the input, it decides which child function to + // call. // All functions in the array are 1-value-input functions - //See PDF Reference section 3.9.3. + // See PDF Reference section 3.9.3. PDFunction function = null; float x = input[0]; PDRange domain = getDomainForInput(0); @@ -84,28 +86,33 @@ // This doesn't make sense but it may happen ... function = functionsArray[0]; PDRange encRange = getEncodeForParameter(0); - x = interpolate(x, domain.getMin(), domain.getMax(), encRange.getMin(), encRange.getMax()); + x = interpolate(x, domain.getMin(), domain.getMax(), encRange.getMin(), + encRange.getMax()); } - else + 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 - float[] partitionValues = new float[boundsSize+2]; + float[] partitionValues = new float[boundsSize + 2]; int partitionValuesSize = partitionValues.length; partitionValues[0] = domain.getMin(); - partitionValues[partitionValuesSize-1] = domain.getMax(); + partitionValues[partitionValuesSize - 1] = domain.getMax(); System.arraycopy(boundsValues, 0, partitionValues, 1, boundsSize); - // find the partition - for (int i=0; i < partitionValuesSize-1; i++) + // find the partition + for (int i = 0; i < partitionValuesSize - 1; i++) { - if ( x >= partitionValues[i] && - (x < partitionValues[i+1] || (i == partitionValuesSize - 2 && x == partitionValues[i+1]))) + if (x >= partitionValues[i] && (x < partitionValues[i + 1] + || (i == partitionValuesSize - 2 && x == partitionValues[i + 1]))) { function = functionsArray[i]; PDRange encRange = getEncodeForParameter(i); - x = interpolate(x, partitionValues[i], partitionValues[i+1], encRange.getMin(), encRange.getMax()); + x = interpolate(x, partitionValues[i], partitionValues[i + 1], + encRange.getMin(), encRange.getMax()); break; } } @@ -114,55 +121,55 @@ throw new IOException("partition not found in type 3 function"); } } - float[] functionValues = new float[]{x}; + float[] functionValues = new float[] { x }; // calculate the output values using the chosen function float[] functionResult = function.eval(functionValues); // clip to range if available return clipToRange(functionResult); } - + /** * Returns all functions values as COSArray. * - * @return the functions array. + * @return the functions array. */ public COSArray getFunctions() { if (functions == null) { - functions = (COSArray)(getCOSObject().getDictionaryObject( COSName.FUNCTIONS )); + functions = (COSArray) (getCOSObject().getDictionaryObject(COSName.FUNCTIONS)); } return functions; } - + /** * Returns all bounds values as COSArray. * - * @return the bounds array. + * @return the bounds array. */ public COSArray getBounds() { - if (bounds == null) + if (bounds == null) { - bounds = (COSArray)(getCOSObject().getDictionaryObject( COSName.BOUNDS )); + bounds = (COSArray) (getCOSObject().getDictionaryObject(COSName.BOUNDS)); } return bounds; } - + /** * Returns all encode values as COSArray. * - * @return the encode array. + * @return the encode array. */ public COSArray getEncode() { if (encode == null) { - encode = (COSArray)(getCOSObject().getDictionaryObject( COSName.ENCODE )); + encode = (COSArray) (getCOSObject().getDictionaryObject(COSName.ENCODE)); } return encode; } - + /** * Get the encode for the input parameter. * @@ -170,9 +177,9 @@ * * @return The encode parameter range or null if none is set. */ - private PDRange getEncodeForParameter(int n) + private PDRange getEncodeForParameter(int n) { COSArray encodeValues = getEncode(); - return new PDRange( encodeValues, n ); + return new PDRange(encodeValues, n); } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/common/PDNumberTreeNode.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/common/PDNumberTreeNode.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/common/PDNumberTreeNode.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/common/PDNumberTreeNode.java 2018-12-03 16:18:13.000000000 +0000 @@ -175,13 +175,20 @@ public Map getNumbers() throws IOException { Map indices = null; - COSArray namesArray = (COSArray) node.getDictionaryObject(COSName.NUMS); - if (namesArray != null) + COSArray namesArray = node.getDictionaryObject(COSName.NUMS, COSArray.class); + if (nonNull(namesArray)) { indices = new HashMap<>(); for (int i = 0; i < namesArray.size(); i += 2) { - COSInteger key = (COSInteger) namesArray.getObject(i); + COSBase base = namesArray.getObject(i); + if (!(base instanceof COSInteger)) + { + LOG.error("page labels ignored, index {} should be a number, but is {}", i, + base); + return null; + } + COSInteger key = (COSInteger) base; COSBase cosValue = namesArray.getObject(i + 1); COSObjectable pdValue = convertCOSToPD(cosValue); indices.put(key.intValue(), pdValue); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/common/PDPageLabelRange.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/common/PDPageLabelRange.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/common/PDPageLabelRange.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/common/PDPageLabelRange.java 2018-12-03 16:18:13.000000000 +0000 @@ -86,6 +86,20 @@ root = dict; } + public PDPageLabelRange(String style, String prefix, Integer start) { + this(); + + if(style != null) { + setStyle(style); + } + if(prefix != null) { + setPrefix(prefix); + } + if(start != null) { + setStart(start); + } + } + /** * Returns the underlying dictionary. * @@ -137,6 +151,13 @@ } /** + * @return true if the start value for page numbering is defined, false otherwise. + */ + public boolean hasStart() { + return root.getInt(KEY_START) != -1; + } + + /** * Sets the start value for page numbering in this page range. * * @param start diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/common/PDPageLabels.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/common/PDPageLabels.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/common/PDPageLabels.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/common/PDPageLabels.java 2018-12-03 16:18:13.000000000 +0000 @@ -20,6 +20,7 @@ import static org.sejda.util.RequireUtils.requireArg; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -142,6 +143,10 @@ labels.put(startPage, item); } + public Map getLabels() { + return Collections.unmodifiableMap(labels); + } + @Override public COSBase getCOSObject() { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java 2018-12-03 16:18:13.000000000 +0000 @@ -89,12 +89,13 @@ */ public PDStructureNode getParent() { - COSDictionary p = (COSDictionary) this.getCOSObject().getDictionaryObject(COSName.P); - if (p == null) + COSBase base = this.getCOSObject().getDictionaryObject(COSName.P); + if (base instanceof COSDictionary) { - return null; + return PDStructureNode.create((COSDictionary) base); } - return PDStructureNode.create(p); + + return null; } /** @@ -136,12 +137,12 @@ */ public PDPage getPage() { - COSDictionary pageDic = (COSDictionary) this.getCOSObject().getDictionaryObject(COSName.PG); - if (pageDic == null) + COSBase base = this.getCOSObject().getDictionaryObject(COSName.PG); + if (base instanceof COSDictionary) { - return null; + return new PDPage((COSDictionary) base); } - return new PDPage(pageDic); + return null; } /** @@ -163,7 +164,7 @@ public Revisions getAttributes() { Revisions attributes = - new Revisions(); + new Revisions<>(); COSBase a = this.getCOSObject().getDictionaryObject(COSName.A); if (a instanceof COSArray) { @@ -172,7 +173,7 @@ PDAttributeObject ao = null; while (it.hasNext()) { - COSBase item = it.next(); + COSBase item = it.next().getCOSObject(); if (item instanceof COSDictionary) { ao = PDAttributeObject.create((COSDictionary) item); @@ -325,7 +326,7 @@ public Revisions getClassNames() { COSName key = COSName.C; - Revisions classNames = new Revisions(); + Revisions classNames = new Revisions<>(); COSBase c = this.getCOSObject().getDictionaryObject(key); if (c instanceof COSName) { @@ -338,7 +339,7 @@ String className = null; while (it.hasNext()) { - COSBase item = it.next(); + COSBase item = it.next().getCOSObject(); if (item instanceof COSName) { className = ((COSName) item).getName(); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java 2018-12-03 16:18:13.000000000 +0000 @@ -75,22 +75,19 @@ public COSArray getKArray() { COSBase k = this.getCOSObject().getDictionaryObject(COSName.K); - if (k != null) + if (k instanceof COSDictionary) { - if (k instanceof COSDictionary) - { - COSDictionary kdict = (COSDictionary) k; - k = kdict.getDictionaryObject(COSName.K); - if (k instanceof COSArray) - { - return (COSArray) k; - } - } - else + COSDictionary kdict = (COSDictionary) k; + k = kdict.getDictionaryObject(COSName.K); + if (k instanceof COSArray) { return (COSArray) k; } } + else if (k instanceof COSArray) + { + return (COSArray) k; + } return null; } @@ -121,10 +118,10 @@ */ public PDNameTreeNode getIDTree() { - COSDictionary idTreeDic = (COSDictionary) this.getCOSObject().getDictionaryObject(COSName.ID_TREE); - if (idTreeDic != null) + COSBase base = this.getCOSObject().getDictionaryObject(COSName.ID_TREE); + if (base instanceof COSDictionary) { - return new PDStructureElementNameTreeNode(idTreeDic); + return new PDStructureElementNameTreeNode((COSDictionary) base); } return null; } @@ -146,10 +143,10 @@ */ public PDNumberTreeNode getParentTree() { - COSDictionary parentTreeDic = (COSDictionary) this.getCOSObject().getDictionaryObject(COSName.PARENT_TREE); - if (parentTreeDic != null) + COSBase base = getCOSObject().getDictionaryObject(COSName.PARENT_TREE); + if (base instanceof COSDictionary) { - return new PDNumberTreeNode(parentTreeDic, COSBase.class); + return new PDNumberTreeNode((COSDictionary) base, COSBase.class); } return null; } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/taggedpdf/PDExportFormatAttributeObject.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/taggedpdf/PDExportFormatAttributeObject.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/taggedpdf/PDExportFormatAttributeObject.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/taggedpdf/PDExportFormatAttributeObject.java 2018-12-03 16:18:13.000000000 +0000 @@ -27,35 +27,34 @@ { /** - * standard attribute owner: XML-1.00 + * standard attribute owner: XML-1.00 */ public static final String OWNER_XML_1_00 = "XML-1.00"; /** - * standard attribute owner: HTML-3.2 + * standard attribute owner: HTML-3.2 */ public static final String OWNER_HTML_3_20 = "HTML-3.2"; /** - * standard attribute owner: HTML-4.01 + * standard attribute owner: HTML-4.01 */ public static final String OWNER_HTML_4_01 = "HTML-4.01"; /** - * standard attribute owner: OEB-1.00 + * standard attribute owner: OEB-1.00 */ public static final String OWNER_OEB_1_00 = "OEB-1.00"; /** - * standard attribute owner: RTF-1.05 + * standard attribute owner: RTF-1.05 */ public static final String OWNER_RTF_1_05 = "RTF-1.05"; /** - * standard attribute owner: CSS-1.00 + * standard attribute owner: CSS-1.00 */ public static final String OWNER_CSS_1_00 = "CSS-1.00"; /** - * standard attribute owner: CSS-2.00 + * standard attribute owner: CSS-2.00 */ public static final String OWNER_CSS_2_00 = "CSS-2.00"; - /** * Default constructor. */ @@ -74,32 +73,29 @@ super(dictionary); } - /** - * Gets the list numbering (ListNumbering). The default value is - * {@link PDListAttributeObject#LIST_NUMBERING_NONE}. + * Gets the list numbering (ListNumbering). The default value is {@link PDListAttributeObject#LIST_NUMBERING_NONE}. * * @return the list numbering */ public String getListNumbering() { return this.getName(PDListAttributeObject.LIST_NUMBERING, - PDListAttributeObject.LIST_NUMBERING_NONE); + PDListAttributeObject.LIST_NUMBERING_NONE); } /** - * Sets the list numbering (ListNumbering). The value shall be one of the - * following: + * Sets the list numbering (ListNumbering). The value shall be one of the following: *
    - *
  • {@link PDListAttributeObject#LIST_NUMBERING_NONE},
  • - *
  • {@link PDListAttributeObject#LIST_NUMBERING_DISC},
  • - *
  • {@link PDListAttributeObject#LIST_NUMBERING_CIRCLE},
  • - *
  • {@link PDListAttributeObject#LIST_NUMBERING_SQUARE},
  • - *
  • {@link PDListAttributeObject#LIST_NUMBERING_DECIMAL},
  • - *
  • {@link PDListAttributeObject#LIST_NUMBERING_UPPER_ROMAN},
  • - *
  • {@link PDListAttributeObject#LIST_NUMBERING_LOWER_ROMAN},
  • - *
  • {@link PDListAttributeObject#LIST_NUMBERING_UPPER_ALPHA},
  • - *
  • {@link PDListAttributeObject#LIST_NUMBERING_LOWER_ALPHA}.
  • + *
  • {@link PDListAttributeObject#LIST_NUMBERING_NONE},
  • + *
  • {@link PDListAttributeObject#LIST_NUMBERING_DISC},
  • + *
  • {@link PDListAttributeObject#LIST_NUMBERING_CIRCLE},
  • + *
  • {@link PDListAttributeObject#LIST_NUMBERING_SQUARE},
  • + *
  • {@link PDListAttributeObject#LIST_NUMBERING_DECIMAL},
  • + *
  • {@link PDListAttributeObject#LIST_NUMBERING_UPPER_ROMAN},
  • + *
  • {@link PDListAttributeObject#LIST_NUMBERING_LOWER_ROMAN},
  • + *
  • {@link PDListAttributeObject#LIST_NUMBERING_UPPER_ALPHA},
  • + *
  • {@link PDListAttributeObject#LIST_NUMBERING_LOWER_ALPHA}.
  • *
* * @param listNumbering the list numbering @@ -110,8 +106,8 @@ } /** - * Gets the number of rows in the enclosing table that shall be spanned by - * the cell (RowSpan). The default value is 1. + * Gets the number of rows in the enclosing table that shall be spanned by the cell (RowSpan). The default value is + * 1. * * @return the row span */ @@ -121,8 +117,7 @@ } /** - * Sets the number of rows in the enclosing table that shall be spanned by - * the cell (RowSpan). + * Sets the number of rows in the enclosing table that shall be spanned by the cell (RowSpan). * * @param rowSpan the row span */ @@ -132,8 +127,8 @@ } /** - * Gets the number of columns in the enclosing table that shall be spanned - * by the cell (ColSpan). The default value is 1. + * Gets the number of columns in the enclosing table that shall be spanned by the cell (ColSpan). The default value + * is 1. * * @return the column span */ @@ -143,8 +138,7 @@ } /** - * Sets the number of columns in the enclosing table that shall be spanned - * by the cell (ColSpan). + * Sets the number of columns in the enclosing table that shall be spanned by the cell (ColSpan). * * @param colSpan the column span */ @@ -154,10 +148,9 @@ } /** - * Gets the headers (Headers). An array of byte strings, where each string - * shall be the element identifier (see the - * {@link org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement#getElementIdentifier()}) for a TH structure - * element that shall be used as a header associated with this cell. + * Gets the headers (Headers). An array of byte strings, where each string shall be the element identifier (see the + * {@link org.sejda.sambox.pdmodel.documentinterchange.logicalstructure.PDStructureElement#getElementIdentifier()}) + * for a TH structure element that shall be used as a header associated with this cell. * * @return the headers. */ @@ -167,10 +160,9 @@ } /** - * Sets the headers (Headers). An array of byte strings, where each string - * shall be the element identifier (see the - * {@link org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement#getElementIdentifier()}) for a TH structure - * element that shall be used as a header associated with this cell. + * Sets the headers (Headers). An array of byte strings, where each string shall be the element identifier (see the + * {@link org.sejda.sambox.pdmodel.documentinterchange.logicalstructure.PDStructureElement#getElementIdentifier()}) + * for a TH structure element that shall be used as a header associated with this cell. * * @param headers the headers */ @@ -180,9 +172,8 @@ } /** - * Gets the scope (Scope). It shall reflect whether the header cell applies - * to the rest of the cells in the row that contains it, the column that - * contains it, or both the row and the column that contain it. + * Gets the scope (Scope). It shall reflect whether the header cell applies to the rest of the cells in the row that + * contains it, the column that contains it, or both the row and the column that contain it. * * @return the scope */ @@ -192,14 +183,13 @@ } /** - * Sets the scope (Scope). It shall reflect whether the header cell applies - * to the rest of the cells in the row that contains it, the column that - * contains it, or both the row and the column that contain it. The value - * shall be one of the following: + * Sets the scope (Scope). It shall reflect whether the header cell applies to the rest of the cells in the row that + * contains it, the column that contains it, or both the row and the column that contain it. The value shall be one + * of the following: *
    - *
  • {@link PDTableAttributeObject#SCOPE_ROW},
  • - *
  • {@link PDTableAttributeObject#SCOPE_COLUMN}, or
  • - *
  • {@link PDTableAttributeObject#SCOPE_BOTH}.
  • + *
  • {@link PDTableAttributeObject#SCOPE_ROW},
  • + *
  • {@link PDTableAttributeObject#SCOPE_COLUMN}, or
  • + *
  • {@link PDTableAttributeObject#SCOPE_BOTH}.
  • *
* * @param scope the scope @@ -229,7 +219,6 @@ this.setString(PDTableAttributeObject.SUMMARY, summary); } - @Override public String toString() { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/taggedpdf/PDTableAttributeObject.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/taggedpdf/PDTableAttributeObject.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/taggedpdf/PDTableAttributeObject.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/documentinterchange/taggedpdf/PDTableAttributeObject.java 2018-12-03 16:18:13.000000000 +0000 @@ -27,20 +27,20 @@ { /** - * standard attribute owner: Table + * standard attribute owner: Table */ public static final String OWNER_TABLE = "Table"; protected static final String ROW_SPAN = "RowSpan"; protected static final String COL_SPAN = "ColSpan"; - protected static final String HEADERS = "Headers"; - protected static final String SCOPE = "Scope"; - protected static final String SUMMARY = "Summary"; + protected static final String HEADERS = "Headers"; + protected static final String SCOPE = "Scope"; + protected static final String SUMMARY = "Summary"; /** * Scope: Both */ - public static final String SCOPE_BOTH = "Both"; + public static final String SCOPE_BOTH = "Both"; /** * Scope: Column */ @@ -48,8 +48,7 @@ /** * Scope: Row */ - public static final String SCOPE_ROW = "Row"; - + public static final String SCOPE_ROW = "Row"; /** * Default constructor. @@ -69,10 +68,9 @@ super(dictionary); } - /** - * Gets the number of rows in the enclosing table that shall be spanned by - * the cell (RowSpan). The default value is 1. + * Gets the number of rows in the enclosing table that shall be spanned by the cell (RowSpan). The default value is + * 1. * * @return the row span */ @@ -82,8 +80,7 @@ } /** - * Sets the number of rows in the enclosing table that shall be spanned by - * the cell (RowSpan). + * Sets the number of rows in the enclosing table that shall be spanned by the cell (RowSpan). * * @param rowSpan the row span */ @@ -93,8 +90,8 @@ } /** - * Gets the number of columns in the enclosing table that shall be spanned - * by the cell (ColSpan). The default value is 1. + * Gets the number of columns in the enclosing table that shall be spanned by the cell (ColSpan). The default value + * is 1. * * @return the column span */ @@ -104,8 +101,7 @@ } /** - * Sets the number of columns in the enclosing table that shall be spanned - * by the cell (ColSpan). + * Sets the number of columns in the enclosing table that shall be spanned by the cell (ColSpan). * * @param colSpan the column span */ @@ -115,10 +111,9 @@ } /** - * Gets the headers (Headers). An array of byte strings, where each string - * shall be the element identifier (see the - * {@link org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement#getElementIdentifier()}) for a TH structure - * element that shall be used as a header associated with this cell. + * Gets the headers (Headers). An array of byte strings, where each string shall be the element identifier (see the + * {@link org.sejda.sambox.pdmodel.documentinterchange.logicalstructure.PDStructureElement#getElementIdentifier()}) + * for a TH structure element that shall be used as a header associated with this cell. * * @return the headers. */ @@ -128,10 +123,9 @@ } /** - * Sets the headers (Headers). An array of byte strings, where each string - * shall be the element identifier (see the - * {@link org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement#getElementIdentifier()}) for a TH structure - * element that shall be used as a header associated with this cell. + * Sets the headers (Headers). An array of byte strings, where each string shall be the element identifier (see the + * {@link org.sejda.sambox.pdmodel.documentinterchange.logicalstructure.PDStructureElement#getElementIdentifier()}) + * for a TH structure element that shall be used as a header associated with this cell. * * @param headers the headers */ @@ -141,9 +135,8 @@ } /** - * Gets the scope (Scope). It shall reflect whether the header cell applies - * to the rest of the cells in the row that contains it, the column that - * contains it, or both the row and the column that contain it. + * Gets the scope (Scope). It shall reflect whether the header cell applies to the rest of the cells in the row that + * contains it, the column that contains it, or both the row and the column that contain it. * * @return the scope */ @@ -153,14 +146,13 @@ } /** - * Sets the scope (Scope). It shall reflect whether the header cell applies - * to the rest of the cells in the row that contains it, the column that - * contains it, or both the row and the column that contain it. The value - * shall be one of the following: + * Sets the scope (Scope). It shall reflect whether the header cell applies to the rest of the cells in the row that + * contains it, the column that contains it, or both the row and the column that contain it. The value shall be one + * of the following: *
    - *
  • {@link #SCOPE_ROW},
  • - *
  • {@link #SCOPE_COLUMN}, or
  • - *
  • {@link #SCOPE_BOTH}.
  • + *
  • {@link #SCOPE_ROW},
  • + *
  • {@link #SCOPE_COLUMN}, or
  • + *
  • {@link #SCOPE_BOTH}.
  • *
* * @param scope the scope diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/encryption/AccessPermission.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/encryption/AccessPermission.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/encryption/AccessPermission.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/encryption/AccessPermission.java 2018-12-03 16:18:13.000000000 +0000 @@ -18,8 +18,8 @@ package org.sejda.sambox.pdmodel.encryption; /** - * This class represents the access permissions to a document. - * These permissions are specified in the PDF format specifications, they include: + * This class represents the access permissions to a document. These permissions are specified in the PDF format + * specifications, they include: *
    *
  • print the document
  • *
  • modify the content of the document
  • @@ -31,15 +31,15 @@ *
  • print in degraded quality
  • *
* - * This class can be used to protect a document by assigning access permissions to recipients. - * In this case, it must be used with a specific ProtectionPolicy. + * This class can be used to protect a document by assigning access permissions to recipients. In this case, it must be + * used with a specific ProtectionPolicy. * * - * When a document is decrypted, it has a currentAccessPermission property which is the access permissions - * granted to the user who decrypted the document. + * When a document is decrypted, it has a currentAccessPermission property which is the access permissions granted to + * the user who decrypted the document. * * @see ProtectionPolicy - * @see org.apache.pdfbox.pdmodel.PDDocument#getCurrentAccessPermission() + * @see org.sejda.sambox.pdmodel.PDDocument#getCurrentAccessPermission() * * @author Ben Litchfield * @author Benoit Guillon @@ -49,7 +49,7 @@ public class AccessPermission { - private static final int DEFAULT_PERMISSIONS = ~3;//bits 0 & 1 need to be zero + private static final int DEFAULT_PERMISSIONS = ~3;// bits 0 & 1 need to be zero private static final int PRINT_BIT = 3; private static final int MODIFICATION_BIT = 4; private static final int EXTRACT_BIT = 5; @@ -64,8 +64,7 @@ private boolean readOnly = false; /** - * Create a new access permission object. - * By default, all permissions are granted. + * Create a new access permission object. By default, all permissions are granted. */ public AccessPermission() { @@ -73,8 +72,7 @@ } /** - * Create a new access permission object from a byte array. - * Bytes are ordered most significant byte first. + * Create a new access permission object from a byte array. Bytes are ordered most significant byte first. * * @param b the bytes as defined in PDF specs */ @@ -96,22 +94,22 @@ * * @param permissions The permission bits. */ - public AccessPermission( int permissions ) + public AccessPermission(int permissions) { bytes = permissions; } - private boolean isPermissionBitOn( int bit ) + private boolean isPermissionBitOn(int bit) { - return (bytes & (1 << (bit-1))) != 0; + return (bytes & (1 << (bit - 1))) != 0; } - private boolean setPermissionBit( int bit, boolean value ) + private boolean setPermissionBit(int bit, boolean value) { int permissions = bytes; - if( value ) + if (value) { - permissions = permissions | (1 << (bit-1)); + permissions = permissions | (1 << (bit - 1)); } else { @@ -119,29 +117,19 @@ } bytes = permissions; - return (bytes & (1 << (bit-1))) != 0; + return (bytes & (1 << (bit - 1))) != 0; } - - - /** - * This will tell if the access permission corresponds to owner - * access permission (no restriction). + * This will tell if the access permission corresponds to owner access permission (no restriction). * * @return true if the access permission does not restrict the use of the document */ public boolean isOwnerPermission() { - return (this.canAssembleDocument() - && this.canExtractContent() - && this.canExtractForAccessibility() - && this.canFillInForm() - && this.canModify() - && this.canModifyAnnotations() - && this.canPrint() - && this.canPrintDegraded() - ); + return (this.canAssembleDocument() && this.canExtractContent() + && this.canExtractForAccessibility() && this.canFillInForm() && this.canModify() + && this.canModifyAnnotations() && this.canPrint() && this.canPrintDegraded()); } /** @@ -165,10 +153,9 @@ } /** - * This returns an integer representing the access permissions. - * This integer can be used for public key encryption. This format - * is not documented in the PDF specifications but is necessary for compatibility - * with Adobe Acrobat and Adobe Reader. + * This returns an integer representing the access permissions. This integer can be used for public key encryption. + * This format is not documented in the PDF specifications but is necessary for compatibility with Adobe Acrobat and + * Adobe Reader. * * @return the integer representing access permissions */ @@ -178,7 +165,7 @@ setPermissionBit(1, true); setPermissionBit(7, false); setPermissionBit(8, false); - for(int i=13; i<=32; i++) + for (int i = 13; i <= 32; i++) { setPermissionBit(i, false); } @@ -186,9 +173,8 @@ } /** - * The returns an integer representing the access permissions. - * This integer can be used for standard PDF encryption as specified - * in the PDF specifications. + * The returns an integer representing the access permissions. This integer can be used for standard PDF encryption + * as specified in the PDF specifications. * * @return the integer representing the access permissions */ @@ -204,7 +190,7 @@ */ public boolean canPrint() { - return isPermissionBitOn( PRINT_BIT ); + return isPermissionBitOn(PRINT_BIT); } /** @@ -214,11 +200,11 @@ * * @param allowPrinting A boolean determining if the user can print. */ - public void setCanPrint( boolean allowPrinting ) + public void setCanPrint(boolean allowPrinting) { - if(!readOnly) + if (!readOnly) { - setPermissionBit( PRINT_BIT, allowPrinting ); + setPermissionBit(PRINT_BIT, allowPrinting); } } @@ -229,7 +215,7 @@ */ public boolean canModify() { - return isPermissionBitOn( MODIFICATION_BIT ); + return isPermissionBitOn(MODIFICATION_BIT); } /** @@ -239,23 +225,22 @@ * * @param allowModifications A boolean determining if the user can modify the document. */ - public void setCanModify( boolean allowModifications ) + public void setCanModify(boolean allowModifications) { - if(!readOnly) + if (!readOnly) { - setPermissionBit( MODIFICATION_BIT, allowModifications ); + setPermissionBit(MODIFICATION_BIT, allowModifications); } } /** * This will tell if the user can extract text and images from the PDF document. * - * @return true If supplied with the user password they are allowed to extract content - * from the PDF document + * @return true If supplied with the user password they are allowed to extract content from the PDF document */ public boolean canExtractContent() { - return isPermissionBitOn( EXTRACT_BIT ); + return isPermissionBitOn(EXTRACT_BIT); } /** @@ -263,88 +248,83 @@ *

* This method will have no effect if the object is in read only mode * - * @param allowExtraction A boolean determining if the user can extract content - * from the document. + * @param allowExtraction A boolean determining if the user can extract content from the document. */ - public void setCanExtractContent( boolean allowExtraction ) + public void setCanExtractContent(boolean allowExtraction) { - if(!readOnly) + if (!readOnly) { - setPermissionBit( EXTRACT_BIT, allowExtraction ); + setPermissionBit(EXTRACT_BIT, allowExtraction); } } /** - * This will tell if the user can add or modify text annotations and fill in interactive forms - * fields and, if {@link #canModify() canModify()} returns true, create or modify interactive - * form fields (including signature fields). Note that if - * {@link #canFillInForm() canFillInForm()} returns true, it is still possible to fill in + * This will tell if the user can add or modify text annotations and fill in interactive forms fields and, if + * {@link #canModify() canModify()} returns true, create or modify interactive form fields (including signature + * fields). Note that if {@link #canFillInForm() canFillInForm()} returns true, it is still possible to fill in * interactive forms (including signature fields) even if this method here returns false. * * @return true If supplied with the user password they are allowed to modify annotations. */ public boolean canModifyAnnotations() { - return isPermissionBitOn( MODIFY_ANNOTATIONS_BIT ); + return isPermissionBitOn(MODIFY_ANNOTATIONS_BIT); } /** - * Set if the user can add or modify text annotations and fill in interactive forms fields and, - * (including signature fields). Note that if {@link #canFillInForm() canFillInForm()} returns - * true, it is still possible to fill in interactive forms (including signature fields) even the - * parameter here is false. + * Set if the user can add or modify text annotations and fill in interactive forms fields and, (including signature + * fields). Note that if {@link #canFillInForm() canFillInForm()} returns true, it is still possible to fill in + * interactive forms (including signature fields) even the parameter here is false. *

* This method will have no effect if the object is in read only mode. * * @param allowAnnotationModification A boolean determining the new setting. */ - public void setCanModifyAnnotations( boolean allowAnnotationModification ) + public void setCanModifyAnnotations(boolean allowAnnotationModification) { - if(!readOnly) + if (!readOnly) { - setPermissionBit( MODIFY_ANNOTATIONS_BIT, allowAnnotationModification ); + setPermissionBit(MODIFY_ANNOTATIONS_BIT, allowAnnotationModification); } } /** - * This will tell if the user can fill in interactive form fields (including signature fields) - * even if {@link #canModifyAnnotations() canModifyAnnotations()} returns false. + * This will tell if the user can fill in interactive form fields (including signature fields) even if + * {@link #canModifyAnnotations() canModifyAnnotations()} returns false. * * @return true If supplied with the user password they are allowed to fill in form fields. */ public boolean canFillInForm() { - return isPermissionBitOn( FILL_IN_FORM_BIT ); + return isPermissionBitOn(FILL_IN_FORM_BIT); } /** * Set if the user can fill in interactive form fields (including signature fields) even if - * {@link #canModifyAnnotations() canModifyAnnotations()} returns false. Therefore, if you want - * to prevent a user from filling in interactive form fields, you need to call - * {@link #setCanModifyAnnotations(boolean) setCanModifyAnnotations(false)} as well. - *

+ * {@link #canModifyAnnotations() canModifyAnnotations()} returns false. Therefore, if you want to prevent a user + * from filling in interactive form fields, you need to call {@link #setCanModifyAnnotations(boolean) + * setCanModifyAnnotations(false)} as well. + *

* This method will have no effect if the object is in read only mode. * * @param allowFillingInForm A boolean determining if the user can fill in interactive forms. */ - public void setCanFillInForm( boolean allowFillingInForm ) + public void setCanFillInForm(boolean allowFillingInForm) { - if(!readOnly) + if (!readOnly) { - setPermissionBit( FILL_IN_FORM_BIT, allowFillingInForm ); + setPermissionBit(FILL_IN_FORM_BIT, allowFillingInForm); } } /** - * This will tell if the user can extract text and images from the PDF document - * for accessibility purposes. + * This will tell if the user can extract text and images from the PDF document for accessibility purposes. * - * @return true If supplied with the user password they are allowed to extract content - * from the PDF document + * @return true If supplied with the user password they are allowed to extract content from the PDF document */ public boolean canExtractForAccessibility() { - return isPermissionBitOn( EXTRACT_FOR_ACCESSIBILITY_BIT ); + return isPermissionBitOn(EXTRACT_FOR_ACCESSIBILITY_BIT); } /** @@ -352,26 +332,24 @@ *

* This method will have no effect if the object is in read only mode. * - * @param allowExtraction A boolean determining if the user can extract content - * from the document. + * @param allowExtraction A boolean determining if the user can extract content from the document. */ - public void setCanExtractForAccessibility( boolean allowExtraction ) + public void setCanExtractForAccessibility(boolean allowExtraction) { - if(!readOnly) + if (!readOnly) { - setPermissionBit( EXTRACT_FOR_ACCESSIBILITY_BIT, allowExtraction ); + setPermissionBit(EXTRACT_FOR_ACCESSIBILITY_BIT, allowExtraction); } } /** * This will tell if the user can insert/rotate/delete pages. * - * @return true If supplied with the user password they are allowed to extract content - * from the PDF document + * @return true If supplied with the user password they are allowed to extract content from the PDF document */ public boolean canAssembleDocument() { - return isPermissionBitOn( ASSEMBLE_DOCUMENT_BIT ); + return isPermissionBitOn(ASSEMBLE_DOCUMENT_BIT); } /** @@ -381,23 +359,22 @@ * * @param allowAssembly A boolean determining if the user can assemble the document. */ - public void setCanAssembleDocument( boolean allowAssembly ) + public void setCanAssembleDocument(boolean allowAssembly) { - if(!readOnly) + if (!readOnly) { - setPermissionBit( ASSEMBLE_DOCUMENT_BIT, allowAssembly ); + setPermissionBit(ASSEMBLE_DOCUMENT_BIT, allowAssembly); } } /** * This will tell if the user can print the document in a degraded format. * - * @return true If supplied with the user password they are allowed to print the - * document in a degraded format. + * @return true If supplied with the user password they are allowed to print the document in a degraded format. */ public boolean canPrintDegraded() { - return isPermissionBitOn( DEGRADED_PRINT_BIT ); + return isPermissionBitOn(DEGRADED_PRINT_BIT); } /** @@ -409,17 +386,16 @@ */ public void setCanPrintDegraded(boolean canPrintDegraded) { - if(!readOnly) + if (!readOnly) { setPermissionBit(DEGRADED_PRINT_BIT, canPrintDegraded); } } /** - * Locks the access permission read only (ie, the setters will have no effects). - * After that, the object cannot be unlocked. - * This method is used for the currentAccessPermssion of a document to avoid - * users to change access permission. + * Locks the access permission read only (ie, the setters will have no effects). After that, the object cannot be + * unlocked. This method is used for the currentAccessPermssion of a document to avoid users to change access + * permission. */ public void setReadOnly() { @@ -436,7 +412,7 @@ { return readOnly; } - + /** * Indicates if any revision 3 access permission is set or not. * diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/encryption/PublicKeySecurityHandler.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/encryption/PublicKeySecurityHandler.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/encryption/PublicKeySecurityHandler.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/encryption/PublicKeySecurityHandler.java 2018-12-03 16:18:13.000000000 +0000 @@ -49,7 +49,7 @@ public static final String FILTER = "Adobe.PubSec"; private PublicKeyProtectionPolicy policy = null; - + /** * Constructor. */ @@ -71,21 +71,17 @@ /** * Prepares everything to decrypt the document. * - * @param encryption encryption dictionary, can be retrieved via - * {@link PDDocument#getEncryption()} - * @param documentIDArray document id which is returned via - * {@link org.apache.pdfbox.cos.COSDocument#getDocumentID()} (not used by - * this handler) + * @param encryption encryption dictionary, can be retrieved via {@link PDDocument#getEncryption()} + * @param documentIDArray document id which is returned via {@link org.sejda.sambox.cos.COSDocument#getDocumentID()} + * (not used by this handler) * @param decryptionMaterial Information used to decrypt the document. * - * @throws IOException If there is an error accessing data. If verbose mode - * is enabled, the exception message will provide more details why the - * match wasn't successful. + * @throws IOException If there is an error accessing data. If verbose mode is enabled, the exception message will + * provide more details why the match wasn't successful. */ @Override public void prepareForDecryption(PDEncryption encryption, COSArray documentIDArray, - DecryptionMaterial decryptionMaterial) - throws IOException + DecryptionMaterial decryptionMaterial) throws IOException { if (!(decryptionMaterial instanceof PublicKeyDecryptionMaterial)) { @@ -139,7 +135,8 @@ { foundRecipient = true; PrivateKey privateKey = (PrivateKey) material.getPrivateKey(); - envelopedData = ri.getContent(new JceKeyTransEnvelopedRecipient(privateKey)); + envelopedData = ri + .getContent(new JceKeyTransEnvelopedRecipient(privateKey)); break; } j++; @@ -150,7 +147,8 @@ extraInfo.append(": "); if (rid instanceof KeyTransRecipientId) { - appendCertInfo(extraInfo, (KeyTransRecipientId) rid, certificate, materialCert); + appendCertInfo(extraInfo, (KeyTransRecipientId) rid, certificate, + materialCert); } } } @@ -159,8 +157,8 @@ } if (!foundRecipient || envelopedData == null) { - throw new IOException("The certificate matches none of " + i - + " recipient entries" + extraInfo.toString()); + throw new IOException("The certificate matches none of " + i + " recipient entries" + + extraInfo.toString()); } if (envelopedData.length != 24) { @@ -213,7 +211,7 @@ } } - private void appendCertInfo(StringBuilder extraInfo, KeyTransRecipientId ktRid, + private void appendCertInfo(StringBuilder extraInfo, KeyTransRecipientId ktRid, X509Certificate certificate, X509CertificateHolder materialCert) { BigInteger ridSerialNumber = ktRid.getSerialNumber(); @@ -236,7 +234,7 @@ extraInfo.append("\' "); } } - + /** * {@inheritDoc} */ diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/encryption/SecurityHandler.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/encryption/SecurityHandler.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/encryption/SecurityHandler.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/encryption/SecurityHandler.java 2018-12-03 16:18:13.000000000 +0000 @@ -109,8 +109,7 @@ * Prepares everything to decrypt the document. * * @param encryption encryption dictionary, can be retrieved via {@link PDDocument#getEncryption()} - * @param documentIDArray document id which is returned via - * {@link org.apache.pdfbox.cos.COSDocument#getDocumentID()} + * @param documentIDArray document id which is returned via {@link org.sejda.sambox.cos.COSDocument#getDocumentID()} * @param decryptionMaterial Information used to decrypt the document. * * @throws IOException If there is an error accessing data. diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/encoding/GlyphList.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/encoding/GlyphList.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/encoding/GlyphList.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/encoding/GlyphList.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,12 +16,15 @@ */ package org.sejda.sambox.pdmodel.font.encoding; +import static java.util.Objects.nonNull; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,7 +98,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. @@ -283,7 +286,11 @@ LOG.warn("Not a number in Unicode character name: {}", name); } } - uniNameToUnicodeCache.put(name, unicode); + if (nonNull(unicode)) + { + // null value not allowed in ConcurrentHashMap + uniNameToUnicodeCache.put(name, unicode); + } } return unicode; } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/FileSystemFontProvider.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/FileSystemFontProvider.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/FileSystemFontProvider.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/FileSystemFontProvider.java 2018-12-03 16:18:13.000000000 +0000 @@ -61,6 +61,7 @@ private final List fontInfoList = new ArrayList<>(); private final FontCache cache; + private boolean initialized = false; private static class FSFontInfo extends FontInfo { @@ -136,7 +137,10 @@ default: throw new RuntimeException("can't happen"); } - parent.cache.addFont(this, font); + if(font != null) + { + parent.cache.addFont(this, font); + } return font; } @@ -200,6 +204,19 @@ FileSystemFontProvider(FontCache cache) { this.cache = cache; + // init block moved to lazy initialization when required + } + + private synchronized void initializeIfRequired() + { + if(!this.initialized) { + initialize(); + this.initialized = true; + } + } + + private void initialize() + { try { LOG.trace("Will search the local system for fonts"); @@ -276,57 +293,60 @@ */ private void saveDiskCache() { - File file = getDiskCacheFile(); - try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) + try { - for (FSFontInfo fontInfo : fontInfoList) + File file = getDiskCacheFile(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { - writer.write(fontInfo.postScriptName.trim().replace("|", "\\|")); - writer.write(FONT_CACHE_SEPARATOR); - writer.write(fontInfo.format.toString()); - writer.write(FONT_CACHE_SEPARATOR); - if (fontInfo.cidSystemInfo != null) - { - writer.write(fontInfo.cidSystemInfo.getRegistry() + '-' - + fontInfo.cidSystemInfo.getOrdering() + '-' - + fontInfo.cidSystemInfo.getSupplement()); - } - writer.write(FONT_CACHE_SEPARATOR); - if (fontInfo.usWeightClass > -1) - { - writer.write(Integer.toHexString(fontInfo.usWeightClass)); - } - writer.write(FONT_CACHE_SEPARATOR); - if (fontInfo.sFamilyClass > -1) - { - writer.write(Integer.toHexString(fontInfo.sFamilyClass)); - } - writer.write(FONT_CACHE_SEPARATOR); - writer.write(Integer.toHexString(fontInfo.ulCodePageRange1)); - writer.write(FONT_CACHE_SEPARATOR); - writer.write(Integer.toHexString(fontInfo.ulCodePageRange2)); - writer.write(FONT_CACHE_SEPARATOR); - if (fontInfo.macStyle > -1) + for (FSFontInfo fontInfo : fontInfoList) { - writer.write(Integer.toHexString(fontInfo.macStyle)); - } - writer.write(FONT_CACHE_SEPARATOR); - if (fontInfo.panose != null) - { - byte[] bytes = fontInfo.panose.getBytes(); - for (int i = 0; i < 10; i++) + writer.write(fontInfo.postScriptName.trim().replace("|", "\\|")); + writer.write(FONT_CACHE_SEPARATOR); + writer.write(fontInfo.format.toString()); + writer.write(FONT_CACHE_SEPARATOR); + if (fontInfo.cidSystemInfo != null) + { + writer.write( + fontInfo.cidSystemInfo.getRegistry() + '-' + fontInfo.cidSystemInfo.getOrdering() + '-' + + fontInfo.cidSystemInfo.getSupplement()); + } + writer.write(FONT_CACHE_SEPARATOR); + if (fontInfo.usWeightClass > -1) + { + writer.write(Integer.toHexString(fontInfo.usWeightClass)); + } + writer.write(FONT_CACHE_SEPARATOR); + if (fontInfo.sFamilyClass > -1) + { + writer.write(Integer.toHexString(fontInfo.sFamilyClass)); + } + writer.write(FONT_CACHE_SEPARATOR); + writer.write(Integer.toHexString(fontInfo.ulCodePageRange1)); + writer.write(FONT_CACHE_SEPARATOR); + writer.write(Integer.toHexString(fontInfo.ulCodePageRange2)); + writer.write(FONT_CACHE_SEPARATOR); + if (fontInfo.macStyle > -1) { - String str = Integer.toHexString(bytes[i]); - if (str.length() == 1) + writer.write(Integer.toHexString(fontInfo.macStyle)); + } + writer.write(FONT_CACHE_SEPARATOR); + if (fontInfo.panose != null) + { + byte[] bytes = fontInfo.panose.getBytes(); + for (int i = 0; i < 10; i++) { - writer.write('0'); + String str = Integer.toHexString(bytes[i]); + if (str.length() == 1) + { + writer.write('0'); + } + writer.write(str); } - writer.write(str); } + writer.write(FONT_CACHE_SEPARATOR); + writer.write(fontInfo.file.getAbsolutePath()); + writer.newLine(); } - writer.write(FONT_CACHE_SEPARATOR); - writer.write(fontInfo.file.getAbsolutePath()); - writer.newLine(); } } catch (IOException | SecurityException e) @@ -416,10 +436,18 @@ } fontFile = new File(parts[9]); - FSFontInfo info = new FSFontInfo(fontFile, format, postScriptName, - cidSystemInfo, usWeightClass, sFamilyClass, ulCodePageRange1, - ulCodePageRange2, macStyle, panose, this); - results.add(info); + if(fontFile.exists()) + { + + FSFontInfo info = new FSFontInfo(fontFile, format, postScriptName, + cidSystemInfo, usWeightClass, sFamilyClass, ulCodePageRange1, + ulCodePageRange2, macStyle, panose, this); + results.add(info); + } + else + { + LOG.debug("Font file {} not found, skipped", fontFile.getAbsolutePath()); + } pending.remove(fontFile.getAbsolutePath()); } } @@ -679,6 +707,7 @@ @Override public String toDebugString() { + initializeIfRequired(); StringBuilder sb = new StringBuilder(); for (FSFontInfo info : fontInfoList) { @@ -695,6 +724,7 @@ @Override public List getFontInfo() { + initializeIfRequired(); return fontInfoList; } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/FontMapperImpl.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/FontMapperImpl.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/FontMapperImpl.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/FontMapperImpl.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,6 +16,8 @@ */ package org.sejda.sambox.pdmodel.font; +import static java.util.Optional.ofNullable; + import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -502,7 +504,7 @@ { return new CIDFontMapping((OpenTypeFont) font, null, true); } - else + else if (font != null) { return new CIDFontMapping(null, font, true); } @@ -542,6 +544,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()) { @@ -606,6 +616,14 @@ return queue; } + private static boolean probablyBarcodeFont(PDFontDescriptor fontDescriptor) + { + String ff = ofNullable(fontDescriptor.getFontFamily()).orElse(""); + String fn = ofNullable(fontDescriptor.getFontName()).orElse(""); + 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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/FontUtils.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/FontUtils.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/FontUtils.java 1970-01-01 00:00:00.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/FontUtils.java 2018-12-03 16:18:13.000000000 +0000 @@ -0,0 +1,119 @@ +/* + * 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.sejda.sambox.pdmodel.font; + +import java.io.IOException; +import java.util.Map; + +import org.apache.fontbox.ttf.OS2WindowsMetricsTable; +import org.apache.fontbox.ttf.TrueTypeFont; + +/** + * @author Andrea Vacondio + */ +public final class FontUtils +{ + private static final String BASE25 = "BCDEFGHIJKLMNOPQRSTUVWXYZ"; + + private FontUtils() + { + // util + } + + /** + * @return true if the fsType in the OS/2 table permits embedding. + */ + public static boolean isEmbeddingPermitted(TrueTypeFont ttf) throws IOException + { + if (ttf.getOS2Windows() != null) + { + int fsType = ttf.getOS2Windows().getFsType(); + int exclusive = fsType & 0x8; // bits 0-3 are a set of exclusive bits + + if ((exclusive + & OS2WindowsMetricsTable.FSTYPE_RESTRICTED) == OS2WindowsMetricsTable.FSTYPE_RESTRICTED) + { + // restricted License embedding + return false; + } + else if ((exclusive + & OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) == OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) + { + // bitmap embedding only + return false; + } + } + return true; + } + + /** + * @return true if the fsType in the OS/2 table permits subsetting. + */ + public static boolean isSubsettingPermitted(TrueTypeFont ttf) throws IOException + { + if (ttf.getOS2Windows() != null) + { + int fsType = ttf.getOS2Windows().getFsType(); + if ((fsType + & OS2WindowsMetricsTable.FSTYPE_NO_SUBSETTING) == OS2WindowsMetricsTable.FSTYPE_NO_SUBSETTING) + { + return false; + } + } + return true; + } + + /** + * @return an uppercase 6-character unique tag for the given subset. + */ + public static String getTag(Map gidToCid) + { + // deterministic + long num = gidToCid.hashCode(); + + // base25 encode + StringBuilder sb = new StringBuilder(); + do + { + long div = num / 25; + int mod = (int) (num % 25); + sb.append(BASE25.charAt(mod)); + num = div; + } while (num != 0 && sb.length() < 6); + + // pad + while (sb.length() < 6) + { + sb.insert(0, 'A'); + } + + return sb.append('+').toString(); + } + + /** + * @return an uppercase 6-character unique tag randomly created + */ + public static String getTag() + { + StringBuilder sb = new StringBuilder(""); + for (int k = 0; k < 6; ++k) + { + sb.append((char) (Math.random() * 26 + 'A')); + } + return sb.append('+').toString(); + } +} diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFont.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFont.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFont.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFont.java 2018-12-03 16:18:13.000000000 +0000 @@ -19,16 +19,13 @@ import static java.util.Objects.nonNull; import java.io.IOException; +import java.io.InputStream; import java.util.HashMap; import java.util.Map; +import org.apache.commons.io.IOUtils; import org.apache.fontbox.util.BoundingBox; -import org.sejda.sambox.cos.COSArray; -import org.sejda.sambox.cos.COSBase; -import org.sejda.sambox.cos.COSDictionary; -import org.sejda.sambox.cos.COSName; -import org.sejda.sambox.cos.COSNumber; -import org.sejda.sambox.cos.COSObjectable; +import org.sejda.sambox.cos.*; import org.sejda.sambox.util.Matrix; import org.sejda.sambox.util.Vector; @@ -138,7 +135,7 @@ COSArray array = (COSArray) next; for (int j = 0; j < array.size(); j++) { - int cid = c.intValue() + j; + int cid = c.intValue() + j / 3; COSNumber w1y = (COSNumber) array.getObject(j); COSNumber v1x = (COSNumber) array.getObject(++j); COSNumber v1y = (COSNumber) array.getObject(++j); @@ -258,6 +255,12 @@ } @Override + public boolean hasExplicitWidth(int code) throws IOException + { + return widths.get(codeToCID(code)) != null; + } + + @Override public Vector getPositionVector(int code) { int cid = codeToCID(code); @@ -375,4 +378,28 @@ * @throws IOException If the text could not be encoded. */ protected abstract byte[] encode(int unicode) throws IOException; + + final int[] readCIDToGIDMap() throws IOException + { + int[] cid2gid = null; + COSBase map = dict.getDictionaryObject(COSName.CID_TO_GID_MAP); + if (map instanceof COSStream) + { + COSStream stream = (COSStream) map; + + InputStream is = stream.getUnfilteredStream(); + byte[] mapAsBytes = IOUtils.toByteArray(is); + IOUtils.closeQuietly(is); + int numberOfInts = mapAsBytes.length / 2; + cid2gid = new int[numberOfInts]; + int offset = 0; + for (int index = 0; index < numberOfInts; index++) + { + int gid = (mapAsBytes[offset] & 0xff) << 8 | mapAsBytes[offset + 1] & 0xff; + cid2gid[index] = gid; + offset += 2; + } + } + return cid2gid; + } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType0.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType0.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType0.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType0.java 2018-12-03 16:18:13.000000000 +0000 @@ -62,6 +62,7 @@ private Matrix fontMatrix; private final AffineTransform fontMatrixTransform; private BoundingBox fontBBox; + private int[] cid2gid = null; /** * Constructor. @@ -119,6 +120,7 @@ cidFont = null; t1Font = cffFont; } + cid2gid = readCIDToGIDMap(); isEmbedded = true; isDamaged = false; } @@ -226,11 +228,13 @@ if (getFontDescriptor() != null) { PDRectangle bbox = getFontDescriptor().getFontBoundingBox(); - if (bbox.getLowerLeftX() != 0 || bbox.getLowerLeftY() != 0 || bbox.getUpperRightX() != 0 - || bbox.getUpperRightY() != 0) + if(bbox != null) { - return new BoundingBox(bbox.getLowerLeftX(), bbox.getLowerLeftY(), - bbox.getUpperRightX(), bbox.getUpperRightY()); + if (bbox.getLowerLeftX() != 0 || bbox.getLowerLeftY() != 0 || bbox.getUpperRightX() != 0 + || bbox.getUpperRightY() != 0) { + return new BoundingBox(bbox.getLowerLeftX(), bbox.getLowerLeftY(), + bbox.getUpperRightX(), bbox.getUpperRightY()); + } } } if (cidFont != null) @@ -316,6 +320,11 @@ public GeneralPath getPath(int code) throws IOException { int cid = codeToCID(code); + if (cid2gid != null && isEmbedded) + { + // PDFBOX-4093: despite being a type 0 font, there is a CIDToGIDMap + cid = cid2gid[cid]; + } Type2CharString charstring = getType2CharString(cid); if (charstring != null) { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType2Embedder.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType2Embedder.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType2Embedder.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType2Embedder.java 2018-12-03 16:18:13.000000000 +0000 @@ -28,7 +28,7 @@ import java.util.Set; import java.util.TreeSet; -import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.fontbox.ttf.*; import org.sejda.sambox.cos.COSArray; import org.sejda.sambox.cos.COSDictionary; import org.sejda.sambox.cos.COSInteger; @@ -36,6 +36,8 @@ import org.sejda.sambox.pdmodel.PDDocument; import org.sejda.sambox.pdmodel.common.PDStream; import org.sejda.sambox.util.SpecVersionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Embedded PDCIDFontType2 builder. Helper class to populate a PDCIDFontType2 and its parent PDType0Font from a TTF. @@ -45,10 +47,14 @@ */ final class PDCIDFontType2Embedder extends TrueTypeEmbedder { + + private static final Logger LOG = LoggerFactory.getLogger(PDCIDFontType2Embedder.class); + private final PDDocument document; private final PDType0Font parent; private final COSDictionary dict; private final COSDictionary cidFont; + private final boolean vertical; /** * Creates a new TrueType font embedder for the given TTF as a PDCIDFontType2. @@ -60,17 +66,18 @@ * @throws IOException if the TTF could not be read */ PDCIDFontType2Embedder(PDDocument document, COSDictionary dict, TrueTypeFont ttf, - boolean embedSubset, PDType0Font parent) throws IOException + boolean embedSubset, PDType0Font parent, boolean vertical) throws IOException { super(dict, ttf, embedSubset); this.document = document; this.dict = dict; this.parent = parent; + this.vertical = vertical; // parent Type 0 font dict.setItem(COSName.SUBTYPE, COSName.TYPE0); dict.setName(COSName.BASE_FONT, fontDescriptor.getFontName()); - dict.setItem(COSName.ENCODING, COSName.IDENTITY_H); // CID = GID + dict.setItem(COSName.ENCODING, vertical ? COSName.IDENTITY_V : COSName.IDENTITY_H); // CID = GID // descendant CIDFont cidFont = createCIDFont(); @@ -106,6 +113,11 @@ // build unicode mapping before subsetting as the subsetted font won't have a cmap buildToUnicodeCMap(gidToCid); + // build vertical metrics before subsetting as the subsetted font won't have vhea, vmtx + if (vertical) + { + buildVerticalMetrics(cidToGid); + } // rebuild the relevant part of the font buildFontFile2(ttfSubset); @@ -137,7 +149,7 @@ } // skip composite glyph components that have no code point - List codes = cmap.getCharCodes(cid); // old GID -> Unicode + List codes = cmapLookup.getCharCodes(cid); // old GID -> Unicode if (codes != null) { // use the first entry even for ambiguous mappings @@ -195,6 +207,12 @@ // W - widths buildWidths(cidFont); + // Vertical metrics + if (vertical) + { + buildVerticalMetrics(cidFont); + } + // CIDToGIDMap cidFont.setItem(COSName.CID_TO_GID_MAP, COSName.IDENTITY); @@ -229,10 +247,8 @@ out.write(new byte[] { (byte) (gid >> 8 & 0xff), (byte) (gid & 0xff) }); } - byte[] byteArray = out.toByteArray(); - InputStream input = new ByteArrayInputStream(byteArray); + InputStream input = new ByteArrayInputStream(out.toByteArray()); PDStream stream = new PDStream(input, COSName.FLATE_DECODE); - stream.getCOSObject().setInt(COSName.LENGTH1, byteArray.length); cidFont.setItem(COSName.CID_TO_GID_MAP, stream); } @@ -293,6 +309,90 @@ cidFont.setItem(COSName.W, widths); } + private boolean buildVerticalHeader(COSDictionary cidFont) throws IOException + { + VerticalHeaderTable vhea = ttf.getVerticalHeader(); + if (vhea == null) + { + LOG.warn("Font to be subset is set to vertical, but has no 'vhea' table"); + return false; + } + + float scaling = 1000f / ttf.getHeader().getUnitsPerEm(); + + long v = Math.round(vhea.getAscender() * scaling); + long w1 = Math.round(-vhea.getAdvanceHeightMax() * scaling); + if (v != 880 || w1 != -1000) + { + COSArray cosDw2 = new COSArray(); + cosDw2.add(COSInteger.get(v)); + cosDw2.add(COSInteger.get(w1)); + cidFont.setItem(COSName.DW2, cosDw2); + } + return true; + } + + /** + * Builds vertical metrics with a custom CIDToGIDMap (for embedding font subset). + */ + private void buildVerticalMetrics(Map cidToGid) throws IOException + { + // The "vhea" and "vmtx" tables that specify vertical metrics shall never be used by a conforming + // reader. The only way to specify vertical metrics in PDF shall be by means of the DW2 and W2 + // entries in a CIDFont dictionary. + + if (!buildVerticalHeader(cidFont)) + { + return; + } + + float scaling = 1000f / ttf.getHeader().getUnitsPerEm(); + + VerticalHeaderTable vhea = ttf.getVerticalHeader(); + VerticalMetricsTable vmtx = ttf.getVerticalMetrics(); + GlyphTable glyf = ttf.getGlyph(); + HorizontalMetricsTable hmtx = ttf.getHorizontalMetrics(); + + long v_y = Math.round(vhea.getAscender() * scaling); + long w1 = Math.round(-vhea.getAdvanceHeightMax() * scaling); + + COSArray heights = new COSArray(); + COSArray w2 = new COSArray(); + int prev = Integer.MIN_VALUE; + // Use a sorted list to get an optimal width array + Set keys = new TreeSet(cidToGid.keySet()); + for (int cid : keys) + { + // Unlike buildWidths, we look up with cid (not gid) here because this is + // the original TTF, not the rebuilt one. + GlyphData glyph = glyf.getGlyph(cid); + if (glyph == null) + { + continue; + } + long height = Math.round((glyph.getYMaximum() + vmtx.getTopSideBearing(cid)) * scaling); + long advance = Math.round(-vmtx.getAdvanceHeight(cid) * scaling); + if (height == v_y && advance == w1) + { + // skip default metrics + continue; + } + // c [w1_1y v_1x v_1y w1_2y v_2x v_2y ... w1_ny v_nx v_ny] + if (prev != cid - 1) + { + w2 = new COSArray(); + heights.add(COSInteger.get(cid)); // c + heights.add(w2); + } + w2.add(COSInteger.get(advance)); // w1_iy + long width = Math.round(hmtx.getAdvanceWidth(cid) * scaling); + w2.add(COSInteger.get(width / 2)); // v_ix + w2.add(COSInteger.get(height)); // v_iy + prev = cid; + } + cidFont.setItem(COSName.W2, heights); + } + /** * Build widths with Identity CIDToGIDMap (for embedding full font). */ @@ -326,7 +426,7 @@ long lastCid = widths[0]; long lastValue = Math.round(widths[1] * scaling); - COSArray inner = null; + COSArray inner = new COSArray(); COSArray outer = new COSArray(); outer.add(COSInteger.get(lastCid)); @@ -334,7 +434,7 @@ for (int i = 2; i < widths.length; i += 2) { - long cid = widths[i]; + long cid = widths[i]; long value = Math.round(widths[i + 1] * scaling); switch (state) @@ -408,6 +508,160 @@ break; } return outer; + } + + /** + * Build vertical metrics with Identity CIDToGIDMap (for embedding full font). + */ + private void buildVerticalMetrics(COSDictionary cidFont) throws IOException + { + if (!buildVerticalHeader(cidFont)) + { + return; + } + + int cidMax = ttf.getNumberOfGlyphs(); + int[] gidMetrics = new int[cidMax * 4]; + for (int cid = 0; cid < cidMax; cid++) + { + GlyphData glyph = ttf.getGlyph().getGlyph(cid); + if (glyph == null) + { + gidMetrics[cid * 4] = Integer.MIN_VALUE; + } + else + { + gidMetrics[cid * 4] = cid; + gidMetrics[cid * 4 + 1] = ttf.getVerticalMetrics().getAdvanceHeight(cid); + gidMetrics[cid * 4 + 2] = ttf.getHorizontalMetrics().getAdvanceWidth(cid); + gidMetrics[cid * 4 + 3] = glyph.getYMaximum() + ttf.getVerticalMetrics().getTopSideBearing(cid); + } + } + + cidFont.setItem(COSName.W2, getVerticalMetrics(gidMetrics)); + } + + private COSArray getVerticalMetrics(int[] values) throws IOException + { + if (values.length == 0) + { + throw new IllegalArgumentException("length of values must be > 0"); + } + + float scaling = 1000f / ttf.getHeader().getUnitsPerEm(); + + long lastCid = values[0]; + long lastW1Value = Math.round(-values[1] * scaling); + long lastVxValue = Math.round(values[2] * scaling / 2f); + long lastVyValue = Math.round(values[3] * scaling); + + COSArray inner = new COSArray(); + COSArray outer = new COSArray(); + outer.add(COSInteger.get(lastCid)); + + State state = State.FIRST; + + for (int i = 4; i < values.length; i += 4) + { + long cid = values[i]; + if (cid == Integer.MIN_VALUE) + { + // no glyph for this cid + continue; + } + long w1Value = Math.round(-values[i + 1] * scaling); + long vxValue = Math.round(values[i + 2] * scaling / 2); + long vyValue = Math.round(values[i + 3] * scaling); + + switch (state) + { + case FIRST: + if (cid == lastCid + 1 && w1Value == lastW1Value && vxValue == lastVxValue && vyValue == lastVyValue) + { + state = State.SERIAL; + } + else if (cid == lastCid + 1) + { + state = State.BRACKET; + inner = new COSArray(); + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + } + else + { + inner = new COSArray(); + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + outer.add(inner); + outer.add(COSInteger.get(cid)); + } + break; + case BRACKET: + if (cid == lastCid + 1 && w1Value == lastW1Value && vxValue == lastVxValue && vyValue == lastVyValue) + { + state = State.SERIAL; + outer.add(inner); + outer.add(COSInteger.get(lastCid)); + } + else if (cid == lastCid + 1) + { + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + } + else + { + state = State.FIRST; + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + outer.add(inner); + outer.add(COSInteger.get(cid)); + } + break; + case SERIAL: + if (cid != lastCid + 1 || w1Value != lastW1Value || vxValue != lastVxValue || vyValue != lastVyValue) + { + outer.add(COSInteger.get(lastCid)); + outer.add(COSInteger.get(lastW1Value)); + outer.add(COSInteger.get(lastVxValue)); + outer.add(COSInteger.get(lastVyValue)); + outer.add(COSInteger.get(cid)); + state = State.FIRST; + } + break; + } + lastW1Value = w1Value; + lastVxValue = vxValue; + lastVyValue = vyValue; + lastCid = cid; + } + + switch (state) + { + case FIRST: + inner = new COSArray(); + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + outer.add(inner); + break; + case BRACKET: + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + outer.add(inner); + break; + case SERIAL: + outer.add(COSInteger.get(lastCid)); + outer.add(COSInteger.get(lastW1Value)); + outer.add(COSInteger.get(lastVxValue)); + outer.add(COSInteger.get(lastVyValue)); + break; + } + return outer; } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType2.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType2.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType2.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDCIDFontType2.java 2018-12-03 16:18:13.000000000 +0000 @@ -20,28 +20,23 @@ import java.awt.geom.GeneralPath; import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; import org.apache.fontbox.cff.Type2CharString; import org.apache.fontbox.cmap.CMap; -import org.apache.fontbox.ttf.CmapSubtable; +import org.apache.fontbox.ttf.CmapLookup; import org.apache.fontbox.ttf.GlyphData; import org.apache.fontbox.ttf.OTFParser; import org.apache.fontbox.ttf.OpenTypeFont; import org.apache.fontbox.ttf.TrueTypeFont; import org.apache.fontbox.util.BoundingBox; -import org.sejda.sambox.cos.COSBase; import org.sejda.sambox.cos.COSDictionary; -import org.sejda.sambox.cos.COSName; -import org.sejda.sambox.cos.COSStream; import org.sejda.sambox.pdmodel.common.PDRectangle; import org.sejda.sambox.pdmodel.common.PDStream; import org.sejda.sambox.util.Matrix; import org.sejda.sambox.util.ReflectionUtils; -import org.sejda.util.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,9 +51,10 @@ private final TrueTypeFont ttf; private final int[] cid2gid; + private final HashMap gid2cid = new HashMap(); private final boolean isEmbedded; private final boolean isDamaged; - private final CmapSubtable cmap; // may be null + private final CmapLookup cmap; // may be null private Matrix fontMatrix; private BoundingBox fontBBox; @@ -157,8 +153,17 @@ } ttf = ttfFont; } - cmap = ttf.getUnicodeCmap(false); + cmap = ttf.getUnicodeCmapLookup(false); cid2gid = readCIDToGIDMap(); + if(cid2gid != null) { + for (int cid = 0; cid < cid2gid.length; cid++) + { + int gid = cid2gid[cid]; + if(gid != 0) { + gid2cid.put(gid, cid); + } + } + } } private TrueTypeFont findFontOrSubstitute() throws IOException @@ -209,38 +214,20 @@ if (getFontDescriptor() != null) { PDRectangle bbox = getFontDescriptor().getFontBoundingBox(); - if (nonNull(bbox) && bbox.getLowerLeftX() != 0 || bbox.getLowerLeftY() != 0 - || bbox.getUpperRightX() != 0 || bbox.getUpperRightY() != 0) + if(nonNull(bbox)) { - return new BoundingBox(bbox.getLowerLeftX(), bbox.getLowerLeftY(), - bbox.getUpperRightX(), bbox.getUpperRightY()); + if ((Float.compare(bbox.getLowerLeftX(), 0) != 0 || + Float.compare(bbox.getLowerLeftY(), 0) != 0 || + Float.compare(bbox.getUpperRightX(), 0) != 0 || + Float.compare(bbox.getUpperRightY(), 0) != 0)) + { + return new BoundingBox(bbox.getLowerLeftX(), bbox.getLowerLeftY(), + bbox.getUpperRightX(), bbox.getUpperRightY()); + } } - } - return ttf.getFontBBox(); - } - private int[] readCIDToGIDMap() throws IOException - { - int[] cid2gid = null; - COSBase map = dict.getDictionaryObject(COSName.CID_TO_GID_MAP); - if (map instanceof COSStream) - { - COSStream stream = (COSStream) map; - - InputStream is = stream.getUnfilteredStream(); - byte[] mapAsBytes = IOUtils.toByteArray(is); - IOUtils.closeQuietly(is); - int numberOfInts = mapAsBytes.length / 2; - cid2gid = new int[numberOfInts]; - int offset = 0; - for (int index = 0; index < numberOfInts; index++) - { - int gid = (mapAsBytes[offset] & 0xff) << 8 | mapAsBytes[offset + 1] & 0xff; - cid2gid[index] = gid; - offset += 2; - } } - return cid2gid; + return ttf.getFontBBox(); } @Override @@ -354,7 +341,11 @@ { if (cmap != null) { - cid = cmap.getGlyphId(unicode); + int gid = cmap.getGlyphId(unicode); + // SAMBOX specific here + // if there's a gid to cid mapping, use it. + // otherwise fallback to the old behaviour, which is to assume cid = gid + cid = gid2cid.getOrDefault(gid, gid); } } else diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDFont.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDFont.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDFont.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDFont.java 2018-12-03 16:18:13.000000000 +0000 @@ -19,7 +19,6 @@ import static java.util.Objects.nonNull; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; @@ -32,6 +31,7 @@ import org.apache.fontbox.afm.FontMetrics; import org.apache.fontbox.cmap.CMap; import org.apache.fontbox.util.BoundingBox; +import org.sejda.io.FastByteArrayOutputStream; import org.sejda.sambox.cos.COSArray; import org.sejda.sambox.cos.COSArrayList; import org.sejda.sambox.cos.COSBase; @@ -51,7 +51,7 @@ * * @author Ben Litchfield */ -public abstract class PDFont implements COSObjectable, PDFontLike +public abstract class PDFont implements COSObjectable, PDFontLike, Subsettable { private static final Logger LOG = LoggerFactory.getLogger(PDFont.class); protected static final Matrix DEFAULT_FONT_MATRIX = new Matrix(0.001f, 0, 0, 0.001f, 0, 0); @@ -312,8 +312,9 @@ */ public final byte[] encode(String text) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - for (int offset = 0; offset < text.length();) + FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + int offset = 0; + while (offset < text.length()) { int codePoint = text.codePointAt(offset); @@ -327,6 +328,40 @@ } /** + * Similar to encode() but handles leniently cases where fonts don't have a glyph by assuming the identity mapping + */ + public final byte[] encodeLeniently(String text) throws IOException + { + FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + for (int offset = 0; offset < text.length();) + { + int codePoint = text.codePointAt(offset); + + // multi-byte encoding with 1 to 4 bytes + byte[] bytes; + try + { + bytes = encode(codePoint); + } + catch (IllegalArgumentException e) + { + if (e.getMessage().contains("No glyph")) + { + bytes = new byte[] { (byte) codePoint }; + } + else + { + throw e; + } + } + out.write(bytes); + + offset += Character.charCount(codePoint); + } + return out.toByteArray(); + } + + /** * Encodes the given Unicode code point for use in a PDF content stream. Content streams use a multi-byte encoding * with 1 to 4 bytes. * @@ -362,6 +397,27 @@ } /** + * Similar to getStringWidth() but handles leniently fonts where glyphs are missing, assuming the identity mapping + * of glyphs + * + * Uses encodeLeniently() instead of encode() + */ + public float getStringWidthLeniently(String text) throws IOException + { + byte[] bytes = encodeLeniently(text); + ByteArrayInputStream in = new ByteArrayInputStream(bytes); + + float width = 0; + while (in.available() > 0) + { + int code = readCode(in); + width += getWidth(code); + } + + return width; + } + + /** * This will get the average font width for all characters. * * @return The width is in 1000 unit of text space, ie 333 or 777 @@ -439,7 +495,8 @@ if (toUnicodeCMap != null) { if (toUnicodeCMap.getName() != null && toUnicodeCMap.getName().startsWith("Identity-") - && dict.getDictionaryObject(COSName.TO_UNICODE) instanceof COSName) + && (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 @@ -575,25 +632,6 @@ return Standard14Fonts.containsName(getName()); } - /** - * Adds the given Unicode point to the subset. - * - * @param codePoint Unicode code point - */ - public abstract void addToSubset(int codePoint); - - /** - * Replaces this font with a subset containing only the given Unicode characters. - * - * @throws IOException if the subset could not be written - */ - public abstract void subset() throws IOException; - - /** - * Returns true if this font will be subset when embedded. - */ - public abstract boolean willBeSubset(); - @Override public abstract boolean isDamaged(); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDFontLike.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDFontLike.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDFontLike.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDFontLike.java 2018-12-03 16:18:13.000000000 +0000 @@ -89,6 +89,15 @@ float getWidth(int code) throws IOException; /** + * Returns true if the Font dictionary specifies an explicit width for the given glyph. + * This includes Width, W but not default widths entries. + * + * @param code character code + * @throws IOException if the font could not be read + */ + boolean hasExplicitWidth(int code) throws IOException; + + /** * Returns the width of a glyph in the embedded font file. * * @param code character code diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDPanoseClassification.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDPanoseClassification.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDPanoseClassification.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDPanoseClassification.java 2018-12-03 16:18:13.000000000 +0000 @@ -90,7 +90,7 @@ @Override public String toString() { - return "{ FamilyType = " + getFamilyKind() + ", " + "SerifStyle = " + getSerifStyle() + ", " + return "{ FamilyKind = " + getFamilyKind() + ", " + "SerifStyle = " + getSerifStyle() + ", " + "Weight = " + getWeight() + ", " + "Proportion = " + getProportion() + ", " + "Contrast = " + getContrast() + ", " + "StrokeVariation = " + getStrokeVariation() + ", " + "ArmStyle = " + getArmStyle() + ", " + "Letterform = " + getLetterform() diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDSimpleFont.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDSimpleFont.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDSimpleFont.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDSimpleFont.java 2018-12-03 16:18:13.000000000 +0000 @@ -424,4 +424,18 @@ { return false; } + + @Override + public boolean hasExplicitWidth(int code) throws IOException + { + if (dict.containsKey(COSName.WIDTHS)) + { + int firstChar = dict.getInt(COSName.FIRST_CHAR, -1); + if (code >= firstChar && code - firstChar < getWidths().size()) + { + return true; + } + } + return false; + } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDTrueTypeFontEmbedder.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDTrueTypeFontEmbedder.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDTrueTypeFontEmbedder.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDTrueTypeFontEmbedder.java 2018-12-03 16:18:13.000000000 +0000 @@ -99,7 +99,7 @@ { String uni = glyphList.toUnicode(name); int charCode = uni.codePointAt(0); - int gid = cmap.getGlyphId(charCode); + int gid = cmapLookup.getGlyphId(charCode); widths.set(entry.getKey() - firstChar, Math.round(hmtx.getAdvanceWidth(gid) * scaling)); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDType0Font.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDType0Font.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDType0Font.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDType0Font.java 2018-12-03 16:18:13.000000000 +0000 @@ -66,7 +66,7 @@ */ public static PDType0Font load(PDDocument doc, File file) throws IOException { - return new PDType0Font(doc, new TTFParser().parse(file), true, true); + return new PDType0Font(doc, new TTFParser().parse(file), true, true, false); } /** @@ -79,7 +79,7 @@ */ public static PDType0Font load(PDDocument doc, InputStream input) throws IOException { - return new PDType0Font(doc, new TTFParser().parse(input), true, true); + return new PDType0Font(doc, new TTFParser().parse(input), true, true, false); } /** @@ -94,7 +94,7 @@ public static PDType0Font load(PDDocument doc, InputStream input, boolean embedSubset) throws IOException { - return new PDType0Font(doc, new TTFParser().parse(input), embedSubset, true); + return new PDType0Font(doc, new TTFParser().parse(input), embedSubset, true, false); } /** @@ -109,7 +109,63 @@ public static PDType0Font load(PDDocument doc, TrueTypeFont ttf, boolean embedSubset) throws IOException { - return new PDType0Font(doc, ttf, embedSubset, false); + return new PDType0Font(doc, ttf, embedSubset, false, false); + } + + /** + * Loads a TTF to be embedded into a document as a vertical Type 0 font. + * + * @param doc The PDF document that will hold the embedded font. + * @param file A TrueType font. + * @return A Type0 font with a CIDFontType2 descendant. + * @throws IOException If there is an error reading the font file. + */ + public static PDType0Font loadVertical(PDDocument doc, File file) throws IOException + { + return new PDType0Font(doc, new TTFParser().parse(file), true, true, true); + } + + /** + * Loads a TTF to be embedded into a document as a vertical Type 0 font. + * + * @param doc The PDF document that will hold the embedded font. + * @param input A TrueType font. + * @return A Type0 font with a CIDFontType2 descendant. + * @throws IOException If there is an error reading the font stream. + */ + public static PDType0Font loadVertical(PDDocument doc, InputStream input) throws IOException + { + return new PDType0Font(doc, new TTFParser().parse(input), true, true, true); + } + + /** + * Loads a TTF to be embedded into a document as a vertical Type 0 font. + * + * @param doc The PDF document that will hold the embedded font. + * @param input A TrueType font. + * @param embedSubset True if the font will be subset before embedding + * @return A Type0 font with a CIDFontType2 descendant. + * @throws IOException If there is an error reading the font stream. + */ + public static PDType0Font loadVertical(PDDocument doc, InputStream input, boolean embedSubset) + throws IOException + { + return new PDType0Font(doc, new TTFParser().parse(input), embedSubset, true, true); + } + + /** + * Loads a TTF to be embedded into a document as a vertical Type 0 font. + * + * @param doc The PDF document that will hold the embedded font. + * @param ttf A TrueType font. + * @param embedSubset True if the font will be subset before embedding + * @return A Type0 font with a CIDFontType2 descendant. + * @throws IOException If there is an error reading the font stream. + */ + public static PDType0Font loadVertical(PDDocument doc, TrueTypeFont ttf, boolean embedSubset) + throws IOException + { + return new PDType0Font(doc, ttf, embedSubset, false, true); } /** @@ -142,21 +198,32 @@ } /** - * Private. Creates a new TrueType 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) - throws IOException + boolean closeTTF, boolean vertical) throws IOException { - embedder = new PDCIDFontType2Embedder(document, dict, ttf, embedSubset, this); + if (vertical) + { + ttf.enableVerticalSubstitutions(); + } + embedder = new PDCIDFontType2Embedder(document, dict, ttf, embedSubset, this, vertical); descendantFont = embedder.getCIDFont(); readEncoding(); fetchCMapUCS2(); - if (closeOnSubset) + if (closeTTF) { if (embedSubset) { this.ttf = ttf; + document.registerTrueTypeFontForClosing(ttf); } else { @@ -344,6 +411,12 @@ } @Override + public boolean hasExplicitWidth(int code) throws IOException + { + return descendantFont.hasExplicitWidth(code); + } + + @Override public float getAverageFontWidth() { return descendantFont.getAverageFontWidth(); @@ -483,7 +556,8 @@ { descendant = getDescendantFont().getClass().getSimpleName(); } - return getClass().getSimpleName() + "/" + descendant + " " + getBaseFont(); + return getClass().getSimpleName() + "/" + descendant + ", PostScript name: " + + getBaseFont(); } @Override diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDType1FontEmbedder.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDType1FontEmbedder.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDType1FontEmbedder.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDType1FontEmbedder.java 2018-12-03 16:18:13.000000000 +0000 @@ -99,6 +99,7 @@ dict.setInt(COSName.FIRST_CHAR, 0); dict.setInt(COSName.LAST_CHAR, 255); dict.setItem(COSName.WIDTHS, COSArrayList.converterToCOSArray(widths)); + dict.setItem(COSName.ENCODING, encoding); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDType1Font.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDType1Font.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDType1Font.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDType1Font.java 2018-12-03 16:18:13.000000000 +0000 @@ -442,7 +442,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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDType3Font.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDType3Font.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/PDType3Font.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/PDType3Font.java 2018-12-03 16:18:13.000000000 +0000 @@ -37,6 +37,8 @@ import org.sejda.sambox.pdmodel.font.encoding.GlyphList; import org.sejda.sambox.util.Matrix; import org.sejda.sambox.util.Vector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A PostScript Type 3 Font. @@ -45,6 +47,8 @@ */ public class PDType3Font extends PDSimpleFont { + private static final Logger LOG = LoggerFactory.getLogger(PDType3Font.class); + private PDResources resources; private COSDictionary charProcs; private Matrix fontMatrix; @@ -65,8 +69,20 @@ @Override protected final void readEncoding() { - encoding = new DictionaryEncoding( - dict.getDictionaryObject(COSName.ENCODING, COSDictionary.class)); + 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(); } @@ -243,12 +259,8 @@ */ public PDRectangle getFontBBox() { - COSArray rect = (COSArray) dict.getDictionaryObject(COSName.FONT_BBOX); - if (nonNull(rect)) - { - return new PDRectangle(rect); - } - return null; + return ofNullable(dict.getDictionaryObject(COSName.FONT_BBOX, COSArray.class)) + .map(PDRectangle::new).orElse(null); } @Override @@ -317,15 +329,8 @@ public PDType3CharProc getCharProc(int code) { String name = getEncoding().getName(code); - if (!".notdef".equals(name)) - { - COSStream stream = getCharProcs().getDictionaryObject(COSName.getPDFName(name), - COSStream.class); - if (nonNull(stream)) - { - return new PDType3CharProc(this, stream); - } - } - return null; + return ofNullable( + getCharProcs().getDictionaryObject(COSName.getPDFName(name), COSStream.class)) + .map((s) -> new PDType3CharProc(this, s)).orElse(null); } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/Standard14Fonts.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/Standard14Fonts.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/Standard14Fonts.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/Standard14Fonts.java 2018-12-03 16:18:13.000000000 +0000 @@ -88,6 +88,12 @@ addAFM("Times,Italic", "Times-Italic"); addAFM("Times,Bold", "Times-Bold"); addAFM("Times,BoldItalic", "Times-BoldItalic"); + + // PDFBOX-3457: PDF.js file bug864847.pdf + addAFM("ArialMT", "Helvetica"); + addAFM("Arial-ItalicMT", "Helvetica-Oblique"); + addAFM("Arial-BoldMT", "Helvetica-Bold"); + addAFM("Arial-BoldItalicMT", "Helvetica-BoldOblique"); } catch (IOException e) { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/Subsettable.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/Subsettable.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/Subsettable.java 1970-01-01 00:00:00.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/Subsettable.java 2018-12-03 16:18:13.000000000 +0000 @@ -0,0 +1,46 @@ +/* + * 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.sejda.sambox.pdmodel.font; + +import java.io.IOException; + +/** + * A subsettable + * + * @author Andrea Vacondio + */ +public interface Subsettable +{ + /** + * Adds the given Unicode point to the subset. + * + * @param codePoint Unicode code point + */ + void addToSubset(int codePoint); + + /** + * Replaces this font with a subset containing only the given Unicode characters. + * + * @throws IOException if the subset could not be written + */ + void subset() throws IOException; + + /** + * @return true if this font will be subset when embedded. + */ + boolean willBeSubset(); +} diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/ToUnicodeWriter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/ToUnicodeWriter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/ToUnicodeWriter.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/ToUnicodeWriter.java 2018-12-03 16:18:13.000000000 +0000 @@ -36,10 +36,15 @@ */ final class ToUnicodeWriter { - private final Map cidToUnicode = new TreeMap(); + private final Map cidToUnicode = new TreeMap<>(); 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() @@ -127,10 +132,10 @@ int cid = entry.getKey(); String text = entry.getValue(); - if (cid == srcPrev + 1 && // CID must be last CID + 1 - dstPrev.codePointCount(0, dstPrev.length()) == 1 && // no UTF-16 surrogates - text.codePointAt(0) == dstPrev.codePointAt(0) + 1 && // dstString must be prev + 1 - dstPrev.codePointAt(0) + 1 <= 255 - (cid - srcCode1)) // increment last byte only + if (cid == srcPrev + 1 && // CID must be last CID + 1 + dstPrev.codePointCount(0, dstPrev.length()) == 1 && // no UTF-16 surrogates + text.codePointAt(0) == dstPrev.codePointAt(0) + 1 && // dstString must be prev + 1 + dstPrev.codePointAt(0) + 1 <= 255 - (cid - srcCode1)) // increment last byte only { // extend range srcTo.set(srcTo.size() - 1, cid); @@ -147,15 +152,16 @@ 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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/TrueTypeEmbedder.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/TrueTypeEmbedder.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/font/TrueTypeEmbedder.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/font/TrueTypeEmbedder.java 2018-12-03 16:18:13.000000000 +0000 @@ -17,17 +17,20 @@ package org.sejda.sambox.pdmodel.font; +import static org.sejda.sambox.pdmodel.font.FontUtils.getTag; +import static org.sejda.sambox.pdmodel.font.FontUtils.isEmbeddingPermitted; +import static org.sejda.sambox.pdmodel.font.FontUtils.isSubsettingPermitted; + import java.awt.geom.GeneralPath; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; - +import org.apache.fontbox.ttf.CmapLookup; import org.apache.fontbox.ttf.CmapSubtable; import org.apache.fontbox.ttf.HeaderTable; import org.apache.fontbox.ttf.HorizontalHeaderTable; @@ -41,7 +44,6 @@ import org.sejda.sambox.pdmodel.common.PDRectangle; import org.sejda.sambox.pdmodel.common.PDStream; import org.sejda.util.IOUtils; - /** * Common functionality for embedding TrueType fonts. * @@ -52,11 +54,13 @@ { private static final int ITALIC = 1; private static final int OBLIQUE = 512; - private static final String BASE25 = "BCDEFGHIJKLMNOPQRSTUVWXYZ"; protected TrueTypeFont ttf; protected PDFontDescriptor fontDescriptor; + @Deprecated protected final CmapSubtable cmap; + + protected final CmapLookup cmapLookup; private final Set subsetCodePoints = new HashSet<>(); private final boolean embedSubset; @@ -86,6 +90,7 @@ // choose a Unicode "cmap" cmap = ttf.getUnicodeCmap(); + cmapLookup = ttf.getUnicodeCmapLookup(); } public void buildFontFile2(InputStream ttfStream) throws IOException @@ -115,48 +120,6 @@ fontDescriptor.setFontFile2(stream); } - /** - * Returns true if the fsType in the OS/2 table permits embedding. - */ - private boolean isEmbeddingPermitted(TrueTypeFont ttf) throws IOException - { - if (ttf.getOS2Windows() != null) - { - int fsType = ttf.getOS2Windows().getFsType(); - int exclusive = fsType & 0x8; // bits 0-3 are a set of exclusive bits - - if ((exclusive - & OS2WindowsMetricsTable.FSTYPE_RESTRICTED) == OS2WindowsMetricsTable.FSTYPE_RESTRICTED) - { - // restricted License embedding - return false; - } - else if ((exclusive - & OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) == OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) - { - // bitmap embedding only - return false; - } - } - return true; - } - - /** - * Returns true if the fsType in the OS/2 table permits subsetting. - */ - private boolean isSubsettingPermitted(TrueTypeFont ttf) throws IOException - { - if (ttf.getOS2Windows() != null) - { - int fsType = ttf.getOS2Windows().getFsType(); - if ((fsType - & OS2WindowsMetricsTable.FSTYPE_NO_SUBSETTING) == OS2WindowsMetricsTable.FSTYPE_NO_SUBSETTING) - { - return false; - } - } - return true; - } /** * Creates a new font descriptor dictionary for the given TTF. @@ -288,21 +251,9 @@ } // PDF spec required tables (if present), all others will be removed - List tables = new ArrayList(); - tables.add("head"); - tables.add("hhea"); - tables.add("loca"); - tables.add("maxp"); - tables.add("cvt "); - tables.add("prep"); - tables.add("glyf"); - tables.add("hmtx"); - tables.add("fpgm"); - // Windows ClearType - tables.add("gasp"); - // set the GIDs to subset - TTFSubsetter subsetter = new TTFSubsetter(ttf, tables); + TTFSubsetter subsetter = new TTFSubsetter(ttf, Arrays.asList("head", "hhea", "loca", "maxp", + "cvt", "prep", "glyf", "hmtx", "fpgm", "gasp")); subsetter.addAll(subsetCodePoints); // calculate deterministic tag based on the chosen subset @@ -320,7 +271,7 @@ } /** - * Returns true if the font needs to be subset. + * @return true if the font needs to be subset. */ public boolean needsSubset() { @@ -328,36 +279,10 @@ } /** - * Rebuild a font subset. + * @return a font subset. */ protected abstract void buildSubset(InputStream ttfSubset, String tag, Map gidToCid) throws IOException; - /** - * Returns an uppercase 6-character unique tag for the given subset. - */ - public String getTag(Map gidToCid) - { - // deterministic - long num = gidToCid.hashCode(); - - // base25 encode - StringBuilder sb = new StringBuilder(); - do - { - long div = num / 25; - int mod = (int) (num % 25); - sb.append(BASE25.charAt(mod)); - num = div; - } while (num != 0 && sb.length() < 6); - // pad - while (sb.length() < 6) - { - sb.insert(0, 'A'); - } - - sb.append('+'); - return sb.toString(); - } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/blend/BlendComposite.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/blend/BlendComposite.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/blend/BlendComposite.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/blend/BlendComposite.java 2018-12-03 16:18:13.000000000 +0000 @@ -35,9 +35,6 @@ */ public final class BlendComposite implements Composite { - /** - * Log instance. - */ private static final Logger LOG = LoggerFactory.getLogger(BlendComposite.class); /** @@ -45,6 +42,7 @@ * * @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) { @@ -62,11 +60,12 @@ { return AlphaComposite.getInstance(AlphaComposite.SRC_OVER, constantAlpha); } - return new BlendComposite(blendMode, constantAlpha); + else + { + return new BlendComposite(blendMode, constantAlpha); + } } - // TODO - non-separable blending modes - private final BlendMode blendMode; private final float constantAlpha; @@ -81,21 +80,18 @@ public CompositeContext createContext(ColorModel srcColorModel, ColorModel dstColorModel, RenderingHints hints) { - return new BlendCompositeContext(srcColorModel, dstColorModel, hints); + return new BlendCompositeContext(srcColorModel, dstColorModel); } class BlendCompositeContext implements CompositeContext { private final ColorModel srcColorModel; private final ColorModel dstColorModel; - private final RenderingHints hints; - BlendCompositeContext(ColorModel srcColorModel, ColorModel dstColorModel, - RenderingHints hints) + BlendCompositeContext(ColorModel srcColorModel, ColorModel dstColorModel) { this.srcColorModel = srcColorModel; this.dstColorModel = dstColorModel; - this.hints = hints; } @Override @@ -127,13 +123,16 @@ int numDstComponents = dstIn.getNumBands(); boolean dstHasAlpha = (numDstComponents > numDstColorComponents); - int colorSpaceType = dstColorSpace.getType(); - boolean subtractive = (colorSpaceType != ColorSpace.TYPE_RGB) - && (colorSpaceType != ColorSpace.TYPE_GRAY); + 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); @@ -146,6 +145,8 @@ 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++) { @@ -167,21 +168,21 @@ 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) { + // 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]; @@ -207,7 +208,50 @@ } else { - // TODO - nonseparable modes + // 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) @@ -220,10 +264,5 @@ } } } - - public RenderingHints getHints() - { - return hints; - } } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/blend/BlendMode.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/blend/BlendMode.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/blend/BlendMode.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/blend/BlendMode.java 2018-12-03 16:18:13.000000000 +0000 @@ -30,42 +30,6 @@ */ public abstract class BlendMode { - 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 @@ -128,7 +92,16 @@ @Override public float blendChannel(float srcValue, float dstValue) { - return (srcValue < 1) ? Math.min(1, dstValue / (1 - srcValue)) : 1; + // See PDF 2.0 specification + if (dstValue == 0) + { + return 0; + } + if (dstValue >= 1 - srcValue) + { + return 1; + } + return dstValue / (1 - srcValue); } }; @@ -137,7 +110,16 @@ @Override public float blendChannel(float srcValue, float dstValue) { - return (srcValue > 0) ? 1 - Math.min(1, (1 - dstValue) / srcValue) : 0; + // See PDF 2.0 specification + if (dstValue == 1) + { + return 1; + } + if (1 - dstValue >= srcValue) + { + return 0; + } + return 1 - (1 - dstValue) / srcValue; } }; @@ -184,14 +166,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); @@ -203,7 +386,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; } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDCalGray.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDCalGray.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDCalGray.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDCalGray.java 2018-12-03 16:18:13.000000000 +0000 @@ -21,6 +21,9 @@ import org.sejda.sambox.cos.COSName; import org.sejda.sambox.cos.COSNumber; +import java.util.HashMap; +import java.util.Map; + /** * A CalGray colour space is a special case of a single-component CIE-based * colour space. @@ -32,6 +35,11 @@ { private final PDColor initialColor = new PDColor(new float[] { 0 }, this); + // PDFBOX-4119: cache the results for much improved performance + // cached values MUST be cloned, because they are modified by the caller. + // this can be observed in rendering of PDFBOX-1724 + private final Map map1 = new HashMap(); + /** * Create a new CalGray color space. */ @@ -77,13 +85,20 @@ @Override public float[] toRGB(float[] value) { - // see implementation of toRGB in PDCabRGB, and PDFBOX-2971 + // see implementation of toRGB in PDCalRGB, and PDFBOX-2971 if (wpX == 1 && wpY == 1 && wpZ == 1) { float a = value[0]; + float[] result = map1.get(a); + if (result != null) + { + return result.clone(); + } float gamma = getGamma(); float powAG = (float) Math.pow(a, gamma); - return convXYZtoRGB(powAG, powAG, powAG); + result = convXYZtoRGB(powAG, powAG, powAG); + map1.put(a, result.clone()); + return result; } else { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDColor.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDColor.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDColor.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDColor.java 2018-12-03 16:18:13.000000000 +0000 @@ -19,7 +19,10 @@ import java.io.IOException; import java.util.Arrays; -import org.sejda.sambox.cos.*; +import org.sejda.sambox.cos.COSArray; +import org.sejda.sambox.cos.COSBase; +import org.sejda.sambox.cos.COSName; +import org.sejda.sambox.cos.COSNumber; /** * A color value, consisting of one or more color components, or for pattern color spaces, a name and optional color @@ -62,9 +65,12 @@ for (int i = 0; i < array.size(); i++) { COSBase component = array.get(i); - if(component instanceof COSNumber) { + if (component instanceof COSNumber) + { components[i] = ((COSNumber) array.get(i)).floatValue(); - } else { + } + else + { components[i] = 0f; } @@ -121,7 +127,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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDColorSpace.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDColorSpace.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDColorSpace.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDColorSpace.java 2018-12-03 16:18:13.000000000 +0000 @@ -25,13 +25,10 @@ import java.awt.image.WritableRaster; import java.io.IOException; -import org.sejda.sambox.cos.COSArray; -import org.sejda.sambox.cos.COSBase; -import org.sejda.sambox.cos.COSDictionary; -import org.sejda.sambox.cos.COSName; -import org.sejda.sambox.cos.COSObjectable; +import org.sejda.sambox.cos.*; import org.sejda.sambox.pdmodel.MissingResourceException; import org.sejda.sambox.pdmodel.PDResources; +import org.sejda.sambox.pdmodel.ResourceCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,7 +51,7 @@ */ public static PDColorSpace create(COSBase colorSpace) throws IOException { - return create(colorSpace, null); + return create(colorSpace, null, false); } /** @@ -72,6 +69,41 @@ return create(colorSpace, resources, false); } + public static PDColorSpace create(COSBase colorSpace, PDResources resources, boolean wasDefault) throws IOException { + boolean canCache = colorSpace.hasId() && resources != null && resources.getResourceCache() != null; + + if(canCache) { + ResourceCache cache = resources.getResourceCache(); + PDColorSpace existing = cache.getColorSpace(colorSpace.id().objectIdentifier); + if(existing != null) { + LOG.debug("Using cached color space for {}", colorSpace.id().objectIdentifier); + return existing; + } + } + + PDColorSpace result = createUncached(colorSpace, resources, wasDefault); + + if(colorSpace.hasId() && resources != null) { + ResourceCache cache = resources.getResourceCache(); + if(cache != null) { + if(isAllowedCache(result)) { + cache.put(colorSpace.id().objectIdentifier, result); + } + } + } + + return result; + } + + public static boolean isAllowedCache(PDColorSpace colorSpace) { + if(colorSpace instanceof PDPattern) { + // cannot cache PDPattern color spaces in a global cache, they carry page resources + return false; + } else { + return true; + } + } + /** * Creates a color space given a name or array. Abbreviated device color names are not supported here, please * replace them first. This method is for PDFBox internal use only, others should use {@link create(COSBase, @@ -84,7 +116,7 @@ * @throws MissingResourceException if the color space is missing in the resources dictionary * @throws IOException if the color space is unknown or cannot be created. */ - public static PDColorSpace create(COSBase colorSpace, PDResources resources, boolean wasDefault) + private static PDColorSpace createUncached(COSBase colorSpace, PDResources resources, boolean wasDefault) throws IOException { colorSpace = colorSpace.getCOSObject(); @@ -204,7 +236,7 @@ || name == COSName.DEVICEGRAY) { // not allowed in an array, but we sometimes encounter these regardless - return create(name, resources, wasDefault); + return createUncached(name, resources, wasDefault); } else { @@ -217,7 +249,7 @@ if (csAsDic.containsKey(COSName.COLORSPACE)) { LOG.warn("Found invalid color space defined as dictionary {}", csAsDic); - return create(csAsDic.getDictionaryObject(COSName.COLORSPACE), resources, + return createUncached(csAsDic.getDictionaryObject(COSName.COLORSPACE), resources, wasDefault); } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDDeviceN.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDDeviceN.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDDeviceN.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDDeviceN.java 2018-12-03 16:18:13.000000000 +0000 @@ -280,9 +280,9 @@ // private BufferedImage toRGBWithTintTransform(WritableRaster raster) throws IOException { - // map only in use if one color component - Map map1 = new HashMap<>(); - float key = 0; + // cache color mappings + Map map1 = new HashMap(); + String key = null; int width = raster.getWidth(); int height = raster.getHeight(); @@ -299,16 +299,17 @@ for (int x = 0; x < width; x++) { raster.getPixel(x, y, src); - if (numSrcComponents == 1) + // use a string representation as key + key = Float.toString(src[0]); + for (int s = 1; s < numSrcComponents; s++) { - int[] pxl = map1.get(src[0]); - if (pxl != null) - { - rgbRaster.setPixel(x, y, pxl); - continue; - } - // need to remember key because src is modified - key = src[0]; + key += "#" + Float.toString(src[s]); + } + int[] pxl = map1.get(key); + if (pxl != null) + { + rgbRaster.setPixel(x, y, pxl); + continue; } // scale to 0..1 @@ -327,14 +328,11 @@ { // scale to 0..255 rgb[s] = (int) (rgbFloat[s] * 255f); - } - - if (numSrcComponents == 1) - { - // must clone because rgb is reused - map1.put(key, rgb.clone()); } + // must clone because rgb is reused + map1.put(key, rgb.clone()); + rgbRaster.setPixel(x, y, rgb); } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDDeviceRGB.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDDeviceRGB.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDDeviceRGB.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDDeviceRGB.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,26 +16,22 @@ */ package org.sejda.sambox.pdmodel.graphics.color; -import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; -import java.awt.image.ColorModel; -import java.awt.image.ComponentColorModel; import java.awt.image.WritableRaster; import java.io.IOException; import org.sejda.sambox.cos.COSName; /** - * Colours in the DeviceRGB colour space are specified according to the additive - * RGB (red-green-blue) colour model. + * Colours in the DeviceRGB colour space are specified according to the additive RGB (red-green-blue) colour model. * * @author Ben Litchfield * @author John Hewson */ public final class PDDeviceRGB extends PDDeviceColorSpace { - /** This is the single instance of this class. */ + /** This is the single instance of this class. */ public static final PDDeviceRGB INSTANCE = new PDDeviceRGB(); private final PDColor initialColor = new PDColor(new float[] { 0, 0, 0 }, this); @@ -55,6 +51,7 @@ { return; } + synchronized (this) { // we might have been waiting for another thread, so check again @@ -108,32 +105,16 @@ public BufferedImage toRGBImage(WritableRaster raster) throws IOException { init(); - ColorModel colorModel = new ComponentColorModel(awtColorSpace, - false, false, Transparency.OPAQUE, raster.getDataBuffer().getDataType()); - - BufferedImage image = new BufferedImage(colorModel, raster, false, null); - // // WARNING: this method is performance sensitive, modify with care! // - // Please read PDFBOX-3854 and look at the related commits first. + // Please read PDFBOX-3854 and PDFBOX-2092 and look at the related commits first. // The current code returns TYPE_INT_RGB images which prevents slowness due to threads // blocking each other when TYPE_CUSTOM images are used. - // ColorConvertOp is not used here because it has a larger memory footprint and no further - // performance improvement. - // The multiparameter setRGB() call is not used because it brings no improvement. - - BufferedImage dest = new BufferedImage(image.getWidth(), image.getHeight(), + BufferedImage image = new BufferedImage(raster.getWidth(), raster.getHeight(), BufferedImage.TYPE_INT_RGB); - int width = image.getWidth(); - int height = image.getHeight(); - for (int x = 0; x < width; ++x) - { - for (int y = 0; y < height; ++y) - { - dest.setRGB(x, y, image.getRGB(x, y)); - } - } - return dest; + image.setData(raster); + return image; } + } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDICCBased.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDICCBased.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDICCBased.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDICCBased.java 2018-12-03 16:18:13.000000000 +0000 @@ -19,12 +19,15 @@ import static org.sejda.util.RequireUtils.requireIOCondition; import java.awt.Color; +import java.awt.Transparency; import java.awt.color.CMMException; import java.awt.color.ColorSpace; import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; import java.awt.color.ProfileDataException; import java.awt.image.BufferedImage; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; import java.awt.image.WritableRaster; import java.io.IOException; import java.io.InputStream; @@ -62,6 +65,11 @@ 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 = Boolean + .getBoolean("org.sejda.sambox.rendering.UseAlternateInsteadOfICCColorSpace"); /** * Creates a new ICC color space with an empty stream. @@ -112,6 +120,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 { @@ -131,6 +151,7 @@ } else { + profile = ensureDisplayProfile(profile); awtColorSpace = new ICC_ColorSpace(profile); iccProfile = profile; } @@ -149,29 +170,30 @@ awtColorSpace.toRGB(new float[awtColorSpace.getNumComponents()]); // this one triggers an exception for PDFBOX-3549 with KCMS new Color(awtColorSpace, new float[getNumberOfComponents()], 1f); + // PDFBOX-4015: this one triggers "CMMException: LCMS error 13" with LCMS + new ComponentColorModel(awtColorSpace, false, false, 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 { @@ -179,6 +201,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. */ @@ -190,6 +228,33 @@ return deviceModel.equals("sRGB"); } + // PDFBOX-4114: fix profile that has the wrong display class, + // as done by Harald Kuhr in twelvemonkeys JPEGImageReader.ensureDisplayProfile() + private static ICC_Profile ensureDisplayProfile(ICC_Profile profile) + { + if (profile.getProfileClass() != ICC_Profile.CLASS_DISPLAY) + { + byte[] profileData = profile.getData(); // Need to clone entire profile, due to a OpenJDK bug + + if (profileData[ICC_Profile.icHdrRenderingIntent] == ICC_Profile.icPerceptual) + { + LOG.debug("ICC profile is Perceptual, ignoring, treating as Display class"); + intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, + ICC_Profile.icHdrDeviceClass); + return ICC_Profile.getInstance(profileData); + } + } + return profile; + } + + private static void intToBigEndian(int value, byte[] array, int index) + { + array[index] = (byte) (value >> 24); + array[index + 1] = (byte) (value >> 16); + array[index + 2] = (byte) (value >> 8); + array[index + 3] = (byte) (value); + } + @Override public float[] toRGB(float[] value) throws IOException { @@ -199,12 +264,26 @@ } if (awtColorSpace != null) { + // PDFBOX-2142: clamp bad values // WARNING: toRGB is very slow when used with LUT-based ICC profiles - return awtColorSpace.toRGB(value); + return awtColorSpace.toRGB(clampColors(awtColorSpace, value)); } return alternateColorSpace.toRGB(value); } + private float[] clampColors(ICC_ColorSpace cs, float[] value) + { + float[] result = new float[value.length]; + for (int i = 0; i < value.length; ++i) + { + float minValue = cs.getMinValue(i); + float maxValue = cs.getMaxValue(i); + result[i] = value[i] < minValue ? minValue + : (value[i] > maxValue ? maxValue : value[i]); + } + return result; + } + @Override public BufferedImage toRGBImage(WritableRaster raster) throws IOException { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDIndexed.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDIndexed.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDIndexed.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/color/PDIndexed.java 2018-12-03 16:18:13.000000000 +0000 @@ -31,6 +31,7 @@ import org.sejda.sambox.cos.COSNumber; import org.sejda.sambox.cos.COSStream; import org.sejda.sambox.cos.COSString; +import org.sejda.sambox.pdmodel.PDResources; import org.sejda.sambox.pdmodel.common.PDStream; /** @@ -66,13 +67,27 @@ } /** - * Creates a new Indexed color space from the given PDF array. + * Creates a new indexed color space from the given PDF array. * @param indexedArray the array containing the indexed parameters + * @throws java.io.IOException */ public PDIndexed(COSArray indexedArray) throws IOException { + this(indexedArray, null); + } + + /** + * Creates a new indexed color space from the given PDF array. + * @param indexedArray the array containing the indexed parameters + * @param resources the resources, can be null. Allows to use its cache for the colorspace. + * @throws java.io.IOException + */ + public PDIndexed(COSArray indexedArray, PDResources resources) throws IOException + { array = indexedArray; - baseColorSpace = PDColorSpace.create(array.getObject(1)); + // don't call getObject(1), we want to pass a reference if possible + // to profit from caching (PDFBOX-4149) + baseColorSpace = PDColorSpace.create(array.get(1), resources); readColorTable(); initRgbColorTable(); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/form/PDFormXObject.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/form/PDFormXObject.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/form/PDFormXObject.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/form/PDFormXObject.java 2018-12-03 16:18:13.000000000 +0000 @@ -222,13 +222,7 @@ @Override public Matrix getMatrix() { - COSArray array = (COSArray) getCOSObject().getDictionaryObject(COSName.MATRIX); - if (array != null) - { - return new Matrix(array); - } - // default value is the identity matrix - return new Matrix(); + return Matrix.createMatrix(getCOSObject().getDictionaryObject(COSName.MATRIX)); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/form/PDTransparencyGroupAttributes.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/form/PDTransparencyGroupAttributes.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/form/PDTransparencyGroupAttributes.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/form/PDTransparencyGroupAttributes.java 2018-12-03 16:18:13.000000000 +0000 @@ -18,6 +18,7 @@ import java.io.IOException; +import org.sejda.sambox.cos.COSBase; import org.sejda.sambox.cos.COSDictionary; import org.sejda.sambox.cos.COSName; import org.sejda.sambox.pdmodel.common.PDDictionaryWrapper; @@ -56,7 +57,11 @@ { if (colorSpace == null && getCOSObject().containsKey(COSName.CS)) { - colorSpace = PDColorSpace.create(getCOSObject().getDictionaryObject(COSName.CS)); + COSBase dictionaryObject = getCOSObject().getDictionaryObject(COSName.CS); + if(dictionaryObject != null) + { + colorSpace = PDColorSpace.create(dictionaryObject); + } } return colorSpace; } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/image/LosslessFactory.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/image/LosslessFactory.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/image/LosslessFactory.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/image/LosslessFactory.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,23 +16,33 @@ package org.sejda.sambox.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.sejda.io.FastByteArrayOutputStream; import org.sejda.sambox.cos.COSDictionary; +import org.sejda.sambox.cos.COSInteger; import org.sejda.sambox.cos.COSName; import org.sejda.sambox.filter.Filter; import org.sejda.sambox.filter.FilterFactory; import org.sejda.sambox.pdmodel.graphics.color.PDColorSpace; +import org.sejda.sambox.pdmodel.graphics.color.PDDeviceCMYK; import org.sejda.sambox.pdmodel.graphics.color.PDDeviceColorSpace; import org.sejda.sambox.pdmodel.graphics.color.PDDeviceGray; import org.sejda.sambox.pdmodel.graphics.color.PDDeviceRGB; +import org.sejda.sambox.pdmodel.graphics.color.PDICCBased; /** * Factory for creating a PDImageXObject containing a lossless compressed image. @@ -41,198 +51,164 @@ */ 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.sejda.sambox.pdmodel.graphics.color.PDColorSpace) + * PDImageXObject.setColorSpace()} * - * @param image the buffered image to embed - * @return a new Image XObject + * @param document the document where the image will be created + * @param image the BufferedImage to embed + * @return a new image XObject * @throws IOException if something goes wrong */ public static PDImageXObject createFromImage(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; - - FastByteArrayOutputStream bos = new FastByteArrayOutputStream( - (width * bpc / 8) + (width * bpc % 8 != 0 ? 1 : 0) * height); - try (MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(bos)) + return createFromGrayImage(image); + } + if (usePredictorEncoder) + { + PDImageXObject pdImageXObject = new PredictorEncoder(image).encode(); + if (pdImageXObject != null) { - - for (int y = 0; y < height; ++y) + if (pdImageXObject.getColorSpace() == PDDeviceRGB.INSTANCE + && pdImageXObject.getBitsPerComponent() < 16 + && image.getWidth() * image.getHeight() <= 50 * 50) { - for (int pixel : image.getRGB(0, y, width, 1, rgbLineBuffer, 0, width)) + // also create classic compressed image, compare sizes + PDImageXObject pdImageXObjectClassic = createFromRGBImage(image); + if (pdImageXObjectClassic.getCOSObject().getFilteredLength() < pdImageXObject + .getCOSObject().getFilteredLength()) { - mcios.writeBits(pixel & 0xFF, bpc); - } - - int bitOffset = mcios.getBitOffset(); - if (bitOffset != 0) - { - mcios.writeBits(0, 8 - bitOffset); + pdImageXObject.getCOSObject().close(); + return pdImageXObjectClassic; } + pdImageXObjectClassic.getCOSObject().close(); } - mcios.flush(); + return pdImageXObject; } - imageData = bos.toByteArray(); } - else - { - // RGB - bpc = 8; - deviceColorSpace = PDDeviceRGB.INSTANCE; - imageData = new byte[width * height * 3]; - int byteIdx = 0; + // Fallback: We export the image as 8-bit sRGB and might loose color information + return createFromRGBImage(image); + } + + // grayscale images need one color per sample + private static PDImageXObject createFromGrayImage(BufferedImage image) throws IOException + { + int height = image.getHeight(); + int width = image.getWidth(); + int[] rgbLineBuffer = new int[width]; + int bpc = image.getColorModel().getPixelSize(); + FastByteArrayOutputStream baos = new FastByteArrayOutputStream( + ((width * bpc / 8) + (width * bpc % 8 != 0 ? 1 : 0)) * height); + try (MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(baos)) + { for (int y = 0; y < height; ++y) { for (int pixel : image.getRGB(0, y, width, 1, rgbLineBuffer, 0, width)) { - imageData[byteIdx++] = (byte) ((pixel >> 16) & 0xFF); - imageData[byteIdx++] = (byte) ((pixel >> 8) & 0xFF); - imageData[byteIdx++] = (byte) (pixel & 0xFF); + mcios.writeBits(pixel & 0xFF, bpc); } - } - } - - PDImageXObject pdImage = prepareImageXObject(imageData, image.getWidth(), image.getHeight(), - bpc, deviceColorSpace); - // alpha -> soft mask - PDImage xAlpha = createAlphaFromARGBImage(image); - if (xAlpha != null) - { - pdImage.getCOSObject().setItem(COSName.SMASK, xAlpha); + int bitOffset = mcios.getBitOffset(); + if (bitOffset != 0) + { + mcios.writeBits(0, 8 - bitOffset); + } + } + mcios.flush(); } - - return pdImage; + return prepareImageXObject(baos.toByteArray(), image.getWidth(), image.getHeight(), bpc, + PDDeviceGray.INSTANCE); } - /** - * Creates a grayscale Flate encoded PDImageXObject from the alpha channel of an image. - * - * @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(BufferedImage image) throws IOException + private static PDImageXObject createFromRGBImage(BufferedImage image) throws IOException { - // this implementation makes the assumption that the raster uses - // SinglePixelPackedSampleModel, i.e. the values can be used 1:1 for - // the stream. - // Sadly the type of the databuffer is TYPE_INT and not TYPE_BYTE. - if (!image.getColorModel().hasAlpha()) + 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) { - return null; + alphaImageData = new byte[((width * apbc / 8) + (width * apbc % 8 != 0 ? 1 : 0)) + * height]; } - - // extract the alpha information - WritableRaster alphaRaster = image.getAlphaRaster(); - if (alphaRaster == null) + else { - // happens sometimes (PDFBOX-2654) despite colormodel claiming to have alpha - return createAlphaFromARGBImage2(image); + alphaImageData = new byte[0]; } - - int[] pixels = alphaRaster.getPixels(0, 0, alphaRaster.getWidth(), alphaRaster.getHeight(), - (int[]) null); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - int bpc; - if (image.getTransparency() == Transparency.BITMASK) + 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) + imageData[byteIdx++] = (byte) ((pixel >> 16) & 0xFF); + imageData[byteIdx++] = (byte) ((pixel >> 8) & 0xFF); + imageData[byteIdx++] = (byte) (pixel & 0xFF); + if (transparency != Transparency.OPAQUE) { - while (mcios.getBitOffset() != 0) + // 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 { - mcios.writeBit(0); + // write a byte + alphaImageData[alphaByteIdx++] = (byte) ((pixel >> 24) & 0xFF); } } } - mcios.flush(); - mcios.close(); - } - else - { - bpc = 8; - for (int pixel : pixels) - { - bos.write(pixel); - } - } - PDImageXObject pdImage = prepareImageXObject(bos.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(BufferedImage bi) throws IOException - { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - int bpc; - if (bi.getTransparency() == Transparency.BITMASK) - { - bpc = 1; - MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(bos); - for (int y = 0, h = bi.getHeight(); y < h; ++y) + // skip boundary if needed + if (transparency == Transparency.BITMASK && alphaBitPos != 7) { - 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); - } + alphaBitPos = 7; + ++alphaByteIdx; } - mcios.flush(); - mcios.close(); } - else + PDImageXObject pdImage = prepareImageXObject(imageData, image.getWidth(), image.getHeight(), + bpc, deviceColorSpace); + if (transparency != Transparency.OPAQUE) { - bpc = 8; - 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; - bos.write(alpha); - } - } + PDImageXObject pdMask = prepareImageXObject(alphaImageData, image.getWidth(), + image.getHeight(), apbc, PDDeviceGray.INSTANCE); + pdImage.getCOSObject().setItem(COSName.SMASK, pdMask); } - - PDImageXObject pdImage = prepareImageXObject(bos.toByteArray(), bi.getWidth(), - bi.getHeight(), bpc, PDDeviceGray.INSTANCE); - return pdImage; } @@ -240,6 +216,7 @@ * Create a PDImageXObject while making a decision whether not to compress, use Flate filter only, or Flate and LZW * filters. * + * @param document The document. * @param byteArray array with data. * @param width the image width * @param height the image height @@ -261,4 +238,453 @@ bitsPerComponent, initColorSpace); } + private static class PredictorEncoder + { + 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; + + /** + * Initialize the encoder and set all final fields + */ + PredictorEncoder(BufferedImage image) + { + 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]; + } + + /** + * 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 + FastByteArrayOutputStream stream = new FastByteArrayOutputStream( + 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(FastByteArrayOutputStream 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(); + 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( + 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(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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/image/PDImageXObject.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/image/PDImageXObject.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/image/PDImageXObject.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/image/PDImageXObject.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,6 +16,8 @@ */ package org.sejda.sambox.pdmodel.graphics.image; +import static java.util.Objects.nonNull; +import static java.util.Optional.ofNullable; import static org.sejda.util.RequireUtils.requireNotNullArg; import java.awt.Graphics2D; @@ -27,7 +29,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.ref.SoftReference; import java.nio.ByteBuffer; +import java.util.List; import javax.imageio.ImageIO; @@ -36,6 +40,7 @@ import org.sejda.sambox.cos.COSBase; import org.sejda.sambox.cos.COSName; import org.sejda.sambox.cos.COSStream; +import org.sejda.sambox.filter.DecodeResult; import org.sejda.sambox.pdmodel.PDResources; import org.sejda.sambox.pdmodel.common.PDMetadata; import org.sejda.sambox.pdmodel.common.PDStream; @@ -44,6 +49,8 @@ import org.sejda.sambox.pdmodel.graphics.color.PDDeviceGray; import org.sejda.sambox.util.filetypedetector.FileType; import org.sejda.sambox.util.filetypedetector.FileTypeDetector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * An Image XObject. @@ -53,7 +60,10 @@ */ public final class PDImageXObject extends PDXObject implements PDImage { - private BufferedImage cachedImage; + + private static final Logger LOG = LoggerFactory.getLogger(PDImageXObject.class); + + private SoftReference cachedImage; private PDColorSpace colorSpace; private PDResources resources; // current resource dictionary (has color spaces) @@ -74,7 +84,6 @@ /** * Creates an Image XObject in the given document. * - * @param document the current document * @throws java.io.IOException if there is an error creating the XObject. */ public PDImageXObject() throws IOException @@ -129,9 +138,15 @@ public PDImageXObject(PDStream stream, PDResources resources) throws IOException { super(stream, COSName.IMAGE); - stream.getCOSObject().addAll(stream.getCOSObject().getDecodeResult().getParameters()); this.resources = resources; - this.colorSpace = stream.getCOSObject().getDecodeResult().getJPXColorSpace(); + List filters = stream.getFilters(); + if (filters != null && !filters.isEmpty() + && COSName.JPX_DECODE.equals(filters.get(filters.size() - 1))) + { + DecodeResult decodeResult = stream.getCOSObject().getDecodeResult(); + stream.getCOSObject().addAll(decodeResult.getParameters()); + this.colorSpace = decodeResult.getJPXColorSpace(); + } } public static PDImageXObject createFromFile(String imagePath) throws IOException @@ -151,11 +166,21 @@ } if (fileType.equals(FileType.TIFF)) { - return CCITTFactory.createFromFile(file); + try + { + return CCITTFactory.createFromFile(file); + } + catch (IOException ex) + { + LOG.warn("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 + } } // last resort, let's see if ImageIO can read it BufferedImage image = ImageIO.read(file); - requireNotNullArg(image, "Image type not supported " + file.getName()); + requireNotNullArg(image, "Image type " + fileType + " not supported " + file.getName()); return LosslessFactory.createFromImage(image); } @@ -212,7 +237,11 @@ { if (cachedImage != null) { - return cachedImage; + BufferedImage cached = cachedImage.get(); + if (cached != null) + { + return cached; + } } // get image as RGB @@ -222,7 +251,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 { @@ -230,14 +260,28 @@ PDImageXObject mask = getMask(); if (mask != null && mask.isStencil()) { - image = applyMask(image, mask.getOpaqueImage(), false); + image = applyMask(image, mask.getOpaqueImage(), false, null); } } - cachedImage = image; + cachedImage = new SoftReference<>(image); return image; } + private float[] extractMatte(PDImageXObject softMask) throws IOException + { + float[] matte = ofNullable( + softMask.getCOSObject().getDictionaryObject(COSName.MATTE, COSArray.class)) + .map(COSArray::toFloatArray).orElse(null); + if (nonNull(matte)) + { + // PDFBOX-4267: process /Matte + // convert to RGB + return getColorSpace().toRGB(matte); + } + return null; + } + /** * * @return the image without mask applied. The image is not cached @@ -275,7 +319,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) { if (mask == null) { @@ -320,6 +365,18 @@ 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 { @@ -333,6 +390,11 @@ return masked; } + private static float clampColor(float color) + { + return color < 0 ? 0 : (color > 255 ? 255 : color); + } + /** * High-quality image scaling. */ @@ -340,9 +402,12 @@ { 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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/image/PDInlineImage.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/image/PDInlineImage.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/image/PDInlineImage.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/image/PDInlineImage.java 2018-12-03 16:18:13.000000000 +0000 @@ -272,7 +272,7 @@ @Override public COSArray getDecode() { - return (COSArray) parameters.getDictionaryObject(COSName.D, COSName.DECODE); + return parameters.getDictionaryObject(COSName.D, COSName.DECODE, COSArray.class); } @Override diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java 2018-12-03 16:18:13.000000000 +0000 @@ -86,7 +86,12 @@ { this.dict = new COSDictionary(); this.dict.setItem(COSName.OCGS, new COSArray()); - this.dict.setItem(COSName.D, new COSDictionary()); + COSDictionary d = new COSDictionary(); + + // Name optional but required for PDF/A-3 + d.setString(COSName.NAME, "Top"); + + this.dict.setItem(COSName.D, d); } /** @@ -118,12 +123,20 @@ private COSDictionary getD() { - COSDictionary d = (COSDictionary)this.dict.getDictionaryObject(COSName.D); - if (d == null) + COSBase base = this.dict.getDictionaryObject(COSName.D); + if (base instanceof COSDictionary) { - d = new COSDictionary(); - this.dict.setItem(COSName.D, d); //D is required + return (COSDictionary) base; } + + COSDictionary d = new COSDictionary(); + + // Name optional but required for PDF/A-3 + d.setString(COSName.NAME, "Top"); + + // D is required + this.dict.setItem(COSName.D, d); + return d; } @@ -245,41 +258,55 @@ */ public boolean isGroupEnabled(String groupName) { + return isGroupEnabled(getGroup(groupName)); + } + + /** + * Indicates whether an optional content group is enabled. + * @param group the group object + * @return true if the group is enabled + */ + public boolean isGroupEnabled(PDOptionalContentGroup group) + { //TODO handle Optional Content Configuration Dictionaries, //i.e. OCProperties/Configs + PDOptionalContentProperties.BaseState baseState = getBaseState(); + boolean enabled = !baseState.equals(BaseState.OFF); + //TODO What to do with BaseState.Unchanged? + + if (group == null) + { + return enabled; + } + COSDictionary d = getD(); - COSArray on = (COSArray)d.getDictionaryObject(COSName.ON); - if (on != null) + COSBase base = d.getDictionaryObject(COSName.ON); + if (base instanceof COSArray) { - for (COSBase o : on) + for (COSBase o : (COSArray) base) { - COSDictionary group = toDictionary(o); - String name = group.getString(COSName.NAME); - if (name.equals(groupName)) + COSDictionary dictionary = toDictionary(o); + if (dictionary == group.getCOSObject()) { return true; } } } - COSArray off = (COSArray)d.getDictionaryObject(COSName.OFF); - if (off != null) + base = d.getDictionaryObject(COSName.OFF); + if (base instanceof COSArray) { - for (COSBase o : off) + for (COSBase o : (COSArray) base) { - COSDictionary group = toDictionary(o); - String name = group.getString(COSName.NAME); - if (name.equals(groupName)) + COSDictionary dictionary = toDictionary(o); + if (dictionary == group.getCOSObject()) { return false; } } } - BaseState baseState = getBaseState(); - boolean enabled = !baseState.equals(BaseState.OFF); - //TODO What to do with BaseState.Unchanged? return enabled; } @@ -296,28 +323,49 @@ */ public boolean setGroupEnabled(String groupName, boolean enable) { + return setGroupEnabled(getGroup(groupName), enable); + } + + /** + * Enables or disables an optional content group. + * @param group the group object + * @param enable true to enable, false to disable + * @return true if the group already had an on or off setting, false otherwise + */ + public boolean setGroupEnabled(PDOptionalContentGroup group, boolean enable) + { + COSArray on; + COSArray off; + COSDictionary d = getD(); - COSArray on = (COSArray)d.getDictionaryObject(COSName.ON); - if (on == null) + COSBase base = d.getDictionaryObject(COSName.ON); + if (!(base instanceof COSArray)) { on = new COSArray(); d.setItem(COSName.ON, on); } - COSArray off = (COSArray)d.getDictionaryObject(COSName.OFF); - if (off == null) + else + { + on = (COSArray) base; + } + base = d.getDictionaryObject(COSName.OFF); + if (!(base instanceof COSArray)) { off = new COSArray(); d.setItem(COSName.OFF, off); } + else + { + off = (COSArray) base; + } boolean found = false; if (enable) { for (COSBase o : off) { - COSDictionary group = toDictionary(o); - String name = group.getString(COSName.NAME); - if (name.equals(groupName)) + COSDictionary groupDictionary = toDictionary(o); + if (groupDictionary == group.getCOSObject()) { //enable group off.remove(o); @@ -331,9 +379,8 @@ { for (COSBase o : on) { - COSDictionary group = toDictionary(o); - String name = group.getString(COSName.NAME); - if (name.equals(groupName)) + COSDictionary groupDictionary = toDictionary(o); + if (groupDictionary == group.getCOSObject()) { //disable group on.remove(o); @@ -345,14 +392,13 @@ } if (!found) { - PDOptionalContentGroup ocg = getGroup(groupName); if (enable) { - on.add(ocg.getCOSObject()); + on.add(group.getCOSObject()); } else { - off.add(ocg.getCOSObject()); + off.add(group.getCOSObject()); } } return found; diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/pattern/PDAbstractPattern.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/pattern/PDAbstractPattern.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/pattern/PDAbstractPattern.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/pattern/PDAbstractPattern.java 2018-12-03 16:18:13.000000000 +0000 @@ -129,16 +129,7 @@ */ public Matrix getMatrix() { - COSArray array = (COSArray)getCOSObject().getDictionaryObject(COSName.MATRIX); - if (array != null) - { - return new Matrix(array); - } - else - { - // default value is the identity matrix - return new Matrix(); - } + return Matrix.createMatrix(getCOSObject().getDictionaryObject(COSName.MATRIX)); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/pattern/PDShadingPattern.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/pattern/PDShadingPattern.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/pattern/PDShadingPattern.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/pattern/PDShadingPattern.java 2018-12-03 16:18:13.000000000 +0000 @@ -18,6 +18,7 @@ import java.io.IOException; +import org.sejda.sambox.cos.COSBase; import org.sejda.sambox.cos.COSDictionary; import org.sejda.sambox.cos.COSName; import org.sejda.sambox.pdmodel.graphics.shading.PDShading; @@ -64,12 +65,11 @@ { if (extendedGraphicsState == null) { - COSDictionary dictionary = (COSDictionary)getCOSObject() - .getDictionaryObject(COSName.EXT_G_STATE); + COSBase base = getCOSObject().getDictionaryObject(COSName.EXT_G_STATE); - if( dictionary != null ) + if(base instanceof COSDictionary) { - extendedGraphicsState = new PDExtendedGraphicsState( dictionary ); + extendedGraphicsState = new PDExtendedGraphicsState((COSDictionary) base); } } return extendedGraphicsState; @@ -94,10 +94,10 @@ { if (shading == null) { - COSDictionary dictionary = (COSDictionary) getCOSObject().getDictionaryObject(COSName.SHADING); - if( dictionary != null ) + COSBase base = getCOSObject().getDictionaryObject(COSName.SHADING); + if(base instanceof COSDictionary) { - shading = PDShading.create(dictionary); + shading = PDShading.create((COSDictionary) base); } } return shading; diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/GouraudShadingContext.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/GouraudShadingContext.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/GouraudShadingContext.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/GouraudShadingContext.java 2018-12-03 16:18:13.000000000 +0000 @@ -101,7 +101,7 @@ return new Vertex(p, colorComponentTab); } - void setTriangleList(List triangleList) + final void setTriangleList(List triangleList) { this.triangleList = triangleList; } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PatchMeshesShadingContext.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PatchMeshesShadingContext.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PatchMeshesShadingContext.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PatchMeshesShadingContext.java 2018-12-03 16:18:13.000000000 +0000 @@ -24,10 +24,7 @@ import java.awt.image.ColorModel; import java.io.EOFException; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.MemoryCacheImageInputStream; @@ -89,9 +86,18 @@ Matrix matrix, int controlPoints) throws IOException { COSDictionary dict = shadingType.getCOSObject(); + if (!(dict instanceof COSStream)) + { + return Collections.emptyList(); + } int bitsPerFlag = shadingType.getBitsPerFlag(); PDRange rangeX = shadingType.getDecodeForParameter(0); PDRange rangeY = shadingType.getDecodeForParameter(1); + if (Float.compare(rangeX.getMin(), rangeX.getMax()) == 0 || + Float.compare(rangeY.getMin(), rangeY.getMax()) == 0) + { + return Collections.emptyList(); + } PDRange[] colRange = new PDRange[numberOfColorComponents]; for (int i = 0; i < numberOfColorComponents; ++i) { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PDShading.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PDShading.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PDShading.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PDShading.java 2018-12-03 16:18:13.000000000 +0000 @@ -253,36 +253,36 @@ /** * Create the correct PD Model shading based on the COS base shading. * - * @param resourceDictionary the COS shading dictionary + * @param shadingDictionary the COS shading dictionary * @return the newly created shading resources object * @throws IOException if we are unable to create the PDShading object */ - public static PDShading create(COSDictionary resourceDictionary) throws IOException + public static PDShading create(COSDictionary shadingDictionary) throws IOException { PDShading shading = null; - int shadingType = resourceDictionary.getInt(COSName.SHADING_TYPE, 0); + int shadingType = shadingDictionary.getInt(COSName.SHADING_TYPE, 0); switch (shadingType) { case SHADING_TYPE1: - shading = new PDShadingType1(resourceDictionary); + shading = new PDShadingType1(shadingDictionary); break; case SHADING_TYPE2: - shading = new PDShadingType2(resourceDictionary); + shading = new PDShadingType2(shadingDictionary); break; case SHADING_TYPE3: - shading = new PDShadingType3(resourceDictionary); + shading = new PDShadingType3(shadingDictionary); break; case SHADING_TYPE4: - shading = new PDShadingType4(resourceDictionary); + shading = new PDShadingType4(shadingDictionary); break; case SHADING_TYPE5: - shading = new PDShadingType5(resourceDictionary); + shading = new PDShadingType5(shadingDictionary); break; case SHADING_TYPE6: - shading = new PDShadingType6(resourceDictionary); + shading = new PDShadingType6(shadingDictionary); break; case SHADING_TYPE7: - shading = new PDShadingType7(resourceDictionary); + shading = new PDShadingType7(shadingDictionary); break; default: throw new IOException("Error: Unknown shading type " + shadingType); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PDShadingType1.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PDShadingType1.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PDShadingType1.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/PDShadingType1.java 2018-12-03 16:18:13.000000000 +0000 @@ -55,16 +55,7 @@ */ public Matrix getMatrix() { - COSArray array = (COSArray) getCOSObject().getDictionaryObject(COSName.MATRIX); - if (array != null) - { - return new Matrix(array); - } - else - { - // identity matrix is the default - return new Matrix(); - } + return Matrix.createMatrix(getCOSObject().getDictionaryObject(COSName.MATRIX)); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/Type4ShadingContext.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/Type4ShadingContext.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/Type4ShadingContext.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/Type4ShadingContext.java 2018-12-03 16:18:13.000000000 +0000 @@ -23,6 +23,7 @@ import java.io.EOFException; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.imageio.stream.ImageInputStream; @@ -71,8 +72,17 @@ AffineTransform xform, Matrix matrix) throws IOException { COSDictionary dict = freeTriangleShadingType.getCOSObject(); + if (!(dict instanceof COSStream)) + { + return Collections.emptyList(); + } PDRange rangeX = freeTriangleShadingType.getDecodeForParameter(0); PDRange rangeY = freeTriangleShadingType.getDecodeForParameter(1); + if (Float.compare(rangeX.getMin(), rangeX.getMax()) == 0 || + Float.compare(rangeY.getMin(), rangeY.getMax()) == 0) + { + return Collections.emptyList(); + } PDRange[] colRange = new PDRange[numberOfColorComponents]; for (int i = 0; i < numberOfColorComponents; ++i) { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/Type5ShadingContext.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/Type5ShadingContext.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/Type5ShadingContext.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/shading/Type5ShadingContext.java 2018-12-03 16:18:13.000000000 +0000 @@ -23,6 +23,7 @@ import java.io.EOFException; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.imageio.stream.ImageInputStream; @@ -68,9 +69,18 @@ private List collectTriangles(PDShadingType5 latticeTriangleShadingType, AffineTransform xform, Matrix matrix) throws IOException { - COSDictionary cosDictionary = latticeTriangleShadingType.getCOSObject(); + COSDictionary dict = latticeTriangleShadingType.getCOSObject(); + if (!(dict instanceof COSStream)) + { + return Collections.emptyList(); + } PDRange rangeX = latticeTriangleShadingType.getDecodeForParameter(0); PDRange rangeY = latticeTriangleShadingType.getDecodeForParameter(1); + if (Float.compare(rangeX.getMin(), rangeX.getMax()) == 0 || + Float.compare(rangeY.getMin(), rangeY.getMax()) == 0) + { + return Collections.emptyList(); + } int numPerRow = latticeTriangleShadingType.getVerticesPerRow(); PDRange[] colRange = new PDRange[numberOfColorComponents]; for (int i = 0; i < numberOfColorComponents; ++i) @@ -80,7 +90,7 @@ List vlist = new ArrayList(); long maxSrcCoord = (long) Math.pow(2, bitsPerCoordinate) - 1; long maxSrcColor = (long) Math.pow(2, bitsPerColorComponent) - 1; - COSStream cosStream = (COSStream) cosDictionary; + COSStream cosStream = (COSStream) dict; try (ImageInputStream mciis = new MemoryCacheImageInputStream( cosStream.getUnfilteredStream())) diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/state/PDExtendedGraphicsState.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/state/PDExtendedGraphicsState.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/graphics/state/PDExtendedGraphicsState.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/graphics/state/PDExtendedGraphicsState.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,6 +16,9 @@ */ package org.sejda.sambox.pdmodel.graphics.state; +import static java.util.Objects.nonNull; +import static java.util.Optional.ofNullable; + import java.io.IOException; import org.sejda.sambox.cos.COSArray; @@ -70,7 +73,7 @@ { if (key.equals(COSName.LW)) { - gs.setLineWidth(getLineWidth()); + gs.setLineWidth(defaultIfNull(getLineWidth(), 1)); } else if (key.equals(COSName.LC)) { @@ -82,7 +85,7 @@ } else if (key.equals(COSName.ML)) { - gs.setMiterLimit(getMiterLimit()); + gs.setMiterLimit(defaultIfNull(getMiterLimit(), 10)); } else if (key.equals(COSName.D)) { @@ -94,7 +97,7 @@ } else if (key.equals(COSName.OPM)) { - gs.setOverprintMode(getOverprintMode().doubleValue()); + gs.setOverprintMode(defaultIfNull(getOverprintMode(), 0)); } else if (key.equals(COSName.FONT)) { @@ -107,11 +110,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)) { @@ -119,11 +122,11 @@ } else if (key.equals(COSName.CA)) { - gs.setAlphaConstant(getStrokingAlphaConstant()); + gs.setAlphaConstant(defaultIfNull(getStrokingAlphaConstant(), 1.0f)); } else if (key.equals(COSName.CA_NS)) { - gs.setNonStrokeAlphaConstants(getNonStrokingAlphaConstant()); + gs.setNonStrokeAlphaConstants(defaultIfNull(getNonStrokingAlphaConstant(), 1.0f)); } else if (key.equals(COSName.AIS)) { @@ -166,6 +169,11 @@ } } + private float defaultIfNull(Float val, float fallback) + { + return nonNull(val) ? val : fallback; + } + /** * This will get the underlying dictionary that this class acts on. * @@ -264,18 +272,17 @@ */ public PDLineDashPattern getLineDashPattern() { - PDLineDashPattern retval = null; - COSArray dp = (COSArray) dict.getDictionaryObject(COSName.D); - if (dp != null) + COSArray dp = dict.getDictionaryObject(COSName.D, COSArray.class); + if (nonNull(dp) && 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 = dp.getObject(0); + COSBase phase = dp.getObject(1); + if (dashArray instanceof COSArray && phase instanceof COSNumber) + { + return new PDLineDashPattern((COSArray) dashArray, ((COSNumber) phase).intValue()); + } } - return retval; + return null; } /** @@ -573,13 +580,8 @@ */ private Float getFloatItem(COSName key) { - Float retval = null; - COSNumber value = (COSNumber) dict.getDictionaryObject(key); - if (value != null) - { - retval = value.floatValue(); - } - return retval; + return ofNullable(dict.getDictionaryObject(key, COSNumber.class)).map(COSNumber::floatValue) + .orElse(null); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/OpenMode.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/OpenMode.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/OpenMode.java 1970-01-01 00:00:00.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/OpenMode.java 2018-12-03 16:18:13.000000000 +0000 @@ -0,0 +1,40 @@ +/* + * 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.sejda.sambox.pdmodel.interactive.action; + +/** + * This will specify whether to open the destination document in a new window. + * + * @author Tilman Hausherr + */ +public enum OpenMode +{ + /** + * The viewer application should behave in accordance with the current user preference. + */ + USER_PREFERENCE, + + /** + * Destination document will replace the current document in the same window. + */ + SAME_WINDOW, + + /** + * Open the destination document in a new window. + */ + NEW_WINDOW +} diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionEmbeddedGoTo.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionEmbeddedGoTo.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionEmbeddedGoTo.java 1970-01-01 00:00:00.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionEmbeddedGoTo.java 2018-12-03 16:18:13.000000000 +0000 @@ -0,0 +1,191 @@ +/* + * 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.sejda.sambox.pdmodel.interactive.action; + +import java.io.IOException; + +import org.sejda.sambox.cos.COSArray; +import org.sejda.sambox.cos.COSBase; +import org.sejda.sambox.cos.COSBoolean; +import org.sejda.sambox.cos.COSDictionary; +import org.sejda.sambox.cos.COSName; +import org.sejda.sambox.pdmodel.common.filespecification.PDFileSpecification; +import org.sejda.sambox.pdmodel.interactive.documentnavigation.destination.PDDestination; +import org.sejda.sambox.pdmodel.interactive.documentnavigation.destination.PDPageDestination; + +/** + * This represents a embedded go-to action that can be executed in a PDF document. + * + * @author Ben Litchfield + * @author Panagiotis Toumasis + * @author Tilman Hausherr + */ +public class PDActionEmbeddedGoTo extends PDAction +{ + /** + * This type of action this object represents. + */ + public static final String SUB_TYPE = "GoToE"; + + /** + * Default constructor. + */ + public PDActionEmbeddedGoTo() + { + setSubType(SUB_TYPE); + } + + /** + * Constructor. + * + * @param a The action dictionary. + */ + public PDActionEmbeddedGoTo(COSDictionary a) + { + super(a); + } + + /** + * This will get the destination to jump to. + * + * @return The D entry of the specific go-to action dictionary. + * + * @throws IOException If there is an error creating the destination. + */ + public PDDestination getDestination() throws IOException + { + return PDDestination.create(getCOSObject().getDictionaryObject(COSName.D)); + } + + /** + * This will set the destination to jump to. + * + * @param d The destination. + * + * @throws IllegalArgumentException if the destination is not a page dictionary object. + */ + public void setDestination(PDDestination d) + { + if (d instanceof PDPageDestination) + { + PDPageDestination pageDest = (PDPageDestination) d; + COSArray destArray = pageDest.getCOSObject(); + if (destArray.size() >= 1) + { + COSBase page = destArray.getObject(0); + if (!(page instanceof COSDictionary)) + { + throw new IllegalArgumentException("Destination of a GoToE action must be " + + "a page dictionary object"); + } + } + } + getCOSObject().setItem(COSName.D, d); + } + + /** + * This will get the file in which the destination is located. + * + * @return The F entry of the specific embedded go-to action dictionary. + * + * @throws IOException If there is an error creating the file spec. + */ + public PDFileSpecification getFile() throws IOException + { + return PDFileSpecification.createFS(getCOSObject().getDictionaryObject(COSName.F)); + } + + /** + * This will set the file in which the destination is located. + * + * @param fs The file specification. + */ + public void setFile(PDFileSpecification fs) + { + getCOSObject().setItem(COSName.F, fs); + } + + /** + * This will specify whether to open the destination document in a new window, in the same + * window, or behave in accordance with the current user preference. + * + * @return A flag specifying how to open the destination document. + */ + public OpenMode getOpenInNewWindow() + { + if (getCOSObject().getDictionaryObject(COSName.NEW_WINDOW) instanceof COSBoolean) + { + COSBoolean b = (COSBoolean) getCOSObject().getDictionaryObject(COSName.NEW_WINDOW); + return b.getValue() ? OpenMode.NEW_WINDOW : OpenMode.SAME_WINDOW; + } + return OpenMode.USER_PREFERENCE; + } + + /** + * This will specify whether to open the destination document in a new window. + * + * @param value The flag value. + */ + public void setOpenInNewWindow(OpenMode value) + { + if (null == value) + { + getCOSObject().removeItem(COSName.NEW_WINDOW); + return; + } + switch (value) + { + case USER_PREFERENCE: + getCOSObject().removeItem(COSName.NEW_WINDOW); + break; + case SAME_WINDOW: + getCOSObject().setBoolean(COSName.NEW_WINDOW, false); + break; + case NEW_WINDOW: + getCOSObject().setBoolean(COSName.NEW_WINDOW, true); + break; + default: + // shouldn't happen unless the enum type is changed + break; + } + } + + /** + * Get the target directory. + * + * @return the target directory or null if there is none. + */ + public PDTargetDirectory getTargetDirectory() + { + COSBase base = getCOSObject().getDictionaryObject(COSName.T); + if (base instanceof COSDictionary) + { + return new PDTargetDirectory((COSDictionary) base); + } + return null; + } + + /** + * Sets the target directory. + * + * @param targetDirectory the target directory. + */ + public void setTargetDirectory(PDTargetDirectory targetDirectory) + { + getCOSObject().setItem(COSName.T, targetDirectory); + } +} diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionFactory.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionFactory.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionFactory.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionFactory.java 2018-12-03 16:18:13.000000000 +0000 @@ -99,6 +99,10 @@ { return new PDActionThread(action); } + else if (PDActionEmbeddedGoTo.SUB_TYPE.equals(type)) + { + return new PDActionEmbeddedGoTo(action); + } } return null; } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionGoTo.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionGoTo.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionGoTo.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionGoTo.java 2018-12-03 16:18:13.000000000 +0000 @@ -43,7 +43,6 @@ */ public PDActionGoTo() { - super(); setSubType( SUB_TYPE ); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionImportData.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionImportData.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionImportData.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionImportData.java 2018-12-03 16:18:13.000000000 +0000 @@ -38,7 +38,6 @@ */ public PDActionImportData() { - action = new COSDictionary(); setSubType(SUB_TYPE); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionJavaScript.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionJavaScript.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionJavaScript.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionJavaScript.java 2018-12-03 16:18:13.000000000 +0000 @@ -39,7 +39,6 @@ */ public PDActionJavaScript() { - super(); setSubType(SUB_TYPE); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionLaunch.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionLaunch.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionLaunch.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionLaunch.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,6 +16,7 @@ */ package org.sejda.sambox.pdmodel.interactive.action; +import org.sejda.sambox.cos.COSBoolean; import org.sejda.sambox.cos.COSDictionary; import org.sejda.sambox.cos.COSName; import org.sejda.sambox.pdmodel.common.filespecification.FileSpecifications; @@ -204,26 +205,47 @@ } /** - * This will specify whether to open the destination document in a new window. - * If this flag is false, the destination document will replace the current - * document in the same window. If this entry is absent, the viewer application - * should behave in accordance with the current user preference. This entry is - * ignored if the file designated by the F entry is not a PDF document. + * This will specify whether to open the destination document in a new window, in the same + * window, or behave in accordance with the current user preference. * - * @return A flag specifying whether to open the destination document in a new window. + * @return A flag specifying how to open the destination document. */ - public boolean shouldOpenInNewWindow() + public OpenMode getOpenInNewWindow() { - return action.getBoolean( "NewWindow", true ); + if (getCOSObject().getDictionaryObject(COSName.NEW_WINDOW) instanceof COSBoolean) + { + COSBoolean b = (COSBoolean) getCOSObject().getDictionaryObject(COSName.NEW_WINDOW); + return b.getValue() ? OpenMode.NEW_WINDOW : OpenMode.SAME_WINDOW; + } + return OpenMode.USER_PREFERENCE; } /** - * This will specify the destination document to open in a new window. + * This will specify whether to open the destination document in a new window. * * @param value The flag value. */ - public void setOpenInNewWindow( boolean value ) + public void setOpenInNewWindow(OpenMode value) { - action.setBoolean( "NewWindow", value ); + if (null == value) + { + getCOSObject().removeItem(COSName.NEW_WINDOW); + return; + } + switch (value) + { + case USER_PREFERENCE: + getCOSObject().removeItem(COSName.NEW_WINDOW); + break; + case SAME_WINDOW: + getCOSObject().setBoolean(COSName.NEW_WINDOW, false); + break; + case NEW_WINDOW: + getCOSObject().setBoolean(COSName.NEW_WINDOW, true); + break; + default: + // shouldn't happen unless the enum type is changed + break; + } } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionMovie.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionMovie.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionMovie.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionMovie.java 2018-12-03 16:18:13.000000000 +0000 @@ -36,7 +36,6 @@ */ public PDActionMovie() { - action = new COSDictionary(); setSubType(SUB_TYPE); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionNamed.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionNamed.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionNamed.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionNamed.java 2018-12-03 16:18:13.000000000 +0000 @@ -33,7 +33,6 @@ */ public PDActionNamed() { - action = new COSDictionary(); setSubType(SUB_TYPE); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionRemoteGoTo.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionRemoteGoTo.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionRemoteGoTo.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionRemoteGoTo.java 2018-12-03 16:18:13.000000000 +0000 @@ -17,6 +17,7 @@ package org.sejda.sambox.pdmodel.interactive.action; import org.sejda.sambox.cos.COSBase; +import org.sejda.sambox.cos.COSBoolean; import org.sejda.sambox.cos.COSDictionary; import org.sejda.sambox.cos.COSName; import org.sejda.sambox.pdmodel.common.filespecification.FileSpecifications; @@ -40,7 +41,6 @@ */ public PDActionRemoteGoTo() { - action = new COSDictionary(); setSubType(SUB_TYPE); } @@ -130,24 +130,47 @@ } /** - * This will specify whether to open the destination document in a new window. If this flag is false, the - * destination document will replace the current document in the same window. If this entry is absent, the viewer - * application should behave in accordance with the current user preference. + * This will specify whether to open the destination document in a new window, in the same + * window, or behave in accordance with the current user preference. * - * @return A flag specifying whether to open the destination document in a new window. + * @return A flag specifying how to open the destination document. */ - public boolean shouldOpenInNewWindow() + public OpenMode getOpenInNewWindow() { - return action.getBoolean("NewWindow", true); + if (getCOSObject().getDictionaryObject(COSName.NEW_WINDOW) instanceof COSBoolean) + { + COSBoolean b = (COSBoolean) getCOSObject().getDictionaryObject(COSName.NEW_WINDOW); + return b.getValue() ? OpenMode.NEW_WINDOW : OpenMode.SAME_WINDOW; + } + return OpenMode.USER_PREFERENCE; } /** - * This will specify the destination document to open in a new window. + * This will specify whether to open the destination document in a new window. * * @param value The flag value. */ - public void setOpenInNewWindow(boolean value) + public void setOpenInNewWindow(OpenMode value) { - action.setBoolean("NewWindow", value); + if (null == value) + { + getCOSObject().removeItem(COSName.NEW_WINDOW); + return; + } + switch (value) + { + case USER_PREFERENCE: + getCOSObject().removeItem(COSName.NEW_WINDOW); + break; + case SAME_WINDOW: + getCOSObject().setBoolean(COSName.NEW_WINDOW, false); + break; + case NEW_WINDOW: + getCOSObject().setBoolean(COSName.NEW_WINDOW, true); + break; + default: + // shouldn't happen unless the enum type is changed + break; + } } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionResetForm.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionResetForm.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionResetForm.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionResetForm.java 2018-12-03 16:18:13.000000000 +0000 @@ -38,7 +38,6 @@ */ public PDActionResetForm() { - action = new COSDictionary(); setSubType(SUB_TYPE); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionSound.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionSound.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionSound.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionSound.java 2018-12-03 16:18:13.000000000 +0000 @@ -38,7 +38,6 @@ */ public PDActionSound() { - action = new COSDictionary(); setSubType(SUB_TYPE); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionURI.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionURI.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionURI.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDActionURI.java 2018-12-03 16:18:13.000000000 +0000 @@ -42,7 +42,6 @@ */ public PDActionURI() { - action = new COSDictionary(); setSubType(SUB_TYPE); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDTargetDirectory.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDTargetDirectory.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDTargetDirectory.java 1970-01-01 00:00:00.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/action/PDTargetDirectory.java 2018-12-03 16:18:13.000000000 +0000 @@ -0,0 +1,276 @@ +/* + * 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.sejda.sambox.pdmodel.interactive.action; + +import org.sejda.sambox.cos.*; +import org.sejda.sambox.pdmodel.interactive.documentnavigation.destination.PDNamedDestination; + +/** + * A target dictionary specifying path information to the target document. Each target dictionary + * specifies one element in the full path to the target and may have nested target dictionaries + * specifying additional elements. + * + * @author Tilman Hausherr + */ +public class PDTargetDirectory implements COSObjectable +{ + private final COSDictionary dict; + + /** + * Default constructor, creates target directory. + */ + public PDTargetDirectory() + { + dict = new COSDictionary(); + } + + /** + * Create a target directory from an existing dictionary. + * + * @param dictionary The existing graphics state. + */ + public PDTargetDirectory(COSDictionary dictionary) + { + dict = dictionary; + } + + /** + * This will get the underlying dictionary that this class acts on. + * + * @return The underlying dictionary for this class. + */ + @Override + public COSDictionary getCOSObject() + { + return dict; + } + + /** + * Get the relationship between the current document and the target (which may be an + * intermediate target). + * + * @return the relationship as a name. Valid values are P (the target is the parent of the + * current document) and C (the target is a child of the current document). Invalid values or + * null are also returned. + */ + public COSName getRelationship() + { + COSBase base = dict.getItem(COSName.R); + if (base instanceof COSName) + { + return (COSName) base; + } + return null; + } + + /** + * Set the relationship between the current document and the target (which may be an + * intermediate target). + * + * @param relationship Valid values are P (the target is the parent of the current document) and + * C (the target is a child of the current document). + * + * throws IllegalArgumentException if the parameter is not P or C. + */ + public void setRelationship(COSName relationship) + { + if (!COSName.P.equals(relationship) && !COSName.C.equals(relationship)) + { + throw new IllegalArgumentException("The only valid are P or C, not " + relationship.getName()); + } + dict.setItem(COSName.R, relationship); + } + + /** + * Get the name of the file as found in the EmbeddedFiles name tree. This is only to be used if + * the target is a child of the current document. + * + * @return a filename or null if there is none. + */ + public String getFilename() + { + return dict.getString(COSName.N); + } + + /** + * Sets the name of the file as found in the EmbeddedFiles name tree. This is only to be used if + * the target is a child of the current document. + * + * @param filename a filename or null if the entry is to be deleted. + */ + public void setFilename(String filename) + { + dict.setString(COSName.N, filename); + } + + /** + * Get the target directory. If this entry is absent, the current document is the target file + * containing the destination. + * + * @return the target directory or null if the current document is the target file containing + * the destination. + */ + public PDTargetDirectory getTargetDirectory() + { + COSBase base = dict.getDictionaryObject(COSName.T); + if (base instanceof COSDictionary) + { + return new PDTargetDirectory((COSDictionary) base); + } + return null; + } + + /** + * Sets the target directory. + * + * @param targetDirectory the target directory or null if the current document is the target + * file containing the destination. + */ + public void setTargetDirectory(PDTargetDirectory targetDirectory) + { + dict.setItem(COSName.T, targetDirectory); + } + + /** + * If the value in the /P entry is an integer, this will get the page number (zero-based) in the + * current document containing the file attachment annotation. + * + * @return the zero based page number or -1 if the /P entry value is missing or not a number. + */ + public int getPageNumber() + { + COSBase base = dict.getDictionaryObject(COSName.P); + if (base instanceof COSInteger) + { + return ((COSInteger) base).intValue(); + } + return -1; + } + + /** + * Set the page number (zero-based) in the current document containing the file attachment + * annotation. + * + * @param pageNumber the zero based page number. If this is < 0 then the entry is removed. + */ + public void setPageNumber(int pageNumber) + { + if (pageNumber < 0) + { + dict.removeItem(COSName.P); + } + else + { + dict.setInt(COSName.P, pageNumber); + } + } + + /** + * If the value in the /P entry is a string, this will get a named destination in the current + * document that provides the page number of the file attachment annotation. + * + * @return a named destination or null if the /P entry value is missing or not a string. + */ + public PDNamedDestination getNamedDestination() + { + COSBase base = dict.getDictionaryObject(COSName.P); + if (base instanceof COSString) + { + return new PDNamedDestination((COSString) base); + } + return null; + } + + /** + * This will set a named destination in the current document that provides the page number of + * the file attachment annotation. + * + * @param dest a named destination or null if the entry is to be removed. + */ + public void setNamedDestination(PDNamedDestination dest) + { + if (dest == null) + { + dict.removeItem(COSName.P); + } + else + { + dict.setItem(COSName.P, dest); + } + } + + /** + * If the value in the /A entry is an integer, this will get the index (zero-based) of the + * annotation in the /Annots array of the page specified by the /P entry. + * + * @return the zero based page number or -1 if the /P entry value is missing or not a number. + */ + public int getAnnotationIndex() + { + COSBase base = dict.getDictionaryObject(COSName.A); + if (base instanceof COSInteger) + { + return ((COSInteger) base).intValue(); + } + return -1; + } + + /** + * This will set the index (zero-based) of the annotation in the /Annots array of the page + * specified by the /P entry. + * + * @param index the zero based index. If this is < 0 then the entry is removed. + */ + public void setAnnotationIndex(int index) + { + if (index < 0) + { + dict.removeItem(COSName.A); + } + else + { + dict.setInt(COSName.A, index); + } + } + + /** + * If the value in the /A entry is a string, this will get the value of the /NM entry in the + * annotation dictionary. + * + * @return the /NM value of an annotation dictionary or null if the /A entry value is missing or + * not a string. + */ + public String getAnnotationName() + { + COSBase base = dict.getDictionaryObject(COSName.A); + if (base instanceof COSString) + { + return ((COSString) base).getString(); + } + return null; + } + + /** + * This will get the value of the /NM entry in the annotation dictionary. + * + * @param name the /NM value of an annotation dictionary or null if the entry is to be removed. + */ + public void setAnnotationName(String name) + { + dict.setString(COSName.A, name); + } +} diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/AnnotationFilter.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/AnnotationFilter.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/AnnotationFilter.java 1970-01-01 00:00:00.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/AnnotationFilter.java 2018-12-03 16:18:13.000000000 +0000 @@ -0,0 +1,30 @@ +/* + * 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.sejda.sambox.pdmodel.interactive.annotation; + +/** + * Simple interface allowing the use of an annotation filter visitor. + * + * @author Maxime Veron + * + */ +public interface AnnotationFilter +{ + boolean accept(PDAnnotation annotation); +} + diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationFileAttachment.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationFileAttachment.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationFileAttachment.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationFileAttachment.java 2018-12-03 16:18:13.000000000 +0000 @@ -55,13 +55,13 @@ */ public PDAnnotationFileAttachment() { - getCOSObject().setItem(COSName.SUBTYPE, COSName.getPDFName(SUB_TYPE)); + getCOSObject().setName(COSName.SUBTYPE, SUB_TYPE); } /** * Creates a Link annotation from a COSDictionary, expected to be a correct object definition. * - * @param field the PDF objet to represent as a field. + * @param field the PDF object to represent as a field. */ public PDAnnotationFileAttachment(COSDictionary field) { @@ -95,16 +95,16 @@ */ public String getAttachmentName() { - return getCOSObject().getNameAsString("Name", ATTACHMENT_NAME_PUSH_PIN); + return getCOSObject().getNameAsString(COSName.NAME, ATTACHMENT_NAME_PUSH_PIN); } /** - * Set the name used to draw the attachement icon. See the ATTACHMENT_NAME_XXX constants. + * Set the name used to draw the attachment icon. See the ATTACHMENT_NAME_XXX constants. * * @param name The name of the visual icon to draw. */ - public void setAttachementName(String name) + public void setAttachmentName(String name) { - getCOSObject().setName("Name", name); + getCOSObject().setName(COSName.NAME, name); } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotation.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotation.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotation.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotation.java 2018-12-03 16:18:13.000000000 +0000 @@ -159,10 +159,6 @@ // see 12.5.6.10 Text Markup Annotations return new PDAnnotationTextMarkup(annotDic); } - else if (PDAnnotationLink.SUB_TYPE.equals(subtype)) - { - return new PDAnnotationLink(annotDic); - } else if (COSName.WIDGET.getName().equals(subtype)) { return new PDAnnotationWidget(annotDic); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationLine.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationLine.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationLine.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationLine.java 2018-12-03 16:18:13.000000000 +0000 @@ -111,7 +111,7 @@ */ public PDAnnotationLine() { - getCOSObject().setItem(COSName.SUBTYPE, COSName.getPDFName(SUB_TYPE)); + getCOSObject().setName(COSName.SUBTYPE, SUB_TYPE); // Dictionary value L is mandatory, fill in with arbitary value setLine(new float[] { 0, 0, 0, 0 }); } @@ -306,7 +306,7 @@ */ public float getLeaderLineLength() { - return this.getCOSObject().getFloat(COSName.LL); + return this.getCOSObject().getFloat(COSName.LL, 0); } /** @@ -326,7 +326,7 @@ */ public float getLeaderLineExtensionLength() { - return this.getCOSObject().getFloat(COSName.LLE); + return this.getCOSObject().getFloat(COSName.LLE, 0); } /** @@ -346,7 +346,7 @@ */ public float getLeaderLineOffsetLength() { - return this.getCOSObject().getFloat(COSName.LLO); + return this.getCOSObject().getFloat(COSName.LLO, 0); } /** @@ -366,7 +366,7 @@ */ public String getCaptionPositioning() { - return this.getCOSObject().getString(COSName.CP); + return this.getCOSObject().getNameAsString(COSName.CP); } /** @@ -376,7 +376,7 @@ */ public void setCaptionPositioning(String captionPositioning) { - this.getCOSObject().setString(COSName.CP, captionPositioning); + this.getCOSObject().setName(COSName.CP, captionPositioning); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationLink.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationLink.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationLink.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationLink.java 2018-12-03 16:18:13.000000000 +0000 @@ -65,8 +65,7 @@ */ public PDAnnotationLink() { - super(); - getCOSObject().setItem(COSName.SUBTYPE, COSName.getPDFName(SUB_TYPE)); + getCOSObject().setName(COSName.SUBTYPE, SUB_TYPE); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationPopup.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationPopup.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationPopup.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationPopup.java 2018-12-03 16:18:13.000000000 +0000 @@ -36,7 +36,7 @@ */ public PDAnnotationPopup() { - getCOSObject().setItem(COSName.SUBTYPE, COSName.getPDFName(SUB_TYPE)); + getCOSObject().setName(COSName.SUBTYPE, SUB_TYPE); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationRubberStamp.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationRubberStamp.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationRubberStamp.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationRubberStamp.java 2018-12-03 16:18:13.000000000 +0000 @@ -98,8 +98,7 @@ */ public PDAnnotationRubberStamp() { - super(); - getCOSObject().setItem(COSName.SUBTYPE, COSName.getPDFName(SUB_TYPE)); + getCOSObject().setName(COSName.SUBTYPE, SUB_TYPE); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java 2018-12-03 16:18:13.000000000 +0000 @@ -37,7 +37,7 @@ */ public static final String SUB_TYPE_SQUARE = "Square"; /** - * Constant for an Eliptical type of annotation. + * Constant for an elliptical type of annotation. */ public static final String SUB_TYPE_CIRCLE = "Circle"; @@ -54,7 +54,7 @@ /** * Creates a Line annotation from a COSDictionary, expected to be a correct object definition. * - * @param field the PDF objet to represent as a field. + * @param field the PDF object to represent as a field. */ public PDAnnotationSquareCircle(COSDictionary field) { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationText.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationText.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationText.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAnnotationText.java 2018-12-03 16:18:13.000000000 +0000 @@ -76,7 +76,7 @@ */ public PDAnnotationText() { - getCOSObject().setItem(COSName.SUBTYPE, COSName.getPDFName(SUB_TYPE)); + getCOSObject().setName(COSName.SUBTYPE, SUB_TYPE); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAppearanceCharacteristicsDictionary.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAppearanceCharacteristicsDictionary.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAppearanceCharacteristicsDictionary.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAppearanceCharacteristicsDictionary.java 2018-12-03 16:18:13.000000000 +0000 @@ -108,7 +108,7 @@ */ public String getNormalCaption() { - return this.getCOSObject().getString("CA"); + return this.getCOSObject().getString(COSName.CA); } /** @@ -118,7 +118,7 @@ */ public void setNormalCaption(String caption) { - this.getCOSObject().setString("CA", caption); + this.getCOSObject().setString(COSName.CA, caption); } /** @@ -138,7 +138,7 @@ */ public void setRolloverCaption(String caption) { - this.getCOSObject().setString("RC", caption); + this.getCOSObject().setString(COSName.RC, caption); } /** @@ -148,7 +148,7 @@ */ public String getAlternateCaption() { - return this.getCOSObject().getString("AC"); + return this.getCOSObject().getString(COSName.AC); } /** @@ -158,7 +158,7 @@ */ public void setAlternateCaption(String caption) { - this.getCOSObject().setString("AC", caption); + this.getCOSObject().setString(COSName.AC, caption); } /** @@ -168,7 +168,7 @@ */ public PDFormXObject getNormalIcon() { - COSStream i = this.getCOSObject().getDictionaryObject("I", COSStream.class); + COSStream i = this.getCOSObject().getDictionaryObject(COSName.I, COSStream.class); if (nonNull(i)) { return new PDFormXObject(i); @@ -183,7 +183,7 @@ */ public PDFormXObject getRolloverIcon() { - COSStream i = this.getCOSObject().getDictionaryObject("RI", COSStream.class); + COSStream i = this.getCOSObject().getDictionaryObject(COSName.RI, COSStream.class); if (nonNull(i)) { return new PDFormXObject(i); @@ -198,7 +198,7 @@ */ public PDFormXObject getAlternateIcon() { - COSStream i = this.getCOSObject().getDictionaryObject("IX", COSStream.class); + COSStream i = this.getCOSObject().getDictionaryObject(COSName.IX, COSStream.class); if (nonNull(i)) { return new PDFormXObject(i); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAppearanceEntry.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAppearanceEntry.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAppearanceEntry.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/annotation/PDAppearanceEntry.java 2018-12-03 16:18:13.000000000 +0000 @@ -90,7 +90,7 @@ { if (!isSubDictionary()) { - throw new IllegalStateException(); + throw new IllegalStateException("Expecting a sub-dictionary, but got: " + this.entry); } COSDictionary dict = (COSDictionary) entry; diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/documentnavigation/destination/PDPageDestination.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/documentnavigation/destination/PDPageDestination.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/documentnavigation/destination/PDPageDestination.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/documentnavigation/destination/PDPageDestination.java 2018-12-03 16:18:13.000000000 +0000 @@ -113,7 +113,7 @@ * Returns the page number for this destination, regardless of whether this is a page number or a reference to a * page. * - * @see org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem + * @see org.sejda.sambox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem * @return the 0-based page number, or -1 if the destination type is unknown. */ public int retrievePageNumber() @@ -128,15 +128,15 @@ } else if (page instanceof COSDictionary) { - COSBase parent = page; - while (((COSDictionary) parent).getDictionaryObject(COSName.PARENT, - COSName.P) != null) + COSDictionary parent = (COSDictionary) page; + while (parent.getDictionaryObject(COSName.PARENT, COSName.P, + COSDictionary.class) != null) { - parent = ((COSDictionary) parent).getDictionaryObject(COSName.PARENT, - COSName.P); + parent = parent.getDictionaryObject(COSName.PARENT, COSName.P, + COSDictionary.class); } // now parent is the pages node - PDPageTree pages = new PDPageTree((COSDictionary) parent); + PDPageTree pages = new PDPageTree(parent); return pages.indexOf(new PDPage((COSDictionary) page)); } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/documentnavigation/outline/PDOutlineItem.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/documentnavigation/outline/PDOutlineItem.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/documentnavigation/outline/PDOutlineItem.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/documentnavigation/outline/PDOutlineItem.java 2018-12-03 16:18:13.000000000 +0000 @@ -240,8 +240,8 @@ PDPageDestination pageDestination = null; if (dest instanceof PDNamedDestination) { - pageDestination = doc.getDocumentCatalog().findNamedDestinationPage( - (PDNamedDestination) dest); + pageDestination = doc.getDocumentCatalog() + .findNamedDestinationPage((PDNamedDestination) dest); if (pageDestination == null) { return null; @@ -277,8 +277,8 @@ */ public PDAction getAction() { - return PDActionFactory.createAction((COSDictionary) getCOSObject().getDictionaryObject( - COSName.A)); + return PDActionFactory + .createAction(getCOSObject().getDictionaryObject(COSName.A, COSDictionary.class)); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/AppearanceGeneratorHelper.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/AppearanceGeneratorHelper.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/AppearanceGeneratorHelper.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/AppearanceGeneratorHelper.java 2018-12-03 16:18:13.000000000 +0000 @@ -18,13 +18,17 @@ import static java.util.Arrays.asList; import static java.util.Objects.nonNull; +import static java.util.Optional.ofNullable; import static org.sejda.io.CountingWritableByteChannel.from; +import static org.sejda.util.RequireUtils.requireNotNullArg; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.sejda.sambox.contentstream.operator.Operator; import org.sejda.sambox.cos.COSName; @@ -84,6 +88,8 @@ */ private static final float DEFAULT_FONT_SIZE = 12; + private static final int MINIMUM_LINES_TO_FIT_IN_A_MULTILINE_FIELD = 5; + /** * The default padding applied by Acrobat to the fields bbox. */ @@ -123,6 +129,7 @@ && widget.getNormalAppearanceStream().getResources() != null) { PDResources widgetResources = widget.getNormalAppearanceStream().getResources(); + Map missingFonts = new HashMap<>(); for (COSName fontResourceName : widgetResources.getFontNames()) { try @@ -131,7 +138,7 @@ { LOG.debug("Adding font resource " + fontResourceName + " from widget to AcroForm"); - acroFormResources.put(fontResourceName, + missingFonts.put(fontResourceName, widgetResources.getFont(fontResourceName)); } } @@ -140,6 +147,12 @@ LOG.warn("Unable to match field level font with AcroForm font"); } } + + // add all missing font resources from widget to AcroForm + for (COSName key : missingFonts.keySet()) + { + acroFormResources.put(key, missingFonts.get(key)); + } } } } @@ -253,7 +266,17 @@ { COSString da = (COSString) widget.getCOSObject().getDictionaryObject(COSName.DA); PDResources dr = field.getAcroForm().getDefaultResources(); - return new PDDefaultAppearanceString(da, dr); + try + { + return new PDDefaultAppearanceString(da, dr); + } + catch (IOException ex) + { + LOG.warn( + "Failed to process default appearance string for widget {}, will use fallback default appearance", + widget); + return new PDDefaultAppearanceString(); + } } private int resolveRotation(PDAnnotationWidget widget) @@ -415,24 +438,20 @@ contents.clip(); // get the font - // field's defined appearance font has priority // callers might have determined that the default font does not support rendering the field's value // so the font was substituted to another one, which has better unicode support // see PDVariableText.setAppearanceOverrideFont() - PDFont font = field.getAppearanceFont(); + PDFont font = ofNullable(field.getAppearanceFont()) + .orElseGet(() -> defaultAppearance.getFont()); - // fallback to default appearance - if (font == null) - { - font = defaultAppearance.getFont(); - } + requireNotNullArg(font, "font is null, check whether /DA entry is incomplete or incorrect"); - // calculate the fontSize (because 0 = autosize) float fontSize = defaultAppearance.getFontSize(); if (fontSize == 0) { + // calculate the fontSize (because 0 = autosize) fontSize = calculateFontSize(font, contentRect); } @@ -440,7 +459,7 @@ // options if (field instanceof PDListBox) { - insertGeneratedSelectionHighlight(contents, appearanceStream, font, fontSize); + insertGeneratedListboxSelectionHighlight(contents, appearanceStream, font, fontSize); } // start the text output @@ -459,7 +478,7 @@ if (field instanceof PDTextField && ((PDTextField) field).isMultiline()) { - y = contentRect.getUpperRightY() - fontBoundingBoxAtSize; + y = contentRect.getUpperRightY() - calculateLineHeight(font, fontScaleY); } else { @@ -493,7 +512,7 @@ // chars if (shallComb()) { - insertGeneratedCombAppearance(contents, appearanceStream, font, fontSize); + insertGeneratedCombAppearance(contents, bbox, font, fontSize); } else if (field instanceof PDListBox) { @@ -508,7 +527,7 @@ appearanceStyle.setFontSize(fontSize); // Adobe Acrobat uses the font's bounding box for the leading between the lines - appearanceStyle.setLeading(font.getBoundingBox().getHeight() * fontScaleY); + appearanceStyle.setLeading(calculateLineHeight(font, fontScaleY)); PlainTextFormatter formatter = new PlainTextFormatter.Builder(contents) .style(appearanceStyle).text(textContent).width(contentRect.getWidth()) @@ -574,13 +593,13 @@ * Generate the appearance for comb fields. * * @param contents the content stream to write to - * @param appearanceStream the appearance stream used + * @param bbox the bbox used * @param font the font to be used * @param fontSize the font size to be used * @throws IOException */ - private void insertGeneratedCombAppearance(PDPageContentStream contents, - PDAppearanceStream appearanceStream, PDFont font, float fontSize) throws IOException + private void insertGeneratedCombAppearance(PDPageContentStream contents, PDRectangle bbox, + PDFont font, float fontSize) throws IOException { // TODO: Currently the quadding is not taken into account @@ -589,12 +608,12 @@ int maxLen = ((PDTextField) field).getMaxLen(); int numChars = Math.min(value.length(), maxLen); - PDRectangle paddingEdge = applyPadding(appearanceStream.getBBox(), 1); + PDRectangle paddingEdge = applyPadding(bbox, 1); - float combWidth = appearanceStream.getBBox().getWidth() / maxLen; + float combWidth = bbox.getWidth() / maxLen; float ascentAtFontSize = font.getFontDescriptor().getAscent() / FONTSCALE * fontSize; float baselineOffset = paddingEdge.getLowerLeftY() - + (appearanceStream.getBBox().getHeight() - ascentAtFontSize) / 2; + + (bbox.getHeight() - ascentAtFontSize) / 2; float prevCharWidth = 0f; @@ -621,7 +640,7 @@ contents.restoreGraphicsState(); } - private void insertGeneratedSelectionHighlight(PDPageContentStream contents, + private void insertGeneratedListboxSelectionHighlight(PDPageContentStream contents, PDAppearanceStream appearanceStream, PDFont font, float fontSize) throws IOException { List indexEntries = ((PDListBox) field).getSelectedOptionsIndex(); @@ -710,51 +729,85 @@ contents.newLineAtOffset(contentRect.getLowerLeftX(), yTextPos); contents.showText(options.get(i)); - if (i - topIndex != (numOptions - 1)) + if (i != (numOptions - 1)) { contents.endText(); } } } + private float calculateLineHeight(PDFont font, float fontScaleY) throws IOException + { + float fontBoundingBoxAtSize = font.getBoundingBox().getHeight() * fontScaleY; + float fontCapAtSize = font.getFontDescriptor().getCapHeight() * fontScaleY; + float fontDescentAtSize = font.getFontDescriptor().getDescent() * fontScaleY; + + float lineHeight = fontCapAtSize - fontDescentAtSize; + if (lineHeight < 0) + { + lineHeight = fontBoundingBoxAtSize; + } + + return lineHeight; + } + /** * My "not so great" method for calculating the fontsize. It does not work superb, but it handles ok. * * @return the calculated font-size * @throws IOException If there is an error getting the font information. */ - private float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOException + float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOException { - float fontSize = defaultAppearance.getFontSize(); + float yScalingFactor = FONTSCALE * font.getFontMatrix().getScaleY(); + float xScalingFactor = FONTSCALE * font.getFontMatrix().getScaleX(); - // zero is special, it means the text is auto-sized - if (fontSize == 0) + if (isMultiLine()) { - if (isMultiLine()) - { - // Acrobat defaults to 12 for multiline text with size 0 - return DEFAULT_FONT_SIZE; - } - float yScalingFactor = FONTSCALE * font.getFontMatrix().getScaleY(); - float xScalingFactor = FONTSCALE * font.getFontMatrix().getScaleX(); + // Acrobat defaults to 12 for multiline text with size 0 + // PDFBOX decided to just return that and finish with it + // return DEFAULT_FONT_SIZE; + + // SAMBOX specifics below + // We calculate a font size that fits at least 5 lines + // We detect faux multiline fields (text fields flagged as multiline which have a small height to just fit + // one line) + + float lineHeight = calculateLineHeight(font, font.getFontMatrix().getScaleY()); + float scaledContentHeight = contentRect.getHeight() * yScalingFactor; + + boolean looksLikeFauxMultiline = calculateLineHeight(font, + DEFAULT_FONT_SIZE / FONTSCALE) > scaledContentHeight; + boolean userTypedMultipleLines = new PlainText(value).getParagraphs().size() > 1; - // fit width - float width = font.getStringWidth(value) * font.getFontMatrix().getScaleX(); - float widthBasedFontSize = contentRect.getWidth() / width * xScalingFactor; + if (looksLikeFauxMultiline && !userTypedMultipleLines) + { + // faux multiline detected + // because 1 line written with the default font size would not fit the height + // just continue to the non multiline part of the algorithm - // fit height - float height = (font.getFontDescriptor().getCapHeight() - + -font.getFontDescriptor().getDescent()) * font.getFontMatrix().getScaleY(); - if (height <= 0) + LOG.warn("Faux multiline field found: {}", field.getFullyQualifiedName()); + } + else { - height = font.getBoundingBox().getHeight() * font.getFontMatrix().getScaleY(); + // calculate a font size which fits at least x lines + float fontSize = scaledContentHeight + / (MINIMUM_LINES_TO_FIT_IN_A_MULTILINE_FIELD * lineHeight); + // don't return a font size larger than the default + return Math.min(fontSize, DEFAULT_FONT_SIZE); } + } - float heightBasedFontSize = contentRect.getHeight() / height * yScalingFactor; + // fit width + float width = font.getStringWidth(value) * font.getFontMatrix().getScaleX(); + float widthBasedFontSize = contentRect.getWidth() / width * xScalingFactor; - return Math.min(heightBasedFontSize, widthBasedFontSize); - } - return fontSize; + // fit height + float height = calculateLineHeight(font, font.getFontMatrix().getScaleY()); + + float heightBasedFontSize = contentRect.getHeight() / height * yScalingFactor; + + return Math.min(heightBasedFontSize, widthBasedFontSize); } /** @@ -768,13 +821,18 @@ PDAppearanceStream appearanceStream) { PDRectangle boundingBox = appearanceStream.getBBox(); - if (boundingBox == null) + if (boundingBox == null || hasZeroDimensions(boundingBox)) { boundingBox = fieldWidget.getRectangle().createRetranslatedRectangle(); } return boundingBox; } + private boolean hasZeroDimensions(PDRectangle bbox) + { + return bbox.getHeight() == 0 || bbox.getWidth() == 0; + } + /** * Apply padding to a box. * diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/FieldUtils.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/FieldUtils.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/FieldUtils.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/FieldUtils.java 2018-12-03 16:18:13.000000000 +0000 @@ -173,23 +173,24 @@ } else if (items instanceof COSArray) { - COSArray array = (COSArray) items; - - List result = new ArrayList<>(); - int numItems = ((COSArray) items).size(); - for (int i = 0; i < numItems; i++) + List entryList = new ArrayList<>(); + for (COSBase entry : (COSArray) items) { - COSBase item = array.get(i); - if(item instanceof COSArray) + if (entry instanceof COSString) { - COSArray pair = (COSArray) array.get(i); - COSString displayValue = (COSString) pair.get(pairIdx); - result.add(displayValue.getString()); - } else if(item instanceof COSString) { - result.add(((COSString) item).getString()); + 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 result; + return entryList; } return Collections.emptyList(); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDAcroForm.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDAcroForm.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDAcroForm.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDAcroForm.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,14 +16,19 @@ */ package org.sejda.sambox.pdmodel.interactive.form; +import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import static java.util.Optional.ofNullable; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; import org.sejda.sambox.cos.COSArray; import org.sejda.sambox.cos.COSArrayList; @@ -174,6 +179,12 @@ */ public void flatten(List 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()) @@ -194,22 +205,24 @@ // the content stream to write to PDPageContentStream contentStream; + Map toFlatten = widgets(fields); // preserve all non widget annotations for (PDPage page : document.getPages()) { isContentStreamWrapped = false; - List annotations = new ArrayList(); + List annotations = new ArrayList<>(); for (PDAnnotation annotation : page.getAnnotations()) { - if (!(annotation instanceof PDAnnotationWidget)) + PDAnnotationWidget widget = toFlatten.get(annotation.getCOSObject()); + if (isNull(widget)) { annotations.add(annotation); } else if (!annotation.isInvisible() && !annotation.isHidden() - && annotation.getNormalAppearanceStream() != null) + && nonNull(annotation.getNormalAppearanceStream())) { if (!isContentStreamWrapped) { @@ -276,8 +289,7 @@ page.setAnnotations(annotations); } - // remove the fields - setFields(Collections. emptyList()); + removeFields(fields); // remove XFA for hybrid forms getCOSObject().removeItem(COSName.XFA); @@ -332,20 +344,19 @@ */ public List getFields() { - List pdFields = new ArrayList<>(); - COSArray fields = getCOSObject().getDictionaryObject(COSName.FIELDS, COSArray.class); - if (nonNull(fields)) + return fieldsFromArray(getCOSObject().getDictionaryObject(COSName.FIELDS, COSArray.class)); + } + + private List fieldsFromArray(COSArray array) + { + if (nonNull(array) && array.size() > 0) { - for (COSBase field : fields) - { - if (nonNull(field) && field.getCOSObject() instanceof COSDictionary) - { - pdFields.add(PDField.fromDictionary(this, (COSDictionary) field.getCOSObject(), - null)); - } - } + return array.stream().filter(Objects::nonNull).map(COSBase::getCOSObject) + .filter(d -> d instanceof COSDictionary) + .map(d -> PDField.fromDictionary(this, (COSDictionary) d, null)) + .collect(Collectors.toList()); } - return pdFields; + return new ArrayList<>(); } /** @@ -447,6 +458,16 @@ getCOSObject().setString(COSName.DA, daValue); } + public List getCalculationOrder() + { + return fieldsFromArray(getCOSObject().getDictionaryObject(COSName.CO, COSArray.class)); + } + + public void setCalculationOrder(COSArray co) + { + getCOSObject().setItem(COSName.CO, co); + } + /** * True if the viewing application should construct the appearances of all field widgets. The default value is * false. @@ -601,7 +622,7 @@ */ private boolean resolveNeedsTranslation(PDAppearanceStream appearanceStream) { - boolean needsTranslation = false; + boolean needsTranslation = true; PDResources resources = appearanceStream.getResources(); if (resources != null && resources.getXObjectNames().iterator().hasNext()) @@ -621,9 +642,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; } } } @@ -651,4 +672,34 @@ PDResources resources = appearanceStream.getResources(); return resources != null && resources.getXObjectNames().iterator().hasNext(); } + + private Map widgets(List fields) + { + return fields.stream().flatMap(f -> f.getWidgets().stream()) + .collect(toMap(w -> w.getCOSObject(), identity())); + + } + + private void removeFields(List fields) + { + for (PDField current : fields) + { + if (current.isTerminal()) + { + if (nonNull(current.getParent())) + { + current.getParent().removeChild(current); + } + else + { + // it's a root field + removeField(current); + } + } + else + { + LOG.warn("Unable to remove non terminal field {}", current.getFullyQualifiedName()); + } + } + } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDButton.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDButton.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDButton.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDButton.java 2018-12-03 16:18:13.000000000 +0000 @@ -32,6 +32,8 @@ import org.sejda.sambox.pdmodel.interactive.annotation.PDAnnotationWidget; import org.sejda.sambox.pdmodel.interactive.annotation.PDAppearanceDictionary; import org.sejda.sambox.pdmodel.interactive.annotation.PDAppearanceEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A button field represents an interactive control on the screen that the user can manipulate with the mouse. @@ -40,6 +42,9 @@ */ public abstract class PDButton extends PDTerminalField { + + private static final Logger LOG = LoggerFactory.getLogger(PDButton.class); + /** * A Ff flag. If set, the field is a set of radio buttons */ @@ -131,7 +136,9 @@ { return ((COSName) value).getName(); } - return ""; + // Off is the default value if there is nothing else set. + // See PDF Spec. + return "Off"; } /** @@ -297,13 +304,14 @@ return onValues; } - public List getNormalAppearanceValues() { + public List getNormalAppearanceValues() + { List values = new ArrayList<>(); List widgets = this.getWidgets(); for (PDAnnotationWidget widget : widgets) { String value = getOnValueForWidget(widget); - if(value != null) + if (value != null) { values.add(value); } @@ -338,14 +346,22 @@ PDAppearanceEntry normalAppearance = apDictionary.getNormalAppearance(); if (normalAppearance != null) { - Set entries = normalAppearance.getSubDictionary().keySet(); - for (COSName entry : entries) + try { - if (COSName.Off.compareTo(entry) != 0) + Set entries = normalAppearance.getSubDictionary().keySet(); + for (COSName entry : entries) { - return entry.getName(); + if (COSName.Off.compareTo(entry) != 0) + { + return entry.getName(); + } } } + catch (IllegalStateException ex) + { + LOG.warn("Could not parse normal appearances sub-dictionary for field {}", + this.getFullyQualifiedName()); + } } } // PDFBox returns an empty string here. @@ -364,7 +380,8 @@ { Set onValues = getOnValues(); - if(onValues.isEmpty()) { + if (onValues.isEmpty()) + { return; } @@ -384,14 +401,16 @@ { boolean matchesAppearance = false; // don't crash when there's no appearances (eg: checkboxes) - if(widget.getAppearance() != null && widget.getAppearance().getNormalAppearance() != null) + if (widget.getAppearance() != null + && widget.getAppearance().getNormalAppearance() != null) { - matchesAppearance =((COSDictionary) widget.getAppearance().getNormalAppearance().getCOSObject()) - .containsKey(value); + matchesAppearance = ((COSDictionary) widget.getAppearance().getNormalAppearance() + .getCOSObject()).containsKey(value); } // checkbox with no appearances scenario - if(!COSName.OFF.getName().equals(value) && widget.getAppearance() == null && getWidgets().size() == 1) + if (!COSName.OFF.getName().equals(value) && widget.getAppearance() == null + && getWidgets().size() == 1) { matchesAppearance = true; } @@ -406,7 +425,6 @@ } } - } private void updateByOption(String value) throws IOException @@ -435,7 +453,7 @@ if (optionsIndex != -1) { String onValue = getOnValue(optionsIndex); - if(onValue != null) + if (onValue != null) { updateByValue(onValue); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDDefaultAppearanceString.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDDefaultAppearanceString.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDDefaultAppearanceString.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDDefaultAppearanceString.java 2018-12-03 16:18:13.000000000 +0000 @@ -92,6 +92,10 @@ processAppearanceStringOperators(defaultAppearance.getBytes()); } + PDDefaultAppearanceString() throws IOException { + this(null, null); + } + /** * Processes the operators of the given content stream. * @@ -248,7 +252,7 @@ /** * Returns the font. */ - PDFont getFont() throws IOException + PDFont getFont() { return font; } @@ -309,7 +313,11 @@ { fontSize = zeroFontSize; } - contents.setFont(getFont(), fontSize); + + if(getFont() != null) + { + contents.setFont(getFont(), fontSize); + } if (getFontColor() != null) { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDFieldFactory.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDFieldFactory.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDFieldFactory.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDFieldFactory.java 2018-12-03 16:18:13.000000000 +0000 @@ -120,8 +120,8 @@ String retval = dic.getNameAsString(COSName.FT); if (retval == null) { - COSDictionary parent = (COSDictionary) dic.getDictionaryObject(COSName.PARENT, - COSName.P); + COSDictionary parent = dic.getDictionaryObject(COSName.PARENT, COSName.P, + COSDictionary.class); if (parent != null) { retval = findFieldType(parent); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDNonTerminalField.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDNonTerminalField.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDNonTerminalField.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDNonTerminalField.java 2018-12-03 16:18:13.000000000 +0000 @@ -30,6 +30,8 @@ import org.sejda.sambox.cos.COSInteger; import org.sejda.sambox.cos.COSName; import org.sejda.sambox.pdmodel.interactive.annotation.PDAnnotationWidget; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A non terminal field in an interactive form. @@ -41,6 +43,9 @@ */ public class PDNonTerminalField extends PDField { + + private static final Logger LOG = LoggerFactory.getLogger(PDNonTerminalField.class); + /** * Constructor. * @@ -89,6 +94,11 @@ { if (nonNull(kid) && kid.getCOSObject() instanceof COSDictionary) { + if (kid.getCOSObject() == this.getCOSObject()) + { + LOG.warn("Child field is same object as parent"); + continue; + } children.add(PDField.fromDictionary(getAcroForm(), (COSDictionary) kid.getCOSObject(), this)); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDTerminalField.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDTerminalField.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDTerminalField.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDTerminalField.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,19 +16,20 @@ */ package org.sejda.sambox.pdmodel.interactive.form; +import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.io.IOException; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.sejda.sambox.cos.COSArray; import org.sejda.sambox.cos.COSArrayList; -import org.sejda.sambox.cos.COSBase; import org.sejda.sambox.cos.COSDictionary; import org.sejda.sambox.cos.COSInteger; import org.sejda.sambox.cos.COSName; -import org.sejda.sambox.cos.COSNull; import org.sejda.sambox.pdmodel.interactive.action.PDFormFieldAdditionalActions; import org.sejda.sambox.pdmodel.interactive.annotation.PDAnnotationWidget; @@ -106,25 +107,21 @@ @Override public List getWidgets() { - List widgets = new ArrayList<>(); - COSArray kids = (COSArray) getCOSObject().getDictionaryObject(COSName.KIDS); - if (kids == null) + COSArray kids = getCOSObject().getDictionaryObject(COSName.KIDS, COSArray.class); + if (isNull(kids)) { // the field itself is a widget - widgets.add(new PDAnnotationWidget(getCOSObject())); + return Arrays.asList(new PDAnnotationWidget(getCOSObject())); } - else if (kids.size() > 0) + if (kids.size() > 0) { - // there are multiple widgets - for (COSBase kid : kids) - { - if (nonNull(kid) && !COSNull.NULL.equals(kid.getCOSObject())) - { - widgets.add(new PDAnnotationWidget((COSDictionary) kid.getCOSObject())); - } - } + return kids.stream().filter(k -> nonNull(k)).map(k -> k.getCOSObject()) + .filter(k -> k instanceof COSDictionary) + .map(k -> new PDAnnotationWidget((COSDictionary) k)) + .collect(Collectors.toList()); + } - return widgets; + return Collections.emptyList(); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDVariableText.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDVariableText.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDVariableText.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/form/PDVariableText.java 2018-12-03 16:18:13.000000000 +0000 @@ -91,9 +91,16 @@ */ PDDefaultAppearanceString getDefaultAppearanceString() throws IOException { - COSString da = (COSString) getInheritableAttribute(COSName.DA); - PDResources dr = getAcroForm().getDefaultResources(); - return new PDDefaultAppearanceString(da, dr); + try + { + COSString da = (COSString) getInheritableAttribute(COSName.DA); + PDResources dr = getAcroForm().getDefaultResources(); + return new PDDefaultAppearanceString(da, dr); + } + catch (IOException ex) + { + return new PDDefaultAppearanceString(); + } } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/measurement/PDViewportDictionary.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/measurement/PDViewportDictionary.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/interactive/measurement/PDViewportDictionary.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/interactive/measurement/PDViewportDictionary.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,10 +16,7 @@ */ package org.sejda.sambox.pdmodel.interactive.measurement; -import org.sejda.sambox.cos.COSArray; -import org.sejda.sambox.cos.COSDictionary; -import org.sejda.sambox.cos.COSName; -import org.sejda.sambox.cos.COSObjectable; +import org.sejda.sambox.cos.*; import org.sejda.sambox.pdmodel.common.PDRectangle; /** @@ -34,7 +31,7 @@ */ public static final String TYPE = "Viewport"; - private COSDictionary viewportDictionary; + private final COSDictionary viewportDictionary; /** * Constructor. @@ -83,10 +80,10 @@ */ public PDRectangle getBBox() { - COSArray bbox = (COSArray)this.getCOSObject().getDictionaryObject("BBox"); - if (bbox != null) + COSBase bbox = this.getCOSObject().getDictionaryObject(COSName.BBOX); + if (bbox instanceof COSArray) { - return new PDRectangle(bbox); + return new PDRectangle((COSArray) bbox); } return null; } @@ -98,7 +95,7 @@ */ public void setBBox(PDRectangle rectangle) { - this.getCOSObject().setItem("BBox", rectangle); + this.getCOSObject().setItem(COSName.BBOX, rectangle); } /** @@ -128,10 +125,10 @@ */ public PDMeasureDictionary getMeasure() { - COSDictionary measure = (COSDictionary)this.getCOSObject().getDictionaryObject("Measure"); - if (measure != null) + COSBase base = this.getCOSObject().getDictionaryObject(COSName.MEASURE); + if (base instanceof COSDictionary) { - return new PDMeasureDictionary(measure); + return new PDMeasureDictionary((COSDictionary) base); } return null; } @@ -143,7 +140,7 @@ */ public void setMeasure(PDMeasureDictionary measure) { - this.getCOSObject().setItem("Measure", measure); + this.getCOSObject().setItem(COSName.MEASURE, measure); } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/PDDocumentCatalog.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/PDDocumentCatalog.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/PDDocumentCatalog.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/PDDocumentCatalog.java 2018-12-03 16:18:13.000000000 +0000 @@ -16,6 +16,7 @@ */ package org.sejda.sambox.pdmodel; +import static java.util.Objects.nonNull; import static java.util.Optional.ofNullable; import static org.sejda.sambox.util.SpecVersionUtils.V1_5; @@ -241,27 +242,23 @@ * Get the Document Open Action for this object. * * @return The action to perform when the document is opened. - * @throws IOException If there is an error creating the destination or action. */ public PDDestinationOrAction getOpenAction() throws IOException { COSBase openAction = root.getDictionaryObject(COSName.OPEN_ACTION); - if (openAction == null) + if (nonNull(openAction)) { - return null; - } - else if (openAction instanceof COSDictionary) - { - return PDActionFactory.createAction((COSDictionary) openAction); - } - else if (openAction instanceof COSArray) - { - return PDDestination.create(openAction); - } - else - { - throw new IOException("Unknown OpenAction " + openAction); + if (openAction instanceof COSDictionary) + { + return PDActionFactory.createAction((COSDictionary) openAction); + } + else if (openAction instanceof COSArray) + { + return PDDestination.create(openAction); + } + LOG.warn("Invalid OpenAction {}", openAction); } + return null; } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/PDDocument.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/PDDocument.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/PDDocument.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/PDDocument.java 2018-12-03 16:18:13.000000000 +0000 @@ -36,10 +36,13 @@ import java.security.MessageDigest; import java.util.Calendar; import java.util.HashSet; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import org.apache.fontbox.ttf.TrueTypeFont; import org.sejda.io.CountingWritableByteChannel; +import org.sejda.io.SeekableSources; import org.sejda.sambox.cos.COSArray; import org.sejda.sambox.cos.COSBase; import org.sejda.sambox.cos.COSDictionary; @@ -52,13 +55,14 @@ import org.sejda.sambox.encryption.EncryptionContext; import org.sejda.sambox.encryption.MessageDigests; import org.sejda.sambox.encryption.StandardSecurity; +import org.sejda.sambox.input.PDFParser; import org.sejda.sambox.output.PDDocumentWriter; import org.sejda.sambox.output.WriteOption; import org.sejda.sambox.pdmodel.common.PDStream; import org.sejda.sambox.pdmodel.encryption.AccessPermission; import org.sejda.sambox.pdmodel.encryption.PDEncryption; import org.sejda.sambox.pdmodel.encryption.SecurityHandler; -import org.sejda.sambox.pdmodel.font.PDFont; +import org.sejda.sambox.pdmodel.font.Subsettable; import org.sejda.sambox.pdmodel.graphics.color.PDDeviceRGB; import org.sejda.sambox.util.Version; import org.sejda.util.IOUtils; @@ -104,11 +108,11 @@ private PDDocumentCatalog documentCatalog; private SecurityHandler securityHandler; private boolean open = true; - private OnClose onClose; + private OnClose onClose = () -> LOG.debug("Closing document"); private ResourceCache resourceCache = new DefaultResourceCache(); // fonts to subset before saving - private final Set fontsToSubset = new HashSet<>(); + private final Set fontsToSubset = new HashSet<>(); public PDDocument() { @@ -284,9 +288,21 @@ } /** + * 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) + { + onClose.andThen(() -> IOUtils.closeQuietly(ttf)); + } + + /** * @return the list of fonts which will be subset before the document is saved. */ - Set getFontsToSubset() + public Set getFontsToSubset() { return fontsToSubset; } @@ -396,7 +412,7 @@ public void setOnCloseAction(OnClose onClose) { requireOpen(); - this.onClose = onClose; + this.onClose = onClose.andThen(this.onClose); } private void requireOpen() throws IllegalStateException @@ -569,7 +585,7 @@ requireOpen(); getDocumentInformation().setProducer("SAMBox " + Version.getVersion() + " (www.sejda.org)"); getDocumentInformation().setModificationDate(Calendar.getInstance()); - for (PDFont font : fontsToSubset) + for (Subsettable font : fontsToSubset) { font.subset(); } @@ -605,10 +621,7 @@ @Override public void close() throws IOException { - if (onClose != null) - { - onClose.onClose(); - } + onClose.onClose(); this.resourceCache.clear(); this.open = false; } @@ -627,6 +640,15 @@ * @param onClose */ void onClose() throws IOException; + + default OnClose andThen(OnClose after) + { + Objects.requireNonNull(after); + return () -> { + onClose(); + after.onClose(); + }; + } } /** @@ -637,4 +659,10 @@ return resourceCache; } + // bridge to pdfbox style api, used in tests + public static PDDocument load(File file) throws IOException + { + return PDFParser.parse(SeekableSources.seekableSourceFrom(file)); + } + } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/PDPageContentStream.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/PDPageContentStream.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/PDPageContentStream.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/PDPageContentStream.java 2018-12-03 16:18:13.000000000 +0000 @@ -52,6 +52,7 @@ import org.sejda.sambox.pdmodel.graphics.form.PDFormXObject; import org.sejda.sambox.pdmodel.graphics.image.PDImageXObject; import org.sejda.sambox.pdmodel.graphics.image.PDInlineImage; +import org.sejda.sambox.pdmodel.graphics.pattern.PDTilingPattern; import org.sejda.sambox.pdmodel.graphics.shading.PDShading; import org.sejda.sambox.pdmodel.graphics.state.PDExtendedGraphicsState; import org.sejda.sambox.pdmodel.graphics.state.RenderingMode; @@ -263,6 +264,26 @@ } /** + * Create a new appearance stream. Note that this is not actually a "page" content stream. + * + * @param doc The document the appearance is part of. + * @param pattern The pattern to add to. + * @param outputStream The output stream to write to. + * @throws IOException If there is an error writing to the page contents. + */ + public PDPageContentStream(PDDocument doc, PDTilingPattern pattern, ContentStreamWriter writer) + throws IOException + { + this.document = doc; + + this.writer = writer; + this.resources = pattern.getResources(); + + formatDecimal.setMaximumFractionDigits(4); + formatDecimal.setGroupingUsed(false); + } + + /** * Begin some text operations. * * @throws IOException If there is an error writing to the stream or if you attempt to nest beginText calls. @@ -332,6 +353,40 @@ } /** + * Shows the given text at the location specified by the current text matrix with the given interspersed + * positioning. This allows the user to efficiently position each glyph or sequence of glyphs. + * + * @param textWithPositioningArray An array consisting of String and Float types. Each String is output to the page + * using the current text matrix. Using the default coordinate system, each interspersed number adjusts the current + * text matrix by translating to the left or down for horizontal and vertical text respectively. The number is + * expressed in thousands of a text space unit, and may be negative. + * + * @throws IOException if an io exception occurs. + */ + public void showTextWithPositioning(Object[] textWithPositioningArray) throws IOException + { + write("["); + for (Object obj : textWithPositioningArray) + { + if (obj instanceof String) + { + showTextInternal((String) obj); + } + else if (obj instanceof Float) + { + writeOperand((Float) obj); + } + else + { + throw new IllegalArgumentException( + "Argument must consist of array of Float and String types"); + } + } + write("] "); + writeOperator("TJ"); + } + + /** * Shows the given text at the location specified by the current text matrix. * * @param text The Unicode text to show. @@ -339,6 +394,19 @@ */ public void showText(String text) throws IOException { + showTextInternal(text); + writer.writeSpace(); + writeOperator("Tj"); + } + + /** + * Shows the given text at the location specified by the current text matrix. + * + * @param text The Unicode text to show. + * @throws IOException If an io exception occurs. + */ + protected void showTextInternal(String text) throws IOException + { if (!inTextMode) { throw new IllegalStateException("Must call beginText() before showText()"); @@ -354,7 +422,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); @@ -363,8 +432,6 @@ } COSString.newInstance(font.encode(text)).accept(writer); - writer.writeSpace(); - writeOperator("Tj"); } /** @@ -373,9 +440,9 @@ * @param leading The leading in unscaled text units. * @throws IOException If there is an error writing to the stream. */ - public void setLeading(double leading) throws IOException + public void setLeading(float leading) throws IOException { - writeOperand((float) leading); + writeOperand(leading); writeOperator("TL"); } @@ -635,6 +702,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"); } @@ -646,6 +718,10 @@ */ 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()); @@ -668,6 +744,10 @@ */ public void restoreGraphicsState() throws IOException { + if (inTextMode) + { + LOG.warn("Restoring the graphics state is not allowed within text objects."); + } if (!fontStack.isEmpty()) { fontStack.pop(); @@ -690,10 +770,7 @@ { return COSName.getPDFName(colorSpace.getName()); } - else - { - return resources.add(colorSpace); - } + return resources.add(colorSpace); } public void setTextRenderingMode(RenderingMode renderingMode) throws IOException @@ -812,13 +889,13 @@ * @throws IOException If an IO error occurs while writing to the stream. * @throws IllegalArgumentException If the parameter is invalid. */ - public void setStrokingColor(double g) throws IOException + public void setStrokingColor(float g) throws IOException { if (isOutsideOneInterval(g)) { throw new IllegalArgumentException("Parameter must be within 0..1, but is " + g); } - writeOperand((float) g); + writeOperand(g); writeOperator("G"); setStrokingColorSpaceStack(PDDeviceGray.INSTANCE); } @@ -929,7 +1006,7 @@ * @param k The black value. * @throws IOException If an IO error occurs while writing to the stream. */ - public void setNonStrokingColor(double c, double m, double y, double k) throws IOException + public void setNonStrokingColor(float c, float m, float y, float k) throws IOException { if (isOutsideOneInterval(c) || isOutsideOneInterval(m) || isOutsideOneInterval(y) || isOutsideOneInterval(k)) @@ -937,10 +1014,10 @@ throw new IllegalArgumentException("Parameters must be within 0..1, but are " + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k)); } - writeOperand((float) c); - writeOperand((float) m); - writeOperand((float) y); - writeOperand((float) k); + writeOperand(c); + writeOperand(m); + writeOperand(y); + writeOperand(k); writeOperator("k"); setNonStrokingColorSpaceStack(PDDeviceCMYK.INSTANCE); } @@ -1569,12 +1646,12 @@ IOUtils.close(writer); } - private boolean isOutside255Interval(int val) + private static boolean isOutside255Interval(int val) { return val < 0 || val > 255; } - private boolean isOutsideOneInterval(double val) + private static boolean isOutsideOneInterval(double val) { return val < 0 || val > 1; } @@ -1654,4 +1731,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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/PDPage.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/PDPage.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/PDPage.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/PDPage.java 2018-12-03 16:18:13.000000000 +0000 @@ -44,7 +44,9 @@ import org.sejda.sambox.pdmodel.common.PDRectangle; import org.sejda.sambox.pdmodel.common.PDStream; import org.sejda.sambox.pdmodel.interactive.action.PDPageAdditionalActions; +import org.sejda.sambox.pdmodel.interactive.annotation.AnnotationFilter; import org.sejda.sambox.pdmodel.interactive.annotation.PDAnnotation; +import org.sejda.sambox.pdmodel.interactive.measurement.PDViewportDictionary; import org.sejda.sambox.pdmodel.interactive.pagenavigation.PDThreadBead; import org.sejda.sambox.pdmodel.interactive.pagenavigation.PDTransition; import org.sejda.sambox.util.Matrix; @@ -272,10 +274,10 @@ { if (mediaBox == null) { - COSArray array = (COSArray) PDPageTree.getInheritableAttribute(page, COSName.MEDIA_BOX); - if (array != null) + COSBase base = PDPageTree.getInheritableAttribute(page, COSName.MEDIA_BOX); + if (base instanceof COSArray) { - mediaBox = new PDRectangle(array); + mediaBox = new PDRectangle((COSArray) base); } } if (mediaBox == null) @@ -316,11 +318,19 @@ */ public PDRectangle getCropBox() { - COSArray array = (COSArray) PDPageTree.getInheritableAttribute(page, COSName.CROP_BOX); - if (array != null) + try { - return clipToMediaBox(new PDRectangle(array)); + COSBase base = PDPageTree.getInheritableAttribute(page, COSName.CROP_BOX); + if (base instanceof COSArray) + { + return clipToMediaBox(new PDRectangle((COSArray) base)); + } + } + catch (Exception ex) + { + LOG.debug("An error occurred parsing the crop box", ex); } + return getMediaBox(); } @@ -355,10 +365,20 @@ */ public PDRectangle getBleedBox() { - COSArray array = page.getDictionaryObject(COSName.BLEED_BOX, COSArray.class); - if (nonNull(array) && inMediaBoxBounds(new PDRectangle(array))) + try { - return new PDRectangle(array); + COSBase base = page.getDictionaryObject(COSName.BLEED_BOX); + if (base instanceof COSArray) + { + COSArray array = (COSArray) base; + if(inMediaBoxBounds(new PDRectangle(array))) { + return new PDRectangle((COSArray) base); + } + } + } + catch (Exception ex) + { + LOG.debug("An error occurred parsing page bleed box", ex); } return getCropBox(); } @@ -394,10 +414,21 @@ */ public PDRectangle getTrimBox() { - COSArray array = page.getDictionaryObject(COSName.TRIM_BOX, COSArray.class); - if (nonNull(array) && inMediaBoxBounds(new PDRectangle(array))) + try + { + COSBase base = page.getDictionaryObject(COSName.TRIM_BOX); + if (base instanceof COSArray) + { + COSArray array = (COSArray) base; + if(inMediaBoxBounds(new PDRectangle(array))) + { + return new PDRectangle(array); + } + } + } + catch (Exception ex) { - return new PDRectangle(array); + LOG.debug("An error occurred parsing page trim box", ex); } return getCropBox(); } @@ -433,10 +464,21 @@ */ public PDRectangle getArtBox() { - COSArray array = page.getDictionaryObject(COSName.ART_BOX, COSArray.class); - if (nonNull(array) && inMediaBoxBounds(new PDRectangle(array))) + try + { + COSBase base = page.getDictionaryObject(COSName.ART_BOX); + if (base instanceof COSArray) + { + COSArray array = (COSArray) base; + if(inMediaBoxBounds(new PDRectangle(array))) + { + return new PDRectangle(array); + } + } + } + catch (Exception ex) { - return new PDRectangle(array); + LOG.debug("An error occurred parsing page art box", ex); } return getCropBox(); } @@ -665,12 +707,34 @@ } /** - * This will return a list of the Annotations for this page. - * - * @return List of the PDAnnotation objects, never null. + * This will return a list of the annotations for this page. + * + * @return List of the PDAnnotation objects, never null. The returned list is backed by the + * annotations COSArray, so any adding or deleting in this list will change the document too. + * */ public List getAnnotations() { + return getAnnotations(new AnnotationFilter() + { + @Override + public boolean accept(PDAnnotation annotation) + { + return true; + } + }); + } + + /** + * This will return a list of the annotations for this page. + * + * @param annotationFilter the annotation filter provided allowing to filter out specific annotations + * @return List of the PDAnnotation objects, never null. The returned list is backed by the + * annotations COSArray, so any adding or deleting in this list will change the document too. + * + */ + public List getAnnotations(AnnotationFilter annotationFilter) + { COSArray annots = page.getDictionaryObject(COSName.ANNOTS, COSArray.class); if (annots == null) { @@ -686,7 +750,7 @@ LOG.warn("Ignored annotation expected to be a dictionary but was {}", item); return null; }); - if (nonNull(annotation)) + if (nonNull(annotation) && annotationFilter.accept(annotation)) { actuals.add(annotation); } @@ -755,4 +819,53 @@ { return resourceCache; } + + /** + * Get the viewports. + * + * @return a list of viewports or null if there is no /VP entry. + */ + public List getViewports() + { + COSBase base = page.getDictionaryObject(COSName.VP); + if (!(base instanceof COSArray)) + { + return null; + } + COSArray array = (COSArray) base; + List viewports = new ArrayList(); + for (int i = 0; i < array.size(); ++i) + { + COSBase base2 = array.getObject(i); + if (base2 instanceof COSDictionary) + { + viewports.add(new PDViewportDictionary((COSDictionary) base2)); + } + else + { + LOG.warn("Array element {} is skipped, must be a (viewport) dictionary", base2); + } + } + return viewports; + } + + /** + * Set the viewports. + * + * @param viewports A list of viewports, or null if the entry is to be deleted. + */ + public void setViewports(List viewports) + { + if (viewports == null) + { + page.removeItem(COSName.VP); + return; + } + COSArray array = new COSArray(); + for (PDViewportDictionary viewport : viewports) + { + array.add(viewport); + } + page.setItem(COSName.VP, array); + } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/PDPageTree.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/PDPageTree.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/pdmodel/PDPageTree.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/pdmodel/PDPageTree.java 2018-12-03 16:18:13.000000000 +0000 @@ -118,7 +118,8 @@ return value; } - COSDictionary parent = (COSDictionary) node.getDictionaryObject(COSName.PARENT, COSName.P); + COSDictionary parent = node.getDictionaryObject(COSName.PARENT, COSName.P, + COSDictionary.class); if (parent != null) { return getInheritableAttribute(parent, key); @@ -303,7 +304,9 @@ { if (pageNum < 0) { - throw new PageNotFoundException("Index out of bounds: " + pageNum + " in " + getSourcePath(), pageNum, getSourcePath()); + throw new PageNotFoundException( + "Index out of bounds: " + pageNum + " in " + getSourcePath(), pageNum, + getSourcePath()); } if (isPageTreeNode(node)) @@ -338,22 +341,25 @@ } throw new PageNotFoundException( - "Unable to find page " + pageNum + " in " + getSourcePath(), pageNum, getSourcePath()); + "Unable to find page " + pageNum + " in " + getSourcePath(), pageNum, + getSourcePath()); } throw new PageNotFoundException( - "Index out of bounds: " + pageNum + " in " + getSourcePath(), pageNum, getSourcePath()); + "Index out of bounds: " + pageNum + " in " + getSourcePath(), pageNum, + getSourcePath()); } if (encountered == pageNum) { return node; } - throw new PageNotFoundException("Unable to find page " + pageNum + " in " + getSourcePath(), pageNum, getSourcePath()); + throw new PageNotFoundException("Unable to find page " + pageNum + " in " + getSourcePath(), + pageNum, getSourcePath()); } - private String getSourcePath() { - return ofNullable(getCOSObject().id()) - .map(i -> i.ownerIdentifier).orElse("Unknown"); + private String getSourcePath() + { + return ofNullable(getCOSObject().id()).map(i -> i.ownerIdentifier).orElse("Unknown"); } /** @@ -462,14 +468,15 @@ private void remove(COSDictionary node) { // remove from parent's kids - COSDictionary parent = (COSDictionary) node.getDictionaryObject(COSName.PARENT, COSName.P); + COSDictionary parent = node.getDictionaryObject(COSName.PARENT, COSName.P, + COSDictionary.class); COSArray kids = parent.getDictionaryObject(COSName.KIDS, COSArray.class); if (kids.removeObject(node)) { // update ancestor counts do { - node = (COSDictionary) node.getDictionaryObject(COSName.PARENT, COSName.P); + node = node.getDictionaryObject(COSName.PARENT, COSName.P, COSDictionary.class); if (node != null) { node.setInt(COSName.COUNT, node.getInt(COSName.COUNT) - 1); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/GroupGraphics.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/GroupGraphics.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/GroupGraphics.java 1970-01-01 00:00:00.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/GroupGraphics.java 2018-12-03 16:18:13.000000000 +0000 @@ -0,0 +1,723 @@ +/* + * 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.sejda.sambox.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 / 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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/ImageType.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/ImageType.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/ImageType.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/ImageType.java 2018-12-03 16:18:13.000000000 +0000 @@ -27,7 +27,7 @@ BINARY { @Override - int toBufferedImageType() + public int toBufferedImageType() { return BufferedImage.TYPE_BYTE_BINARY; } @@ -37,7 +37,7 @@ GRAY { @Override - int toBufferedImageType() + public int toBufferedImageType() { return BufferedImage.TYPE_BYTE_GRAY; } @@ -47,7 +47,7 @@ RGB { @Override - int toBufferedImageType() + public int toBufferedImageType() { return BufferedImage.TYPE_INT_RGB; } @@ -57,11 +57,11 @@ ARGB { @Override - int toBufferedImageType() + public int toBufferedImageType() { return BufferedImage.TYPE_INT_ARGB; } }; - abstract int toBufferedImageType(); + public abstract int toBufferedImageType(); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/PageDrawer.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/PageDrawer.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/PageDrawer.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/PageDrawer.java 2018-12-03 16:18:13.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; @@ -43,8 +44,13 @@ import java.awt.image.Raster; import java.awt.image.WritableRaster; 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.sejda.sambox.contentstream.PDFGraphicsStreamEngine; import org.sejda.sambox.cos.COSArray; @@ -52,6 +58,7 @@ import org.sejda.sambox.cos.COSDictionary; import org.sejda.sambox.cos.COSName; import org.sejda.sambox.cos.COSNumber; +import org.sejda.sambox.pdmodel.PDResources; import org.sejda.sambox.pdmodel.common.PDRectangle; import org.sejda.sambox.pdmodel.common.function.PDFunction; import org.sejda.sambox.pdmodel.font.PDCIDFontType0; @@ -62,6 +69,8 @@ import org.sejda.sambox.pdmodel.font.PDType1CFont; import org.sejda.sambox.pdmodel.font.PDType1Font; import org.sejda.sambox.pdmodel.graphics.PDLineDashPattern; +import org.sejda.sambox.pdmodel.graphics.PDXObject; +import org.sejda.sambox.pdmodel.graphics.blend.BlendMode; import org.sejda.sambox.pdmodel.graphics.color.PDColor; import org.sejda.sambox.pdmodel.graphics.color.PDColorSpace; import org.sejda.sambox.pdmodel.graphics.color.PDDeviceGray; @@ -73,9 +82,11 @@ import org.sejda.sambox.pdmodel.graphics.pattern.PDShadingPattern; import org.sejda.sambox.pdmodel.graphics.pattern.PDTilingPattern; import org.sejda.sambox.pdmodel.graphics.shading.PDShading; +import org.sejda.sambox.pdmodel.graphics.state.PDExtendedGraphicsState; import org.sejda.sambox.pdmodel.graphics.state.PDGraphicsState; import org.sejda.sambox.pdmodel.graphics.state.PDSoftMask; import org.sejda.sambox.pdmodel.graphics.state.RenderingMode; +import org.sejda.sambox.pdmodel.interactive.annotation.AnnotationFilter; import org.sejda.sambox.pdmodel.interactive.annotation.PDAnnotation; import org.sejda.sambox.pdmodel.interactive.annotation.PDAnnotationLink; import org.sejda.sambox.pdmodel.interactive.annotation.PDAnnotationMarkup; @@ -90,8 +101,9 @@ * *

* If you want to do custom graphics processing rather than Graphics2D rendering, then you should subclass - * PDFGraphicsStreamEngine instead. Subclassing PageDrawer is only suitable for cases where the goal is to render onto a - * Graphics2D surface. + * {@link PDFGraphicsStreamEngine} instead. Subclassing PageDrawer is only suitable for cases where the goal is to + * render onto a {@link Graphics2D} surface. In that case you'll also have to subclass {@link PDFRenderer} and modify + * {@link PDFRenderer#createPageDrawer(PageDrawerParameters)}. * * @author Ben Litchfield */ @@ -109,8 +121,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; @@ -122,14 +132,28 @@ // last clipping path private Area lastClip; - // buffered clipping area for text being drawn - private Area textClippingArea; + // shapes of glyphs being drawn to be used for clipping + private List textClippings; // glyph cache - private final Map fontGlyph2D = new HashMap(); + private final Map fontGlyph2D = new HashMap<>(); private final TilingPaintFactory tilingPaintFactory = new TilingPaintFactory(this); + private final Stack transparencyGroupStack = new Stack(); + + /** + * Default annotations filter, returns all annotations + */ + private AnnotationFilter annotationFilter = new AnnotationFilter() + { + @Override + public boolean accept(PDAnnotation annotation) + { + return true; + } + }; + /** * Constructor. * @@ -143,6 +167,29 @@ } /** + * Return the AnnotationFilter. + * + * @return the AnnotationFilter + */ + public AnnotationFilter getAnnotationFilter() + { + return annotationFilter; + } + + /** + * Set the AnnotationFilter. + * + *

+ * Allows to only render annotation accepted by the filter. + * + * @param annotationFilter the AnnotationFilter + */ + public void setAnnotationFilter(AnnotationFilter annotationFilter) + { + this.annotationFilter = annotationFilter; + } + + /** * Returns the parent renderer. */ public final PDFRenderer getRenderer() @@ -191,7 +238,6 @@ graphics = (Graphics2D) g; xform = graphics.getTransform(); this.pageSize = pageSize; - pageRotation = getPage().getRotation() % 360; setRenderingHints(); @@ -203,7 +249,7 @@ processPage(getPage()); - for (PDAnnotation annotation : getPage().getAnnotations()) + for (PDAnnotation annotation : getPage().getAnnotations(annotationFilter)) { showAnnotation(annotation); } @@ -333,8 +379,8 @@ */ private void beginTextClip() { - // buffer the text clip because it represents a single clipping area - textClippingArea = new Area(); + // buffer the text clippings because they represents a single clipping area + textClippings = new ArrayList<>(); } /** @@ -346,10 +392,17 @@ RenderingMode renderingMode = state.getTextState().getRenderingMode(); // apply the buffered clip as one area - if (renderingMode.isClip() && !textClippingArea.isEmpty()) + if (renderingMode.isClip() && !textClippings.isEmpty()) { - state.intersectClippingPath(textClippingArea); - textClippingArea = null; + // PDFBOX-4150: this is much faster than using textClippingArea.add(new Area(glyph)) + // https://stackoverflow.com/questions/21519007/fast-union-of-shapes-in-java + GeneralPath path = new GeneralPath(); + for (Shape shape : textClippings) + { + path.append(shape, false); + } + state.intersectClippingPath(path); + textClippings = new ArrayList<>(); // PDFBOX-3681: lastClip needs to be reset, because after intersection it is still the same // object, thus setClip() would believe that it is cached. @@ -387,8 +440,11 @@ GeneralPath path = glyph2D.getPathForCharacterCode(code); if (path != null) { - // stretch non-embedded glyph if it does not match the width contained in the PDF - if (!font.isEmbedded()) + // Stretch non-embedded glyph if it does not match the height/width contained in the PDF. + // Vertical fonts have zero X displacement, so the following code scales to 0 if we don't skip it. + // TODO: How should vertical fonts be handled? + if (!font.isEmbedded() && !font.isVertical() && !font.isStandard14() + && font.hasExplicitWidth(code)) { float fontWidth = font.getWidthFromFont(code); if (fontWidth > 0 && // ignore spaces @@ -421,7 +477,7 @@ if (renderingMode.isClip()) { - textClippingArea.add(new Area(glyph)); + textClippings.add(glyph); } } } @@ -548,7 +604,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, @@ -564,57 +620,36 @@ 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) + { + 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 @@ -1153,7 +1188,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; @@ -1362,22 +1402,23 @@ 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(); float y = pageSize.getUpperRightY() - bbox.getUpperRightY(); - graphics.translate(x * xScale, y * yScale); if (flipTG) { graphics.translate(0, image.getHeight()); graphics.scale(1, -1); } + else + { + graphics.translate(x * xScale, y * yScale); + } PDSoftMask softMask = getGraphicsState().getSoftMask(); if (softMask != null) @@ -1407,6 +1448,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; @@ -1443,6 +1486,8 @@ bbox = null; minX = 0; minY = 0; + maxX = 0; + maxY = 0; width = 0; height = 0; return; @@ -1459,8 +1504,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; @@ -1474,7 +1519,39 @@ { 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 @@ -1502,8 +1579,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; @@ -1522,7 +1597,12 @@ } else { + transparencyGroupStack.push(this); processTransparencyGroup(form); + if (!transparencyGroupStack.isEmpty()) + { + transparencyGroupStack.pop(); + } } } finally @@ -1535,7 +1615,10 @@ linePath = linePathOriginal; pageSize = pageSizeOriginal; xform = xformOriginal; - pageRotation = pageRotationOriginal; + if (needsBackdrop) + { + ((GroupGraphics) g).removeBackdrop(backdropImage, backdropX, backdropY); + } } } @@ -1611,4 +1694,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 libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/PDFRenderer.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/PDFRenderer.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/PDFRenderer.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/PDFRenderer.java 2018-12-03 16:18:13.000000000 +0000 @@ -21,9 +21,15 @@ import java.awt.image.BufferedImage; import java.io.IOException; +import org.sejda.sambox.cos.COSName; import org.sejda.sambox.pdmodel.PDDocument; import org.sejda.sambox.pdmodel.PDPage; +import org.sejda.sambox.pdmodel.PDResources; import org.sejda.sambox.pdmodel.common.PDRectangle; +import org.sejda.sambox.pdmodel.graphics.blend.BlendMode; +import org.sejda.sambox.pdmodel.graphics.state.PDExtendedGraphicsState; +import org.sejda.sambox.pdmodel.interactive.annotation.AnnotationFilter; +import org.sejda.sambox.pdmodel.interactive.annotation.PDAnnotation; /** * Renders a PDF document to an AWT BufferedImage. This class may be overridden in order to perform custom rendering. @@ -36,6 +42,20 @@ // 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 BufferedImage pageImage; + + /** * Creates a new PDFRenderer. * * @param document the document to render @@ -46,6 +66,29 @@ } /** + * 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; + } + + /** * Returns the given page as an RGB image at 72 DPI * * @param pageIndex the zero-based index of the page to be converted. @@ -84,10 +127,12 @@ } /** - * @param page the zero-based index of the page to be converted + * 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 page rendered as a {@link BufferedImage} + * @return the rendered page image * @throws IOException if the PDF cannot be read */ public BufferedImage renderImageWithDPI(int pageIndex, float dpi, ImageType imageType) @@ -97,64 +142,54 @@ } /** - * @param page the zero-based index of the page to be converted - * @param dpi the DPI (dots per inch) to render at - * @param bufferedImageType the type of image to return - * @return the page rendered as a {@link BufferedImage} - * @throws IOException if the PDF cannot be read - */ - public BufferedImage renderImageWithDPI(int page, float dpi, int bufferedImageType) - throws IOException - { - return renderImage(page, dpi / 72f, bufferedImageType); - } - - /** + * 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 bufferedImageType the type of image to return - * @return the page rendered as a {@link BufferedImage} + * @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 { - return renderImage(pageIndex, scale, imageType.toBufferedImageType()); - } - - /** - * @param pageIndex the zero-based index of the page to be converted - * @param scale the scaling factor, where 1 = 72 DPI - * @param bufferedImageType the type of image to return - * @return the page rendered as a {@link BufferedImage} - * @throws IOException if the PDF cannot be read - */ - public BufferedImage renderImage(int pageIndex, float scale, int bufferedImageType) - 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); + + // 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, bufferedImageType); + image = new BufferedImage(heightPx, widthPx, bimType); } else { - image = new BufferedImage(widthPx, heightPx, bufferedImageType); + image = new BufferedImage(widthPx, heightPx, bimType); } + pageImage = image; - // use a transparent background if the imageType supports alpha + // use a transparent background if the image type supports alpha Graphics2D g = image.createGraphics(); - if (bufferedImageType == BufferedImage.TYPE_INT_ARGB) + if (image.getType() == BufferedImage.TYPE_INT_ARGB) { g.setBackground(new Color(0, 0, 0, 0)); } @@ -164,7 +199,7 @@ } g.clearRect(0, 0, image.getWidth(), image.getHeight()); - transform(g, page, scale); + transform(g, page, scale, scale); // the end-user may provide a custom PageDrawer PageDrawerParameters parameters = new PageDrawerParameters(this, page); @@ -173,6 +208,19 @@ 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; } @@ -199,10 +247,26 @@ 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, scale); + transform(graphics, page, scaleX, scaleY); PDRectangle cropBox = page.getCropBox(); graphics.clearRect(0, 0, (int) cropBox.getWidth(), (int) cropBox.getHeight()); @@ -213,10 +277,10 @@ drawer.drawPage(graphics, cropBox); } - /// scale rotate translate - private void transform(Graphics2D graphics, PDPage page, float scale) + // scale rotate translate + private void transform(Graphics2D graphics, PDPage page, float scaleX, float scaleY) { - graphics.scale(scale, scale); + graphics.scale(scaleX, scaleY); // TODO should we be passing the scale to PageDrawer rather than messing with Graphics? int rotationAngle = page.getRotation(); @@ -251,6 +315,43 @@ */ protected PageDrawer createPageDrawer(PageDrawerParameters parameters) throws IOException { - return new PageDrawer(parameters); + 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; } } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/TilingPaintFactory.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/TilingPaintFactory.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/TilingPaintFactory.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/TilingPaintFactory.java 2018-12-03 16:18:13.000000000 +0000 @@ -18,6 +18,7 @@ import java.awt.geom.AffineTransform; import java.io.IOException; +import java.lang.ref.WeakReference; import java.util.Map; import java.util.WeakHashMap; @@ -35,7 +36,7 @@ class TilingPaintFactory { private final PageDrawer drawer; - private final Map weakCache = new WeakHashMap<>(); + private final Map> weakCache = new WeakHashMap<>(); TilingPaintFactory(PageDrawer drawer) { @@ -45,14 +46,19 @@ TilingPaint create(PDTilingPattern pattern, PDColorSpace colorSpace, PDColor color, AffineTransform xform) throws IOException { - TilingPaint paint; + TilingPaint paint = null; TilingPaintParameter tilingPaintParameter = new TilingPaintParameter( drawer.getInitialMatrix(), pattern.getCOSObject(), colorSpace, color, xform); - paint = weakCache.get(tilingPaintParameter); + WeakReference weakRef = weakCache.get(tilingPaintParameter); + if (weakRef != null) + { + // PDFBOX-4058: additional WeakReference makes gc work better + paint = weakRef.get(); + } if (paint == null) { paint = new TilingPaint(drawer, pattern, colorSpace, color, xform); - weakCache.put(tilingPaintParameter, paint); + weakCache.put(tilingPaintParameter, new WeakReference<>(paint)); } return paint; } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/TTFGlyph2D.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/TTFGlyph2D.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/TTFGlyph2D.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/TTFGlyph2D.java 2018-12-03 16:18:13.000000000 +0000 @@ -114,6 +114,13 @@ */ public GeneralPath getPathForGID(int gid, int code) throws IOException { + if (gid == 0 && !isCIDFont && code == 10 && font.isStandard14()) + { + // PDFBOX-4001 return empty path for line feed on std14 + // need to catch this early because all "bad" glyphs have gid 0 + LOG.warn("No glyph for code " + code + " in font " + font.getName()); + return new GeneralPath(); + } GeneralPath glyphPath = glyphs.get(gid); if (glyphPath == null) { @@ -123,8 +130,8 @@ { int cid = ((PDType0Font) font).codeToCID(code); String cidHex = String.format("%04x", cid); - LOG.warn("No glyph for " + code + " (CID " + cidHex + ") in font " - + font.getName()); + LOG.warn("No glyph for code " + code + " (CID " + cidHex + ") in font " + + font.getName()); } else { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/Type1Glyph2D.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/Type1Glyph2D.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/rendering/Type1Glyph2D.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/rendering/Type1Glyph2D.java 2018-12-03 16:18:13.000000000 +0000 @@ -58,7 +58,14 @@ String name = font.getEncoding().getName(code); if (!font.hasGlyph(name)) { - LOG.warn("No glyph for " + code + " (" + name + ") in font " + font.getName()); + LOG.warn("No glyph for code " + code + " (" + name + ") in font " + font.getName()); + if (code == 10 && font.isStandard14()) + { + // PDFBOX-4001 return empty path for line feed on std14 + path = new GeneralPath(); + cache.put(code, path); + return path; + } } // todo: can this happen? should it be encapsulated? diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/text/PDFTextStreamEngine.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/text/PDFTextStreamEngine.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/text/PDFTextStreamEngine.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/text/PDFTextStreamEngine.java 2018-12-03 16:18:13.000000000 +0000 @@ -64,7 +64,7 @@ /** * PDFStreamEngine subclass for advanced processing of text via TextPosition. * - * @see org.apache.pdfbox.text.TextPosition + * @see org.sejda.sambox.text.TextPosition * @author Ben Litchfield * @author John Hewson */ @@ -305,10 +305,9 @@ nextX -= cropBox.getLowerLeftX(); nextY -= cropBox.getLowerLeftY(); } - processTextPosition(new TextPosition(pageRotation, cropBox.getWidth(), - cropBox.getHeight(), translatedTextRenderingMatrix, nextX, nextY, - Math.abs(dyDisplay), dxDisplay, Math.abs(spaceWidthDisplay), unicode, - new int[] { code }, font, fontSize, + processTextPosition(new TextPosition(pageRotation, cropBox.getWidth(), cropBox.getHeight(), + translatedTextRenderingMatrix, nextX, nextY, Math.abs(dyDisplay), dxDisplay, + Math.abs(spaceWidthDisplay), unicode, new int[] { code }, font, fontSize, (int) (fontSize * textMatrix.getScalingFactorX()))); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/text/PDFTextStripper.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/text/PDFTextStripper.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/text/PDFTextStripper.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/text/PDFTextStripper.java 2018-12-03 16:18:13.000000000 +0000 @@ -640,14 +640,8 @@ 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) @@ -738,7 +732,7 @@ /** * Write the line separator value to the output stream. * - * @throws IOException If there is a problem writing out the lineseparator to the document. + * @throws IOException If there is a problem writing out the line separator to the document. */ protected void writeLineSeparator() throws IOException { @@ -748,7 +742,7 @@ /** * Write the word separator value to the output stream. * - * @throws IOException If there is a problem writing out the wordseparator to the document. + * @throws IOException If there is a problem writing out the word separator to the document. */ protected void writeWordSeparator() throws IOException { @@ -1837,8 +1831,7 @@ { if (item.isWordSeparator()) { - normalized.add( - createWord(lineBuilder.toString(), new ArrayList<>(wordPositions))); + normalized.add(createWord(lineBuilder.toString(), new ArrayList<>(wordPositions))); lineBuilder = new StringBuilder(); wordPositions.clear(); } diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/text/TextPositionComparator.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/text/TextPositionComparator.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/text/TextPositionComparator.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/text/TextPositionComparator.java 2018-12-03 16:18:13.000000000 +0000 @@ -32,13 +32,10 @@ public int compare(TextPosition pos1, TextPosition pos2) { // only compare text that is in the same direction - if (pos1.getDir() < pos2.getDir()) + int cmp1 = Float.compare(pos1.getDir(), pos2.getDir()); + if (cmp1 != 0) { - return -1; - } - else if (pos1.getDir() > pos2.getDir()) - { - return 1; + return cmp1; } // get the text direction adjusted coordinates @@ -59,22 +56,11 @@ pos2YBottom >= pos1YTop && pos2YBottom <= pos1YBottom || pos1YBottom >= pos2YTop && pos1YBottom <= pos2YBottom) { - if (x1 < x2) - { - return -1; - } - else if (x1 > x2) - { - return 1; - } - else - { - return 0; - } + return Float.compare(x1, x2); } else if (pos1YBottom < pos2YBottom) { - return - 1; + return -1; } else { diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/text/TextPosition.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/text/TextPosition.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/text/TextPosition.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/text/TextPosition.java 2018-12-03 16:18:13.000000000 +0000 @@ -692,7 +692,13 @@ */ public boolean isVisible() { - return new Rectangle2D.Float(0, 0, pageWidth, pageHeight).contains(getX(), getY()); + Rectangle2D.Float rectangle = new Rectangle2D.Float(0, 0, pageWidth, pageHeight); + if(this.rotation == 90 || this.rotation == 270) { + // flip width and height + rectangle = new Rectangle2D.Float(0, 0, pageHeight, pageWidth); + } + + return rectangle.contains(getX(), getY()); } /** diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/util/filetypedetector/FileTypeDetector.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/util/filetypedetector/FileTypeDetector.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/util/filetypedetector/FileTypeDetector.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/util/filetypedetector/FileTypeDetector.java 2018-12-03 16:18:13.000000000 +0000 @@ -63,8 +63,8 @@ ROOT.addPath(FileType.PCX, new byte[] { 0x0A, 0x05, 0x01 }); ROOT.addPath(FileType.RIFF, "RIFF".getBytes(StandardCharsets.ISO_8859_1)); - ROOT.addPath(FileType.ARW, "II".getBytes(StandardCharsets.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(StandardCharsets.ISO_8859_1), new byte[] { 0x1a, 0x00, 0x00, 0x00 }, "HEAPCCDR".getBytes(StandardCharsets.ISO_8859_1)); diff -Nru libsambox-java-1.1.19/src/main/java/org/sejda/sambox/util/Matrix.java libsambox-java-1.1.46/src/main/java/org/sejda/sambox/util/Matrix.java --- libsambox-java-1.1.19/src/main/java/org/sejda/sambox/util/Matrix.java 2017-11-09 11:15:27.000000000 +0000 +++ libsambox-java-1.1.46/src/main/java/org/sejda/sambox/util/Matrix.java 2018-12-03 16:18:13.000000000 +0000 @@ -21,6 +21,7 @@ import java.util.Arrays; import org.sejda.sambox.cos.COSArray; +import org.sejda.sambox.cos.COSBase; import org.sejda.sambox.cos.COSFloat; import org.sejda.sambox.cos.COSNumber; @@ -55,12 +56,12 @@ public Matrix(COSArray array) { single = new float[DEFAULT_SINGLE.length]; - single[0] = ((COSNumber) array.getObject(0)).floatValue(); - single[1] = ((COSNumber) array.getObject(1)).floatValue(); - single[3] = ((COSNumber) array.getObject(2)).floatValue(); - single[4] = ((COSNumber) array.getObject(3)).floatValue(); - single[6] = ((COSNumber) array.getObject(4)).floatValue(); - single[7] = ((COSNumber) array.getObject(5)).floatValue(); + single[0] = ((COSNumber)array.getObject(0)).floatValue(); + single[1] = ((COSNumber)array.getObject(1)).floatValue(); + single[3] = ((COSNumber)array.getObject(2)).floatValue(); + single[4] = ((COSNumber)array.getObject(3)).floatValue(); + single[6] = ((COSNumber)array.getObject(4)).floatValue(); + single[7] = ((COSNumber)array.getObject(5)).floatValue(); single[8] = 1; } @@ -95,6 +96,36 @@ } /** + * Convenience method to be used when creating a matrix from unverified data. If the parameter + * is a COSArray with at least six numbers, a Matrix object is created from the first six + * numbers and returned. If not, then the identity Matrix is returned. + * + * @param base a COS object, preferably a COSArray with six numbers. + * + * @return a Matrix object. + */ + public static Matrix createMatrix(COSBase base) + { + if (!(base instanceof COSArray)) + { + return new Matrix(); + } + COSArray array = (COSArray) base; + if (array.size() < 6) + { + return new Matrix(); + } + for (int i = 0; i < 6; ++i) + { + if (!(array.getObject(i) instanceof COSNumber)) + { + return new Matrix(); + } + } + return new Matrix(array); + } + + /** * This method resets the numbers in this Matrix to the original values, which are * the values that a newly constructed Matrix would have. *