From b93a0fc251a58a10a8490f8cfef8c81fe388a0fd Mon Sep 17 00:00:00 2001 From: Markus Koschany <apo@debian.org> Date: Sat, 29 Aug 2020 01:03:00 +0200 Subject: [PATCH] New upstream version 2.0.21 --- README.md | 35 +- RELEASE-NOTES.txt | 82 +-- app/pom.xml | 2 +- debugger-app/pom.xml | 2 +- debugger/pom.xml | 2 +- .../debugger/stringpane/StringPane.java | 2 +- examples/pom.xml | 21 +- .../examples/pdmodel/ExtractMetadata.java | 2 +- .../examples/pdmodel/HelloWorldType1.java | 5 +- .../signature/CreateEmbeddedTimeStamp.java | 2 +- .../examples/signature/ShowSignature.java | 49 +- .../pdfbox/examples/signature/SigUtils.java | 42 ++ .../examples/signature/cert/CRLVerifier.java | 8 +- .../signature/cert/CertificateVerifier.java | 46 +- .../examples/signature/cert/OcspHelper.java | 3 +- .../validation/CertInformationCollector.java | 36 +- .../pdfbox/examples/pdfa/CreatePDFATest.java | 2 +- .../examples/pdmodel/TestCreateSignature.java | 538 +++++++++++++++--- .../examples/signature/hexsignature.txt | 1 + fontbox/pom.xml | 2 +- .../org/apache/fontbox/cmap/CMapParser.java | 20 +- .../apache/fontbox/cmap/CodespaceRange.java | 15 +- .../org/apache/fontbox/pfb/PfbParser.java | 10 + .../fontbox/ttf/BufferedRandomAccessFile.java | 41 +- .../fontbox/ttf/GlyphSubstitutionTable.java | 8 +- .../fontbox/ttf/OS2WindowsMetricsTable.java | 2 +- .../apache/fontbox/ttf/OpenTypeScript.java | 12 +- .../fontbox/cmap/TestCodespaceRange.java | 10 +- .../ttf/BufferedRandomAccessFileTest.java | 99 +++- parent/pom.xml | 18 +- pdfbox/pom.xml | 2 +- .../pdfbox/contentstream/PDFStreamEngine.java | 23 +- .../java/org/apache/pdfbox/cos/COSFloat.java | 8 +- .../org/apache/pdfbox/cos/COSInteger.java | 2 + .../java/org/apache/pdfbox/cos/COSNumber.java | 50 +- .../org/apache/pdfbox/filter/Predictor.java | 49 -- .../org/apache/pdfbox/multipdf/Overlay.java | 21 +- .../apache/pdfbox/pdfparser/BaseParser.java | 86 +-- .../apache/pdfbox/pdfparser/COSParser.java | 10 +- .../pdfparser/PDFObjectStreamParser.java | 92 +-- .../pdfbox/pdfparser/PDFStreamParser.java | 109 ++-- .../pdfbox/pdfparser/PDFXrefStreamParser.java | 143 +++-- .../pdfbox/pdmodel/common/COSArrayList.java | 2 +- .../pdfbox/pdmodel/common/PDNameTreeNode.java | 19 +- .../pdmodel/common/function/PDFunction.java | 2 +- .../common/function/PDFunctionType0.java | 6 +- .../encryption/StandardSecurityHandler.java | 22 +- .../pdfbox/pdmodel/font/FontMapperImpl.java | 8 +- .../apache/pdfbox/pdmodel/font/PDFont.java | 4 +- .../pdfbox/pdmodel/font/PDFontDescriptor.java | 2 +- .../pdfbox/pdmodel/font/PDType0Font.java | 2 +- .../pdfbox/pdmodel/font/PDType1Font.java | 12 +- .../pdfbox/pdmodel/font/Standard14Fonts.java | 238 +++++--- .../pdfbox/pdmodel/font/TrueTypeEmbedder.java | 35 +- .../font/encoding/DictionaryEncoding.java | 5 + .../font/encoding/MacRomanEncoding.java | 2 +- .../font/encoding/WinAnsiEncoding.java | 2 +- .../pdmodel/graphics/color/PDDeviceCMYK.java | 7 +- .../graphics/image/PDImageXObject.java | 5 + .../PDOptionalContentProperties.java | 6 +- .../form/AppearanceGeneratorHelper.java | 4 +- .../pdmodel/interactive/form/PDAcroForm.java | 22 +- .../interactive/form/PDSignatureField.java | 9 +- .../pdmodel/interactive/form/PlainText.java | 36 +- .../apache/pdfbox/rendering/PageDrawer.java | 94 +-- .../pdfbox/text/LegacyPDFStreamEngine.java | 134 +++-- .../apache/pdfbox/text/PDFTextStripper.java | 17 +- .../java/org/apache/pdfbox/util/Matrix.java | 282 +++++---- .../java/org/apache/pdfbox/util/Version.java | 7 +- .../org/apache/pdfbox/cos/TestCOSFloat.java | 60 ++ .../org/apache/pdfbox/cos/TestCOSNumber.java | 28 +- .../org/apache/pdfbox/filter/TestFilters.java | 6 +- .../pdfparser/PDFObjectStreamParserTest.java | 46 ++ .../pdmodel/common/COSArrayListTest.java | 4 +- .../pdmodel/common/TestPDNumberTreeNode.java | 3 +- .../pdfbox/pdmodel/font/PDFontTest.java | 52 ++ .../interactive/form/MultilineFieldsTest.java | 105 ++++ .../form/PDAcroFormFlattenTest.java | 14 + .../pdfbox/rendering/TestPDFToImage.java | 2 + .../apache/pdfbox/text/TestTextStripper.java | 274 ++++++--- .../org/apache/pdfbox/util/MatrixTest.java | 174 +++++- .../org/apache/pdfbox/util/TestDateUtil.java | 5 + .../org/apache/pdfbox/util/TestHexUtil.java | 36 +- pdfbox/src/test/resources/input/eu-001.pdf | Bin 0 -> 68143 bytes .../resources/input/eu-001.pdf-sorted.txt | 159 ++++++ .../resources/input/eu-001.pdf-tabula.txt | 209 +++++++ .../src/test/resources/input/eu-001.pdf.txt | 195 +++++++ .../{PDFBOX-1777.bin => PDFBOX-1977.bin} | Bin .../form/PDFBOX-3835-input-acrobat-wrap.pdf | Bin 0 -> 9924 bytes .../PDFBOX3812-acrobat-multiline-auto.pdf | Bin 0 -> 29737 bytes pom.xml | 8 +- preflight-app/pom.xml | 2 +- preflight/pom.xml | 2 +- .../preflight/content/StubOperator.java | 3 +- .../metadata/UniquePropertiesValidation.java | 101 ++++ .../preflight/parser/PreflightParser.java | 2 +- .../process/MetadataValidationProcess.java | 9 +- .../reflect/ExtGStateValidationProcess.java | 42 +- .../reflect/SinglePageValidationProcess.java | 6 + .../action/pdfa1b/TestGotoAction.java | 5 +- .../action/pdfa1b/TestGotoRemoteAction.java | 10 +- .../action/pdfa1b/TestSubmitAction.java | 4 +- tools/pom.xml | 2 +- xmpbox/pom.xml | 2 +- .../java/org/apache/xmpbox/DateConverter.java | 5 +- .../org/apache/xmpbox/DateConverterTest.java | 19 + 106 files changed, 3109 insertions(+), 1181 deletions(-) create mode 100644 examples/src/test/resources/org/apache/pdfbox/examples/signature/hexsignature.txt create mode 100644 pdfbox/src/test/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParserTest.java create mode 100644 pdfbox/src/test/resources/input/eu-001.pdf create mode 100644 pdfbox/src/test/resources/input/eu-001.pdf-sorted.txt create mode 100644 pdfbox/src/test/resources/input/eu-001.pdf-tabula.txt create mode 100644 pdfbox/src/test/resources/input/eu-001.pdf.txt rename pdfbox/src/test/resources/org/apache/pdfbox/filter/{PDFBOX-1777.bin => PDFBOX-1977.bin} (100%) create mode 100644 pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/interactive/form/PDFBOX-3835-input-acrobat-wrap.pdf create mode 100644 pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/interactive/form/PDFBOX3812-acrobat-multiline-auto.pdf create mode 100644 preflight/src/main/java/org/apache/pdfbox/preflight/metadata/UniquePropertiesValidation.java diff --git a/README.md b/README.md index 4b3efcf..6ef4006 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,21 @@ -Apache PDFBox <http://pdfbox.apache.org/> +<!--- + 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. +---> + +Apache PDFBox <https://pdfbox.apache.org/> =================================================== The Apache PDFBox library is an open source Java tool for working with PDF @@ -7,18 +24,18 @@ of existing documents and the ability to extract content from documents. PDFBox also includes several command line utilities. PDFBox is published under the Apache License, Version 2.0. -PDFBox is a project of the Apache Software Foundation <http://www.apache.org/>. +PDFBox is a project of the Apache Software Foundation <https://www.apache.org/>. Binary Downloads ---------------- You can download binary versions for releases currently under development or older -releases from our [Download Page](http://pdfbox.apache.org/download.cgi). +releases from our [Download Page](https://pdfbox.apache.org/download.cgi). Build ----- -You need Java 6 (or higher) and Maven 2 <http://maven.apache.org/> to +You need Java 6 (or higher) and Maven 2 <https://maven.apache.org/> to build PDFBox. The recommended build command is: mvn clean install @@ -33,7 +50,7 @@ Contribute There are various ways to help us improve PDFBox. - look at the [Issue Tracker](https://issues.apache.org/jira/browse/PDFBOX) to help us fix bugs. -- answer questions on our [Users Mailing List](http://pdfbox.apache.org/mailinglists.html "Subscribe to Mailing List"). +- answer questions on our [Users Mailing List](https://pdfbox.apache.org/mailinglists.html "Subscribe to Mailing List"). - help us enhance the [Examples](https://svn.apache.org/repos/asf/pdfbox/trunk/examples/) - help us to enhance the [PDFBox Documentation](https://git-wip-us.apache.org/repos/asf/pdfbox-docs) or on [GitHub](https://github.com/apache/pdfbox-docs). @@ -41,7 +58,7 @@ or on [GitHub](https://github.com/apache/pdfbox-docs). Support ------- -**Please follow the guidelines at our [Support Page](http://pdfbox.apache.org/support.html).** +**Please follow the guidelines at our [Support Page](https://pdfbox.apache.org/support.html).** If you have questions about how to use PDFBox do ask on the [Users Mailing List](/mailinglists.html "Subscribe to Mailing List"). @@ -50,7 +67,7 @@ This will get you help from the entire community. The PDFBox examples and the test code in the sources will also provide additional information. And there are additional resources available on sites such as -[Stack Overflow](http://stackoverflow.com/search?q=pdfbox "Stack Overflow"). +[Stack Overflow](https://stackoverflow.com/search?q=pdfbox "Stack Overflow"). If you are sure you have found a bug the please report the issue in our [Issue Tracker](https://issues.apache.org/jira/browse/PDFBOX). @@ -91,7 +108,7 @@ 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 + https://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, @@ -108,7 +125,7 @@ and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. See -<http://www.wassenaar.org/> for more information. +<https://www.wassenaar.org/> for more information. The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 62c4b8b..89dacfa 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,11 +1,11 @@ -Release Notes -- Apache PDFBox -- Version 2.0.20 +Release Notes -- Apache PDFBox -- Version 2.0.21 Introduction ------------ The Apache PDFBox library is an open source Java tool for working with PDF documents. -This is an incremental bugfix release based on the earlier 2.0.19 release. It contains +This is an incremental bugfix release based on the earlier 2.0.20 release. It contains a couple of fixes and small improvements. For more details on these changes and all the other fixes and improvements @@ -14,44 +14,56 @@ PDFBox issue tracker at https://issues.apache.org/jira/browse/PDFBOX. Bug -[PDFBOX-756] - Some characters from TeX-created files are mapped into ASCII range 1-31 -[PDFBOX-4516] - PDFBox text and images are blurry even after rendering with high DPI -[PDFBOX-4783] - empty annotation appearance BBox brings "Multiplying two matrices produces illegal values" -[PDFBOX-4788] - Flattening fields results in non-widget annotations being removed -[PDFBOX-4793] - Questionable fallback font for some embedded chinese fonts -[PDFBOX-4794] - NPE in ExtractImages.ImageGraphicsEngine().run() (2) -[PDFBOX-4799] - isartor-6-2-2-t02-fail-a.pdf fails on jdk15 with ArrayIndexOutOfBoundsException -[PDFBOX-4800] - Parsing of numbers does not always terminate at actual end of number -[PDFBOX-4801] - ArrayIndexOutOfBoundsException in PDICCBased.toRGB() -[PDFBOX-4805] - Regression in 2.0.19 -[PDFBOX-4807] - COSString cannot be cast to COSDictionary -[PDFBOX-4811] - Glyphs getting lost when rendering -[PDFBOX-4814] - Wrong COSType for OCProperties after merge -[PDFBOX-4817] - Generated XMP Metadata with other XSLT processor are XML invalid -[PDFBOX-4819] - Optional Content Membership Dictionaries (OCMD) incorrect -[PDFBOX-4821] - My PDF document is not printed correctly. Rendering it works. -[PDFBOX-4822] - Off-by-one error in PDSignature.getConvertedContents() -[PDFBOX-4824] - NullpointerException with PDFDebugger -[PDFBOX-4825] - PDPushButton.getOnValues() throws IllegalStateException -[PDFBOX-4828] - Encode a text using the vertical type of the font in the attachment, which succeeded in version 2.0.12 but failed in version 2.0.19 -[PDFBOX-4833] - PDColorSpace#create IOException, expected a name or array but got COSDictionary -[PDFBOX-4849] - FlateFilter Inflater leaks -[PDFBOX-4851] - Image rendering issue 2 +[PDFBOX-3835] - Wrap long words for multiline text fields +[PDFBOX-4568] - Field text poorly vertically aligned +[PDFBOX-4729] - Wrong position of text in PDTextField with multiline +[PDFBOX-4850] - Image rendering issue +[PDFBOX-4860] - Preflight doesn't catch repetition of elements in XMP +[PDFBOX-4863] - Bitmapped fonts are rendered very blurry +[PDFBOX-4866] - java.lang.IndexOutOfBoundsException +[PDFBOX-4871] - java.lang.ArrayIndexOutOfBoundsException: 3 +[PDFBOX-4872] - java.lang.ClassCastException: org.apache.fontbox.cmap.CMapParser$Operator cannot be cast to java.lang.Number +[PDFBOX-4878] - Call to DictionaryEncoding.getEncoding () throws NullPointerException for some PDF's +[PDFBOX-4879] - Binary compatibility is broken in 2.0.20 +[PDFBOX-4880] - NullPointerException in TrueTypeEmbedder.createFontDescriptor() with OCR-B font +[PDFBOX-4882] - Two conditions are always false in TrueTypeEmbedder.isEmbeddingPermitted +[PDFBOX-4887] - Using the same font fully embedded in plain text and PDTextField, it throws an exception in PDTextField.setValue +[PDFBOX-4889] - Cannot flatten this file. +[PDFBOX-4890] - Stack overflow in BufferedRandomAccessFile.read() while creating font cache +[PDFBOX-4891] - nbspace missing in WinAnsiEncoding and MacRomanEncoding +[PDFBOX-4894] - Invalid file offsets for PDF files larger than 2G +[PDFBOX-4897] - PDFObjectStreamParser doesnt use offset +[PDFBOX-4900] - PDFBox Rendering of PDF Page incorrect when using the special None Named Separation +[PDFBOX-4902] - PDF/A validation fails when system time zone has minutes +[PDFBOX-4904] - Bold text leads to wrong order - Text extraction +[PDFBOX-4906] - PDOptionalContentProperties hasGroup can null pointer if OCGs data is missing +[PDFBOX-4907] - Signature not detected by Acrobat Reader +[PDFBOX-4913] - ArrayIndexOutOfBoundsException in ShadingContext.convertToRGB() +[PDFBOX-4915] - "Page tree root must be a dictionary" on PDDocument.load +[PDFBOX-4920] - OCSP validation takes very long in ci build +[PDFBOX-4923] - IllegalArgumentException: The start and the end values must not have different lengths +[PDFBOX-4927] - IllegalStateException: Expected 'Page' but found COSName{Annot} in PDPageTree.sanitizeType +[PDFBOX-4930] - Font thickness issue when we use PDFBox for generating images from PDF Improvement -[PDFBOX-4784] - Possibility to provide the SecureRandom to SecurityHandler -[PDFBOX-4804] - Remove no longer needed parameter from PDFStreamEngine#show*Glyph -[PDFBOX-4810] - Improve CodespaceRange to be in line with the spec -[PDFBOX-4844] - Pass resourceCache to patterns +[PDFBOX-3812] - Support auto size font for multiline PDTextField +[PDFBOX-4594] - Multiline field text with auto font sizing should be size adjusted +[PDFBOX-4869] - Reading standard 14 fonts is slow +[PDFBOX-4875] - Lazy load standard 14 fonts, only if needed +[PDFBOX-4877] - Matrix class performance improvements +[PDFBOX-4895] - Faster COSNumber +[PDFBOX-4896] - Don't save and restore graphic states around showGlyph in LegacyPDFStreamEngine +[PDFBOX-4909] - Don't calculate font height for every glyph -Test +Task -[PDFBOX-4854] - Add test that font can be deleted after usage +[PDFBOX-4071] - Improve code quality (3) +[PDFBOX-4933] - Correct PDFBOX-1777 to PDFBOX-1977 in tests -Task +Sub-task -[PDFBOX-4813] - Remove catching NullPointerException +[PDFBOX-3910] - Support auto font sizing in multiline text fields Release Contents ---------------- @@ -74,7 +86,7 @@ documents and the ability to extract content from documents. Apache PDFBox also includes several command line utilities. Apache PDFBox is published under the Apache License, Version 2.0. -For more information, visit http://pdfbox.apache.org/ +For more information, visit https://pdfbox.apache.org/ About The Apache Software Foundation ------------------------------------ @@ -86,4 +98,4 @@ enables individual and commercial users to easily deploy Apache software; the Foundation's intellectual property framework limits the legal exposure of its 2,500+ contributors. -For more information, visit http://www.apache.org/ +For more information, visit https://www.apache.org/ diff --git a/app/pom.xml b/app/pom.xml index 31a7a33..ceef275 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>../parent/pom.xml</relativePath> </parent> diff --git a/debugger-app/pom.xml b/debugger-app/pom.xml index 9b6d9e0..80ff0eb 100644 --- a/debugger-app/pom.xml +++ b/debugger-app/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>../parent/pom.xml</relativePath> </parent> diff --git a/debugger/pom.xml b/debugger/pom.xml index 536147d..89e3c13 100644 --- a/debugger/pom.xml +++ b/debugger/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>../parent/pom.xml</relativePath> </parent> diff --git a/debugger/src/main/java/org/apache/pdfbox/debugger/stringpane/StringPane.java b/debugger/src/main/java/org/apache/pdfbox/debugger/stringpane/StringPane.java index 151e5e5..2a7fcd5 100644 --- a/debugger/src/main/java/org/apache/pdfbox/debugger/stringpane/StringPane.java +++ b/debugger/src/main/java/org/apache/pdfbox/debugger/stringpane/StringPane.java @@ -62,7 +62,7 @@ public class StringPane String text = cosString.getString(); for (char c : text.toCharArray()) { - if (Character.isISOControl(c)) + if (Character.isISOControl(c) && c != '\n' && c != '\r' && c != '\t') { text = "<" + cosString.toHexString() + ">"; break; diff --git a/examples/pom.xml b/examples/pom.xml index 68323b6..610f048 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>../parent/pom.xml</relativePath> </parent> @@ -137,6 +137,25 @@ </excludes> </configuration> </plugin> + <plugin> + <groupId>com.googlecode.maven-download-plugin</groupId> + <artifactId>download-maven-plugin</artifactId> + <executions> + <execution> + <id>testAddValidationInformation</id> + <phase>generate-test-resources</phase> + <goals> + <goal>wget</goal> + </goals> + <configuration> + <url>https://www.quovadisglobal.com/wp-content/uploads/2020/01/QV_RCA1_RCA3_CPCPS_V4_11.pdf</url> + <outputDirectory>${project.build.directory}/pdfs</outputDirectory> + <outputFileName>QV_RCA1_RCA3_CPCPS_V4_11.pdf</outputFileName> + <sha512>940ab0cc5ad45c7b46fba4a079fd69803540bfd68f059326f62fe8d322fb0cc176cf303b9d1d679cb663edf279efe20c2452cc5eba69f5b8afadfca4c77cdb86</sha512> + </configuration> + </execution> + </executions> + </plugin> <!-- JDK9 --> <plugin> <artifactId>maven-surefire-plugin</artifactId> diff --git a/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractMetadata.java b/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractMetadata.java index 470c9ef..47708d4 100644 --- a/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractMetadata.java +++ b/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractMetadata.java @@ -73,7 +73,7 @@ public final class ExtractMetadata DomXmpParser xmpParser = new DomXmpParser(); try { - XMPMetadata metadata = xmpParser.parse(meta.createInputStream()); + XMPMetadata metadata = xmpParser.parse(meta.toByteArray()); showDublinCoreSchema(metadata); showAdobePDFSchema(metadata); diff --git a/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/HelloWorldType1.java b/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/HelloWorldType1.java index c2ba427..bf98379 100644 --- a/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/HelloWorldType1.java +++ b/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/HelloWorldType1.java @@ -19,6 +19,7 @@ package org.apache.pdfbox.examples.pdmodel; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; @@ -54,7 +55,9 @@ public final class HelloWorldType1 PDPage page = new PDPage(); doc.addPage(page); - PDFont font = new PDType1Font(doc, new FileInputStream(pfbPath)); + InputStream is = new FileInputStream(pfbPath); + PDFont font = new PDType1Font(doc, is); + is.close(); PDPageContentStream contents = new PDPageContentStream(doc, page); contents.beginText(); diff --git a/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateEmbeddedTimeStamp.java b/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateEmbeddedTimeStamp.java index b057bae..5287fcc 100644 --- a/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateEmbeddedTimeStamp.java +++ b/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateEmbeddedTimeStamp.java @@ -145,7 +145,7 @@ public class CreateEmbeddedTimeStamp private void processRelevantSignatures(byte[] documentBytes) throws IOException, CMSException, NoSuchAlgorithmException { - SigUtils.getLastRelevantSignature(document); + signature = SigUtils.getLastRelevantSignature(document); if (signature == null) { return; diff --git a/examples/src/main/java/org/apache/pdfbox/examples/signature/ShowSignature.java b/examples/src/main/java/org/apache/pdfbox/examples/signature/ShowSignature.java index 9c1aa07..934e103 100644 --- a/examples/src/main/java/org/apache/pdfbox/examples/signature/ShowSignature.java +++ b/examples/src/main/java/org/apache/pdfbox/examples/signature/ShowSignature.java @@ -34,9 +34,7 @@ import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collection; -import java.util.Date; import java.util.HashSet; -import java.util.Set; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; @@ -233,7 +231,7 @@ public final class ShowSignature { @SuppressWarnings("unchecked") Store<X509CertificateHolder> store = new JcaCertStore(certs); - verifyCertificateChain(store, cert, sig.getSignDate().getTime()); + SigUtils.verifyCertificateChain(store, cert, sig.getSignDate().getTime()); } } } @@ -378,7 +376,7 @@ public final class ShowSignature X509Certificate certFromTimeStamp = (X509Certificate) certs.iterator().next(); SigUtils.checkTimeStampCertificateUsage(certFromTimeStamp); SigUtils.validateTimestampToken(timeStampToken); - verifyCertificateChain(timeStampToken.getCertificates(), + SigUtils.verifyCertificateChain(timeStampToken.getCertificates(), certFromTimeStamp, timeStampToken.getTimeStampInfo().getGenTime()); } @@ -448,10 +446,23 @@ public final class ShowSignature HashSet<X509CertificateHolder> certificateHolderSet = new HashSet<X509CertificateHolder>(); certificateHolderSet.addAll(certificatesStore.getMatches(null)); certificateHolderSet.addAll(timeStampToken.getCertificates().getMatches(null)); - verifyCertificateChain(new CollectionStore<X509CertificateHolder>(certificateHolderSet), + SigUtils.verifyCertificateChain(new CollectionStore<X509CertificateHolder>(certificateHolderSet), certFromTimeStamp, timeStampToken.getTimeStampInfo().getGenTime()); SigUtils.checkTimeStampCertificateUsage(certFromTimeStamp); + + // compare the hash of the signature with the hash in the timestamp + byte[] tsMessageImprintDigest = timeStampToken.getTimeStampInfo().getMessageImprintDigest(); + String hashAlgorithm = timeStampToken.getTimeStampInfo().getMessageImprintAlgOID().getId(); + byte[] sigMessageImprintDigest = MessageDigest.getInstance(hashAlgorithm).digest(signerInformation.getSignature()); + if (Arrays.equals(tsMessageImprintDigest, sigMessageImprintDigest)) + { + System.out.println("timestamp signature verified"); + } + else + { + System.err.println("timestamp signature verification failed"); + } } try @@ -519,7 +530,7 @@ public final class ShowSignature if (sig.getSignDate() != null) { - verifyCertificateChain(certificatesStore, certFromSignedData, sig.getSignDate().getTime()); + SigUtils.verifyCertificateChain(certificatesStore, certFromSignedData, sig.getSignDate().getTime()); } else { @@ -528,32 +539,6 @@ public final class ShowSignature } } - private void verifyCertificateChain(Store<X509CertificateHolder> certificatesStore, - X509Certificate certFromSignedData, Date signDate) - throws CertificateVerificationException, CertificateException - { - // Verify certificate chain (new since 11/2018) - // Please post bad PDF files that succeed and - // good PDF files that fail in - // https://issues.apache.org/jira/browse/PDFBOX-3017 - Collection<X509CertificateHolder> certificateHolders = certificatesStore.getMatches(null); - Set<X509Certificate> additionalCerts = new HashSet<X509Certificate>(); - JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); - for (X509CertificateHolder certHolder : certificateHolders) - { - X509Certificate certificate = certificateConverter.getCertificate(certHolder); - if (!certificate.equals(certFromSignedData)) - { - additionalCerts.add(certificate); - } - } - CertificateVerifier.verifyCertificate(certFromSignedData, additionalCerts, true, signDate); - //TODO check whether the root certificate is in our trusted list. - // For the EU, get a list here: - // https://ec.europa.eu/digital-single-market/en/eu-trusted-lists-trust-service-providers - // ( getRootCertificates() is not helpful because these are SSL certificates) - } - /** * Analyzes the DSS-Dictionary (Document Security Store) of the document. Which is used for * signature validation. The DSS is defined in PAdES Part 4 - Long Term Validation. diff --git a/examples/src/main/java/org/apache/pdfbox/examples/signature/SigUtils.java b/examples/src/main/java/org/apache/pdfbox/examples/signature/SigUtils.java index e65b219..feae45a 100644 --- a/examples/src/main/java/org/apache/pdfbox/examples/signature/SigUtils.java +++ b/examples/src/main/java/org/apache/pdfbox/examples/signature/SigUtils.java @@ -21,7 +21,10 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.Collection; +import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import org.apache.commons.logging.Log; @@ -30,6 +33,8 @@ import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.examples.signature.cert.CertificateVerificationException; +import org.apache.pdfbox.examples.signature.cert.CertificateVerifier; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.encryption.SecurityProvider; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; @@ -39,6 +44,7 @@ import org.bouncycastle.asn1.cms.AttributeTable; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.SignerInformation; @@ -48,6 +54,7 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.tsp.TSPException; import org.bouncycastle.tsp.TimeStampToken; import org.bouncycastle.util.Selector; +import org.bouncycastle.util.Store; /** * Utility class for the signature / timestamp examples. @@ -286,4 +293,39 @@ public class SigUtils new JcaSimpleSignerInfoVerifierBuilder().setProvider(SecurityProvider.getProvider()).build(certificateHolder); timeStampToken.validate(siv); } + + + /** + * Verify the certificate chain up to the root, including OCSP or CRL. However this does not + * test whether the root certificate is in a trusted list.<br><br> + * Please post bad PDF files that succeed and good PDF files that fail in + * <a href="https://issues.apache.org/jira/browse/PDFBOX-3017">PDFBOX-3017</a>. + * + * @param certificatesStore + * @param certFromSignedData + * @param signDate + * @throws CertificateVerificationException + * @throws CertificateException + */ + public static void verifyCertificateChain(Store<X509CertificateHolder> certificatesStore, + X509Certificate certFromSignedData, Date signDate) + throws CertificateVerificationException, CertificateException + { + Collection<X509CertificateHolder> certificateHolders = certificatesStore.getMatches(null); + Set<X509Certificate> additionalCerts = new HashSet<X509Certificate>(); + JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); + for (X509CertificateHolder certHolder : certificateHolders) + { + X509Certificate certificate = certificateConverter.getCertificate(certHolder); + if (!certificate.equals(certFromSignedData)) + { + additionalCerts.add(certificate); + } + } + CertificateVerifier.verifyCertificate(certFromSignedData, additionalCerts, true, signDate); + //TODO check whether the root certificate is in our trusted list. + // For the EU, get a list here: + // https://ec.europa.eu/digital-single-market/en/eu-trusted-lists-trust-service-providers + // ( getRootCertificates() is not helpful because these are SSL certificates) + } } diff --git a/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CRLVerifier.java b/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CRLVerifier.java index 29625d1..a7a092a 100644 --- a/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CRLVerifier.java +++ b/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CRLVerifier.java @@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.security.GeneralSecurityException; import java.security.cert.CRLException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -120,11 +121,16 @@ public final class CRLVerifier X509Certificate crlIssuerCert = null; for (X509Certificate possibleCert : mergedCertSet) { - if (crl.getIssuerX500Principal().equals(possibleCert.getSubjectX500Principal())) + try { + cert.verify(possibleCert.getPublicKey(), SecurityProvider.getProvider().getName()); crlIssuerCert = possibleCert; break; } + catch (GeneralSecurityException ex) + { + // not the issuer + } } if (crlIssuerCert == null) { diff --git a/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerifier.java b/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerifier.java index 6620da1..68f69f5 100644 --- a/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerifier.java +++ b/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/CertificateVerifier.java @@ -113,12 +113,38 @@ public final class CertificateVerifier throw new CertificateVerificationException("The certificate is self-signed."); } - Set<X509Certificate> certSet = CertificateVerifier.downloadExtraCertificates(cert); - int downloadSize = certSet.size(); + Set<X509Certificate> certSet = new HashSet<X509Certificate>(); certSet.addAll(additionalCerts); + + // Download extra certificates. However, each downloaded certificate can lead to + // more extra certificates, e.g. with the file from PDFBOX-4091, which has + // an incomplete chain. + Set<X509Certificate> certsToTrySet = new HashSet<X509Certificate>(); + certsToTrySet.add(cert); + int downloadSize = 0; + while (!certsToTrySet.isEmpty()) + { + Set<X509Certificate> nextCertsToTrySet = new HashSet<X509Certificate>(); + for (X509Certificate tryCert : certsToTrySet) + { + Set<X509Certificate> downloadedExtraCertificatesSet = + CertificateVerifier.downloadExtraCertificates(tryCert); + for (X509Certificate downloadedCertificate : downloadedExtraCertificatesSet) + { + if (!certSet.contains(downloadedCertificate)) + { + nextCertsToTrySet.add(downloadedCertificate); + certSet.add(downloadedCertificate); + downloadSize++; + } + } + } + certsToTrySet = nextCertsToTrySet; + } + if (downloadSize > 0) { - LOG.info("CA issuers: " + (certSet.size() - additionalCerts.size()) + " downloaded certificate(s) are new"); + LOG.info("CA issuers: " + downloadSize + " downloaded certificate(s) are new"); } // Prepare a set of trust anchors (set of root CA certificates) @@ -146,7 +172,8 @@ public final class CertificateVerifier PKIXCertPathBuilderResult verifiedCertChain = verifyCertificate( cert, trustAnchors, intermediateCerts, signDate); - LOG.info("Certification chain verified successfully"); + LOG.info("Certification chain verified successfully up to this root: " + + verifiedCertChain.getTrustAnchor().getTrustedCert().getSubjectX500Principal()); checkRevocations(cert, certSet, signDate); @@ -184,11 +211,16 @@ public final class CertificateVerifier X509Certificate issuerCert = null; for (X509Certificate additionalCert : additionalCerts) { - if (cert.getIssuerX500Principal().equals(additionalCert.getSubjectX500Principal())) + try { + cert.verify(additionalCert.getPublicKey(), SecurityProvider.getProvider().getName()); issuerCert = additionalCert; break; } + catch (GeneralSecurityException ex) + { + // not the issuer + } } // issuerCert is never null here. If it hadn't been found, then there wouldn't be a // verifiedCertChain earlier. @@ -312,10 +344,10 @@ public final class CertificateVerifier } ASN1TaggedObject location = (ASN1TaggedObject) obj.getObjectAt(1); ASN1OctetString uri = (ASN1OctetString) location.getObject(); + String urlString = new String(uri.getOctets()); InputStream in = null; try { - String urlString = new String(uri.getOctets()); LOG.info("CA issuers URL: " + urlString); in = new URL(urlString).openStream(); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); @@ -328,7 +360,7 @@ public final class CertificateVerifier } catch (IOException ex) { - LOG.warn(ex.getMessage(), ex); + LOG.warn(urlString + " failure: " + ex.getMessage(), ex); } catch (CertificateException ex) { diff --git a/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/OcspHelper.java b/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/OcspHelper.java index 0588ca9..77b777f 100644 --- a/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/OcspHelper.java +++ b/examples/src/main/java/org/apache/pdfbox/examples/signature/cert/OcspHelper.java @@ -86,6 +86,8 @@ public class OcspHelper private DEROctetString encodedNonce; private X509Certificate ocspResponderCertificate; private final JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter(); + + // SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux private static final Random rand = new SecureRandom(); /** @@ -585,7 +587,6 @@ public class OcspHelper private byte[] create16BytesNonce() { - // replace with SecureRandom.getInstanceStrong() on jdk8 and higher byte[] nonce = new byte[16]; rand.nextBytes(nonce); return nonce; diff --git a/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationCollector.java b/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationCollector.java index b908221..447cfa9 100644 --- a/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationCollector.java +++ b/examples/src/main/java/org/apache/pdfbox/examples/signature/validation/CertInformationCollector.java @@ -36,6 +36,7 @@ import org.apache.pdfbox.examples.signature.cert.CertificateVerifier; import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.encryption.SecurityProvider; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Object; import org.bouncycastle.asn1.cms.Attribute; import org.bouncycastle.asn1.cms.AttributeTable; @@ -140,10 +141,18 @@ public class CertInformationCollector { return; } - Attribute tsAttribute = signerInformation.getUnsignedAttributes() + Attribute tsAttribute = unsignedAttributes .get(PKCSObjectIdentifiers.id_aa_signatureTimeStampToken); - - ASN1Object tsSeq = (ASN1Object) tsAttribute.getAttrValues().getObjectAt(0); + if (tsAttribute == null) + { + return; + } + ASN1Encodable obj0 = tsAttribute.getAttrValues().getObjectAt(0); + if (!(obj0 instanceof ASN1Object)) + { + return; + } + ASN1Object tsSeq = (ASN1Object) obj0; try { @@ -240,28 +249,27 @@ public class CertInformationCollector for (X509Certificate issuer : certificateSet) { - if (certificate.getIssuerX500Principal().equals(issuer.getSubjectX500Principal())) + try { - try - { - certificate.verify(issuer.getPublicKey(), SecurityProvider.getProvider().getName()); - } - catch (GeneralSecurityException ex) - { - throw new CertificateProccessingException(ex); - } + certificate.verify(issuer.getPublicKey(), SecurityProvider.getProvider().getName()); LOG.info("Found the right Issuer Cert! for Cert: " + certificate.getSubjectX500Principal() - + "\n" + issuer.getSubjectX500Principal()); + + "\n" + issuer.getSubjectX500Principal()); certInfo.issuerCertificate = issuer; certInfo.certChain = new CertSignatureInformation(); traverseChain(issuer, certInfo.certChain, maxDepth - 1); break; } + catch (GeneralSecurityException ex) + { + // not the issuer + } } if (certInfo.issuerCertificate == null) { throw new IOException( - "No Issuer Certificate found for Cert: " + certificate.getSubjectX500Principal()); + "No Issuer Certificate found for Cert: '" + + certificate.getSubjectX500Principal() + "', i.e. Cert '" + + certificate.getIssuerX500Principal() + "' is missing in the chain"); } } diff --git a/examples/src/test/java/org/apache/pdfbox/examples/pdfa/CreatePDFATest.java b/examples/src/test/java/org/apache/pdfbox/examples/pdfa/CreatePDFATest.java index cf21a36..a7fecf4 100644 --- a/examples/src/test/java/org/apache/pdfbox/examples/pdfa/CreatePDFATest.java +++ b/examples/src/test/java/org/apache/pdfbox/examples/pdfa/CreatePDFATest.java @@ -86,7 +86,7 @@ public class CreatePDFATest extends TestCase PDDocumentCatalog catalog = document.getDocumentCatalog(); PDMetadata meta = catalog.getMetadata(); DomXmpParser xmpParser = new DomXmpParser(); - XMPMetadata metadata = xmpParser.parse(meta.createInputStream()); + XMPMetadata metadata = xmpParser.parse(meta.toByteArray()); DublinCoreSchema dc = metadata.getDublinCoreSchema(); assertEquals(pdfaFilename, dc.getTitle()); document.close(); diff --git a/examples/src/test/java/org/apache/pdfbox/examples/pdmodel/TestCreateSignature.java b/examples/src/test/java/org/apache/pdfbox/examples/pdmodel/TestCreateSignature.java index ddba991..f87dda6 100644 --- a/examples/src/test/java/org/apache/pdfbox/examples/pdmodel/TestCreateSignature.java +++ b/examples/src/test/java/org/apache/pdfbox/examples/pdmodel/TestCreateSignature.java @@ -16,39 +16,58 @@ */ package org.apache.pdfbox.examples.pdmodel; +import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.security.Security; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; import java.text.MessageFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSInputStream; import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.cos.COSString; import org.apache.pdfbox.examples.interactive.form.CreateSimpleForm; +import org.apache.pdfbox.examples.signature.CreateEmbeddedTimeStamp; import org.apache.pdfbox.examples.signature.CreateEmptySignatureForm; import org.apache.pdfbox.examples.signature.CreateSignature; +import org.apache.pdfbox.examples.signature.CreateSignedTimeStamp; import org.apache.pdfbox.examples.signature.CreateVisibleSignature; +import org.apache.pdfbox.examples.signature.CreateVisibleSignature2; +import org.apache.pdfbox.examples.signature.SigUtils; +import org.apache.pdfbox.examples.signature.cert.CertificateVerificationException; +import org.apache.pdfbox.examples.signature.validation.AddValidationInformation; import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.encryption.SecurityProvider; @@ -57,25 +76,32 @@ import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.util.Hex; + import org.apache.wink.client.MockHttpServer; -import org.bouncycastle.asn1.ASN1Object; -import org.bouncycastle.asn1.cms.Attribute; -import org.bouncycastle.asn1.cms.AttributeTable; -import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; + +import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.OCSPException; +import org.bouncycastle.cert.ocsp.OCSPResp; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.cms.SignerInformation; -import org.bouncycastle.cms.SignerInformationVerifier; import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import org.bouncycastle.crypto.prng.FixedSecureRandom; +import org.bouncycastle.operator.ContentVerifierProvider; import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; import org.bouncycastle.tsp.TSPException; import org.bouncycastle.tsp.TSPValidationException; import org.bouncycastle.tsp.TimeStampToken; +import org.bouncycastle.tsp.TimeStampTokenInfo; +import org.bouncycastle.util.CollectionStore; import org.bouncycastle.util.Selector; import org.bouncycastle.util.Store; + import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -89,6 +115,8 @@ import org.junit.runners.Parameterized; @RunWith(Parameterized.class) public class TestCreateSignature { + private static CertificateFactory certificateFactory = null; + private static KeyStore keyStore = null; private static final String inDir = "src/test/resources/org/apache/pdfbox/examples/signature/"; private static final String outDir = "target/test-output/"; private static final String keystorePath = inDir + "keystore.p12"; @@ -113,11 +141,16 @@ public class TestCreateSignature @BeforeClass public static void init() throws Exception { + Security.addProvider(SecurityProvider.getProvider()); + certificateFactory = CertificateFactory.getInstance("X.509"); + + // load the keystore + keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new FileInputStream(keystorePath), password.toCharArray()); + new File("target/test-output").mkdirs(); - KeyStore keystore = KeyStore.getInstance("PKCS12"); - keystore.load(new FileInputStream(keystorePath), password.toCharArray()); - certificate = keystore.getCertificateChain(keystore.aliases().nextElement())[0]; + certificate = keyStore.getCertificateChain(keyStore.aliases().nextElement())[0]; tsa = System.getProperty("org.apache.pdfbox.examples.pdmodel.tsa"); } @@ -129,24 +162,33 @@ public class TestCreateSignature * @throws CMSException * @throws OperatorCreationException * @throws TSPException + * @throws CertificateVerificationException */ @Test public void testDetachedSHA256() throws IOException, CMSException, OperatorCreationException, GeneralSecurityException, - TSPException + TSPException, CertificateVerificationException { - // load the keystore - KeyStore keystore = KeyStore.getInstance("PKCS12"); - keystore.load(new FileInputStream(keystorePath), password.toCharArray()); - // sign PDF - CreateSignature signing = new CreateSignature(keystore, password.toCharArray()); + CreateSignature signing = new CreateSignature(keyStore, password.toCharArray()); signing.setExternalSigning(externallySign); final String fileName = getOutputFileName("signed{0}.pdf"); + final String fileName2 = getOutputFileName("signed{0}-late-tsa.pdf"); signing.signDetached(new File(inDir + "sign_me.pdf"), new File(outDir + fileName)); - checkSignature(new File(inDir, "sign_me.pdf"), new File(outDir, fileName)); + checkSignature(new File(inDir, "sign_me.pdf"), new File(outDir, fileName), false); + + // Also test CreateEmbeddedTimeStamp if tsa URL is available + if (tsa == null || tsa.isEmpty()) + { + System.err.println("No TSA URL defined, test skipped"); + return; + } + + CreateEmbeddedTimeStamp tsaSigning = new CreateEmbeddedTimeStamp(tsa); + tsaSigning.embedTimeStamp(new File(outDir, fileName), new File(outDir, fileName2)); + checkSignature(new File(outDir, fileName), new File(outDir, fileName2), true); } /** @@ -163,11 +205,12 @@ public class TestCreateSignature * @throws CMSException * @throws OperatorCreationException * @throws TSPException + * @throws CertificateVerificationException */ @Test public void testDetachedSHA256WithTSA() throws IOException, CMSException, OperatorCreationException, GeneralSecurityException, - TSPException + TSPException, CertificateVerificationException { // mock TSA response content InputStream input = new FileInputStream(inDir + "tsa_response.asn1"); @@ -184,15 +227,11 @@ public class TestCreateSignature response.setMockResponseCode(200); mockServer.setMockHttpServerResponses(response); - // load the keystore - KeyStore keystore = KeyStore.getInstance("PKCS12"); - keystore.load(new FileInputStream(keystorePath), password.toCharArray()); - String inPath = inDir + "sign_me_tsa.pdf"; String outPath = outDir + getOutputFileName("signed{0}_tsa.pdf"); // sign PDF (will fail due to nonce and timestamp differing) - CreateSignature signing1 = new CreateSignature(keystore, password.toCharArray()); + CreateSignature signing1 = new CreateSignature(keyStore, password.toCharArray()); signing1.setExternalSigning(externallySign); try { @@ -213,13 +252,66 @@ public class TestCreateSignature return; } - CreateSignature signing2 = new CreateSignature(keystore, password.toCharArray()); + CreateSignature signing2 = new CreateSignature(keyStore, password.toCharArray()); signing2.setExternalSigning(externallySign); signing2.signDetached(new File(inPath), new File(outPath), tsa); - checkSignature(new File(inPath), new File(outPath)); + checkSignature(new File(inPath), new File(outPath), true); System.out.println("TSA test successful"); } - + + /** + * Test timestamp only signature (ETSI.RFC3161). + * + * @throws IOException + * @throws CMSException + * @throws OperatorCreationException + * @throws GeneralSecurityException + * @throws TSPException + * @throws CertificateVerificationException + */ + @Test + public void testCreateSignedTimeStamp() + throws IOException, CMSException, OperatorCreationException, GeneralSecurityException, + TSPException, CertificateVerificationException + { + if (externallySign) + { + return; // runs only once, independent of externallySign + } + if (tsa == null || tsa.isEmpty()) + { + System.err.println("No TSA URL defined, test skipped"); + return; + } + final String fileName = getOutputFileName("timestamped{0}.pdf"); + CreateSignedTimeStamp signing = new CreateSignedTimeStamp(tsa); + signing.signDetached(new File(inDir + "sign_me.pdf"), new File(outDir + fileName)); + + PDDocument doc = PDDocument.load(new File(outDir + fileName)); + PDSignature signature = doc.getLastSignatureDictionary(); + COSString contents = (COSString) signature.getCOSObject().getDictionaryObject(COSName.CONTENTS); + byte[] signedFileContent = + signature.getSignedContent(new FileInputStream(new File(outDir, fileName))); + TimeStampToken timeStampToken = new TimeStampToken(new CMSSignedData(contents.getBytes())); + certificateFactory.getInstance("X.509"); + ByteArrayInputStream certStream = new ByteArrayInputStream(contents.getBytes()); + Collection<? extends Certificate> certs = certificateFactory.generateCertificates(certStream); + + String hashAlgorithm = timeStampToken.getTimeStampInfo().getMessageImprintAlgOID().getId(); + // compare the hash of the signed content with the hash in the timestamp + Assert.assertArrayEquals(MessageDigest.getInstance(hashAlgorithm).digest(signedFileContent), + timeStampToken.getTimeStampInfo().getMessageImprintDigest()); + + X509Certificate certFromTimeStamp = (X509Certificate) certs.iterator().next(); + SigUtils.checkTimeStampCertificateUsage(certFromTimeStamp); + SigUtils.validateTimestampToken(timeStampToken); + SigUtils.verifyCertificateChain(timeStampToken.getCertificates(), + certFromTimeStamp, + timeStampToken.getTimeStampInfo().getGenTime()); + + doc.close(); + } + /** * Test creating visual signature. * @@ -228,20 +320,17 @@ public class TestCreateSignature * @throws OperatorCreationException * @throws GeneralSecurityException * @throws TSPException + * @throws CertificateVerificationException */ @Test public void testCreateVisibleSignature() throws IOException, CMSException, OperatorCreationException, GeneralSecurityException, - TSPException + TSPException, CertificateVerificationException { - // load the keystore - KeyStore keystore = KeyStore.getInstance("PKCS12"); - keystore.load(new FileInputStream(keystorePath), password.toCharArray()); - // sign PDF - String inPath = inDir + "sign_me.pdf"; + String inPath = inDir + "sign_me_visible.pdf"; FileInputStream fis = new FileInputStream(jpegPath); - CreateVisibleSignature signing = new CreateVisibleSignature(keystore, password.toCharArray()); + CreateVisibleSignature signing = new CreateVisibleSignature(keyStore, password.toCharArray()); signing.setVisibleSignDesigner(inPath, 0, 0, -50, fis, 1); signing.setVisibleSignatureProperties("name", "location", "Security", 0, 1, true); signing.setExternalSigning(externallySign); @@ -250,7 +339,36 @@ public class TestCreateSignature signing.signPDF(new File(inPath), destFile, null); fis.close(); - checkSignature(new File(inPath), destFile); + checkSignature(new File(inPath), destFile, false); + } + + /** + * Test creating visual signature with the modernized example. + * + * @throws IOException + * @throws CMSException + * @throws OperatorCreationException + * @throws GeneralSecurityException + * @throws TSPException + * @throws CertificateVerificationException + */ + @Test + public void testCreateVisibleSignature2() + throws IOException, CMSException, OperatorCreationException, GeneralSecurityException, + TSPException, CertificateVerificationException + { + // sign PDF + String inPath = inDir + "sign_me_visible.pdf"; + File destFile; + + CreateVisibleSignature2 signing = new CreateVisibleSignature2(keyStore, password.toCharArray()); + Rectangle2D humanRect = new Rectangle2D.Float(100, 200, 150, 50); + signing.setImageFile(new File(jpegPath)); + signing.setExternalSigning(externallySign); + destFile = new File(outDir + getOutputFileName("signed{0}_visible2.pdf")); + signing.signPDF(new File(inPath), destFile, humanRect, null); + + checkSignature(new File(inPath), destFile, false); } /** @@ -259,19 +377,19 @@ public class TestCreateSignature * * @throws IOException * @throws NoSuchAlgorithmException - * @throws KeyStoreException * @throws CertificateException * @throws UnrecoverableKeyException * @throws CMSException * @throws OperatorCreationException * @throws GeneralSecurityException * @throws TSPException + * @throws CertificateVerificationException */ @Test - public void testPDFBox3978() throws IOException, NoSuchAlgorithmException, KeyStoreException, + public void testPDFBox3978() throws IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, CMSException, OperatorCreationException, GeneralSecurityException, - TSPException + TSPException, CertificateVerificationException { String filename = outDir + "EmptySignatureForm.pdf"; String filenameSigned1 = outDir + "EmptySignatureForm-signed1.pdf"; @@ -282,19 +400,15 @@ public class TestCreateSignature return; } - // load the keystore - KeyStore keystore = KeyStore.getInstance("PKCS12"); - keystore.load(new FileInputStream(keystorePath), password.toCharArray()); - // create file with empty signature CreateEmptySignatureForm.main(new String[]{filename}); // sign PDF - CreateSignature signing1 = new CreateSignature(keystore, password.toCharArray()); + CreateSignature signing1 = new CreateSignature(keyStore, password.toCharArray()); signing1.setExternalSigning(false); signing1.signDetached(new File(filename), new File(filenameSigned1)); - checkSignature(new File(filename), new File(filenameSigned1)); + checkSignature(new File(filename), new File(filenameSigned1), false); PDDocument doc1 = PDDocument.load(new File(filenameSigned1)); List<PDSignature> signatureDictionaries = doc1.getSignatureDictionaries(); @@ -303,14 +417,14 @@ public class TestCreateSignature // do visual signing in the field FileInputStream fis = new FileInputStream(jpegPath); - CreateVisibleSignature signing2 = new CreateVisibleSignature(keystore, password.toCharArray()); + CreateVisibleSignature signing2 = new CreateVisibleSignature(keyStore, password.toCharArray()); signing2.setVisibleSignDesigner(filenameSigned1, 0, 0, -50, fis, 1); signing2.setVisibleSignatureProperties("name", "location", "Security", 0, 1, true); - signing2.setExternalSigning(externallySign); + signing2.setExternalSigning(true); signing2.signPDF(new File(filenameSigned1), new File(filenameSigned2), null, "Signature1"); fis.close(); - checkSignature(new File(filenameSigned1), new File(filenameSigned2)); + checkSignature(new File(filenameSigned1), new File(filenameSigned2), false); PDDocument doc2 = PDDocument.load(new File(filenameSigned2)); signatureDictionaries = doc2.getSignatureDictionaries(); @@ -324,9 +438,9 @@ public class TestCreateSignature } // This check fails with a file created with the code before PDFBOX-3011 was solved. - private void checkSignature(File origFile, File signedFile) + private void checkSignature(File origFile, File signedFile, boolean checkTimeStamp) throws IOException, CMSException, OperatorCreationException, GeneralSecurityException, - TSPException + TSPException, CertificateVerificationException { PDDocument document = PDDocument.load(origFile); // get string representation of pages COSObject @@ -380,44 +494,37 @@ public class TestCreateSignature Assert.fail("Signature verification failed"); } - TimeStampToken timeStampToken = extractTimeStampTokenFromSignerInformation(signerInformation); - if (timeStampToken != null) + TimeStampToken timeStampToken = SigUtils.extractTimeStampTokenFromSignerInformation(signerInformation); + if (checkTimeStamp) { - validateTimestampToken(timeStampToken); - } - } - document.close(); - } + Assert.assertNotNull(timeStampToken); + SigUtils.validateTimestampToken(timeStampToken); - private void validateTimestampToken(TimeStampToken timeStampToken) - throws TSPException, CertificateException, OperatorCreationException, IOException - { - // https://stackoverflow.com/questions/42114742/ - @SuppressWarnings("unchecked") // TimeStampToken.getSID() is untyped - Collection<X509CertificateHolder> tstMatches = - timeStampToken.getCertificates().getMatches((Selector<X509CertificateHolder>) timeStampToken.getSID()); - X509CertificateHolder holder = tstMatches.iterator().next(); - SignerInformationVerifier siv = new JcaSimpleSignerInfoVerifierBuilder().setProvider(SecurityProvider.getProvider()).build(holder); - timeStampToken.validate(siv); - } + TimeStampTokenInfo timeStampInfo = timeStampToken.getTimeStampInfo(); - private TimeStampToken extractTimeStampTokenFromSignerInformation(SignerInformation signerInformation) - throws CMSException, IOException, TSPException - { - if (signerInformation.getUnsignedAttributes() == null) - { - return null; - } - AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); - // https://stackoverflow.com/questions/1647759/how-to-validate-if-a-signed-jar-contains-a-timestamp - Attribute attribute = unsignedAttributes.get(PKCSObjectIdentifiers.id_aa_signatureTimeStampToken); - if (attribute == null) - { - return null; + // compare the hash of the signed content with the hash in the timestamp + byte[] tsMessageImprintDigest = timeStampInfo.getMessageImprintDigest(); + String hashAlgorithm = timeStampInfo.getMessageImprintAlgOID().getId(); + byte[] sigMessageImprintDigest = MessageDigest.getInstance(hashAlgorithm).digest(signerInformation.getSignature()); + Assert.assertArrayEquals("timestamp signature verification failed", sigMessageImprintDigest, tsMessageImprintDigest); + + Store<X509CertificateHolder> tsCertStore = timeStampToken.getCertificates(); + + // get the certificate from the timeStampToken + @SuppressWarnings("unchecked") // TimeStampToken.getSID() is untyped + Collection<X509CertificateHolder> tsCertStoreMatches = tsCertStore.getMatches(timeStampToken.getSID()); + X509CertificateHolder certHolderFromTimeStamp = tsCertStoreMatches.iterator().next(); + X509Certificate certFromTimeStamp = new JcaX509CertificateConverter().getCertificate(certHolderFromTimeStamp); + + SigUtils.checkTimeStampCertificateUsage(certFromTimeStamp); + SigUtils.verifyCertificateChain(tsCertStore, certFromTimeStamp, timeStampInfo.getGenTime()); + } + else + { + Assert.assertNull(timeStampToken); + } } - ASN1Object obj = (ASN1Object) attribute.getAttrValues().getObjectAt(0); - CMSSignedData signedTSTData = new CMSSignedData(obj.getEncoded()); - return new TimeStampToken(signedTSTData); + document.close(); } private String calculateDigestString(InputStream inputStream) throws NoSuchAlgorithmException, IOException @@ -493,12 +600,8 @@ public class TestCreateSignature CreateSimpleForm.main(new String[0]); // creates "target/SimpleForm.pdf" - // load the keystore - KeyStore keystore = KeyStore.getInstance("PKCS12"); - keystore.load(new FileInputStream(keystorePath), password.toCharArray()); - // sign PDF - CreateSignature signing = new CreateSignature(keystore, password.toCharArray()); + CreateSignature signing = new CreateSignature(keyStore, password.toCharArray()); signing.setExternalSigning(externallySign); final String fileNameSigned = getOutputFileName("SimpleForm_signed{0}.pdf"); @@ -506,7 +609,7 @@ public class TestCreateSignature final String fileNameResaved2 = getOutputFileName("SimpleForm_signed{0}_incrementallyresaved2.pdf"); signing.signDetached(new File("target/SimpleForm.pdf"), new File(outDir + fileNameSigned)); - checkSignature(new File("target/SimpleForm.pdf"), new File(outDir, fileNameSigned)); + checkSignature(new File("target/SimpleForm.pdf"), new File(outDir, fileNameSigned), false); PDDocument doc = PDDocument.load(new File(outDir, fileNameSigned)); @@ -541,7 +644,7 @@ public class TestCreateSignature ((COSDictionary) field.getWidgets().get(0).getAppearance().getNormalAppearance().getCOSObject()).setNeedToBeUpdated(true); doc.saveIncremental(fileOutputStream); doc.close(); - checkSignature(new File("target/SimpleForm.pdf"), new File(outDir, fileNameResaved1)); + checkSignature(new File("target/SimpleForm.pdf"), new File(outDir, fileNameResaved1), false); doc = PDDocument.load(new File(outDir, fileNameResaved1)); @@ -561,7 +664,10 @@ public class TestCreateSignature @Test public void testPDFBox4784() throws Exception { - + if (!externallySign) + { + return; + } Date signingTime = new Date(); byte[] defaultSignedOne = signEncrypted(null, signingTime); @@ -573,16 +679,260 @@ public class TestCreateSignature signingTime); byte[] fixedRandomSignedTwo = signEncrypted(new FixedSecureRandom(new byte[128]), signingTime); - Assert.assertTrue(Arrays.equals(fixedRandomSignedOne, fixedRandomSignedTwo)); + Assert.assertArrayEquals(fixedRandomSignedOne, fixedRandomSignedTwo); + } + /** + * Test getting CRLs when OCSP (adobe-ocsp.geotrust.com) is unavailable. + * This validates the certificates of the signature from the file 083698.pdf, which is + * 109TH CONGRESS 2D SESSION H. R. 5500, from MAY 25, 2006. + * + * @throws IOException + * @throws CMSException + * @throws CertificateException + * @throws TSPException + * @throws OperatorCreationException + * @throws CertificateVerificationException + * @throws NoSuchAlgorithmException + */ + @Test + public void testCRL() throws IOException, CMSException, CertificateException, TSPException, + OperatorCreationException, CertificateVerificationException, NoSuchAlgorithmException + { + if (externallySign) + { + return; // runs only once, independent of externallySign + } + String hexSignature; + BufferedReader bfr + = new BufferedReader(new InputStreamReader(new FileInputStream(inDir + "hexsignature.txt"))); + hexSignature = bfr.readLine(); + bfr.close(); + + CMSSignedData signedData = new CMSSignedData(Hex.decodeHex(hexSignature)); + Collection<SignerInformation> signers = signedData.getSignerInfos().getSigners(); + SignerInformation signerInformation = signers.iterator().next(); + Store<X509CertificateHolder> certificatesStore = signedData.getCertificates(); + @SuppressWarnings("unchecked") // SignerInformation.getSID() is untyped + Collection<X509CertificateHolder> matches = certificatesStore.getMatches(signerInformation.getSID()); + X509CertificateHolder certificateHolder = matches.iterator().next(); + X509Certificate certFromSignedData = new JcaX509CertificateConverter().getCertificate(certificateHolder); + SigUtils.checkCertificateUsage(certFromSignedData); + + TimeStampToken timeStampToken = SigUtils.extractTimeStampTokenFromSignerInformation(signerInformation); + SigUtils.validateTimestampToken(timeStampToken); + @SuppressWarnings("unchecked") // TimeStampToken.getSID() is untyped + Collection<X509CertificateHolder> tstMatches = + timeStampToken.getCertificates().getMatches((Selector<X509CertificateHolder>) timeStampToken.getSID()); + X509CertificateHolder tstCertHolder = tstMatches.iterator().next(); + X509Certificate certFromTimeStamp = new JcaX509CertificateConverter().getCertificate(tstCertHolder); + // merge both stores using a set to remove duplicates + HashSet<X509CertificateHolder> certificateHolderSet = new HashSet<X509CertificateHolder>(); + certificateHolderSet.addAll(certificatesStore.getMatches(null)); + certificateHolderSet.addAll(timeStampToken.getCertificates().getMatches(null)); + SigUtils.verifyCertificateChain(new CollectionStore<X509CertificateHolder>(certificateHolderSet), + certFromTimeStamp, + timeStampToken.getTimeStampInfo().getGenTime()); + SigUtils.checkTimeStampCertificateUsage(certFromTimeStamp); + + // compare the hash of the signature with the hash in the timestamp + byte[] tsMessageImprintDigest = timeStampToken.getTimeStampInfo().getMessageImprintDigest(); + String hashAlgorithm = timeStampToken.getTimeStampInfo().getMessageImprintAlgOID().getId(); + byte[] sigMessageImprintDigest = MessageDigest.getInstance(hashAlgorithm).digest(signerInformation.getSignature()); + Assert.assertArrayEquals(tsMessageImprintDigest, sigMessageImprintDigest); + + certFromSignedData.checkValidity(timeStampToken.getTimeStampInfo().getGenTime()); + SigUtils.verifyCertificateChain(certificatesStore, certFromSignedData, timeStampToken.getTimeStampInfo().getGenTime()); } - private byte[] signEncrypted(SecureRandom secureRandom, Date signingTime) throws Exception + /** + * Test adding LTV information. This tests the status quo. If we use a new file (or if the file + * gets updated) then the test may have to be adjusted. The test is not really perfect, but it + * tries to check a minimum of things that should match. If the test fails and you didn't change + * anything in signing, then find out whether some external servers involved are unresponsive. + * At the time of writing this, the OCSP server http://ocsp.quovadisglobal.com responds with 502 + * "UNAUTHORIZED". That is not a problem as long as the CRL URL works. + * + * @throws java.io.IOException + * @throws java.security.GeneralSecurityException + * @throws org.bouncycastle.cert.ocsp.OCSPException + * @throws org.bouncycastle.operator.OperatorCreationException + * @throws org.bouncycastle.cms.CMSException + */ + @Test + public void testAddValidationInformation() + throws IOException, GeneralSecurityException, OCSPException, OperatorCreationException, CMSException { - KeyStore keystore = KeyStore.getInstance("PKCS12"); - keystore.load(new FileInputStream(keystorePath), password.toCharArray()); + if (externallySign) + { + return; // runs only once, independent of externallySign + } + File inFile = new File("target/pdfs", "QV_RCA1_RCA3_CPCPS_V4_11.pdf"); + String name = inFile.getName(); + String substring = name.substring(0, name.lastIndexOf('.')); + + File outFile = new File(outDir, substring + "_LTV.pdf"); + AddValidationInformation addValidationInformation = new AddValidationInformation(); + addValidationInformation.validateSignature(inFile, outFile); + + certificateFactory.getInstance("X.509"); + PDDocument doc = PDDocument.load(outFile); + + PDSignature signature = doc.getLastSignatureDictionary(); + COSString contents = (COSString) signature.getCOSObject().getDictionaryObject(COSName.CONTENTS); + + PDDocumentCatalog docCatalog = doc.getDocumentCatalog(); + COSDictionary dssDict = docCatalog.getCOSObject().getCOSDictionary(COSName.getPDFName("DSS")); + COSArray dssCertArray = dssDict.getCOSArray(COSName.getPDFName("Certs")); + COSDictionary vriDict = dssDict.getCOSDictionary(COSName.getPDFName("VRI")); + + // Check that all known signature certificates are in the VRI/signaturehash/Cert array + byte[] signatureHash = MessageDigest.getInstance("SHA-1").digest(contents.getBytes()); + String hexSignatureHash = Hex.getString(signatureHash); + System.out.println("hexSignatureHash: " + hexSignatureHash); + CMSSignedData signedData = new CMSSignedData(contents.getBytes()); + Store<X509CertificateHolder> certificatesStore = signedData.getCertificates(); + HashSet<X509CertificateHolder> certificateHolderSet = + new HashSet<X509CertificateHolder>(certificatesStore.getMatches(null)); + COSDictionary sigDict = vriDict.getCOSDictionary(COSName.getPDFName(hexSignatureHash)); + COSArray sigCertArray = sigDict.getCOSArray(COSName.getPDFName("Cert")); + Set<X509CertificateHolder> sigCertHolderSetFromVRIArray = new HashSet<X509CertificateHolder>(); + for (int i = 0; i < sigCertArray.size(); ++i) + { + COSStream certStream = (COSStream) sigCertArray.getObject(i); + COSInputStream is = certStream.createInputStream(); + sigCertHolderSetFromVRIArray.add(new X509CertificateHolder(IOUtils.toByteArray(is))); + is.close(); + } + for (X509CertificateHolder holder : certificateHolderSet) + { + if (holder.getSubject().toString().contains("QuoVadis OCSP Authority Signature")) + { + continue; // not relevant here + } + Assert.assertTrue("VRI/signaturehash/Cert array doesn't contain " + holder.getSubject(), + sigCertHolderSetFromVRIArray.contains(holder)); + } + + // Get all certificates. Each one should either be issued (= signed) by a certificate of the set + Set<X509Certificate> certSet = new HashSet<X509Certificate>(); + for (int i = 0; i < dssCertArray.size(); ++i) + { + COSStream certStream = (COSStream) dssCertArray.getObject(i); + COSInputStream is = certStream.createInputStream(); + X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(is); + is.close(); + certSet.add(cert); + } + for (X509Certificate cert : certSet) + { + boolean verified = false; + for (X509Certificate cert2 : certSet) + { + try + { + cert.verify(cert2.getPublicKey(), SecurityProvider.getProvider().getName()); + verified = true; + } + catch (GeneralSecurityException ex) + { + // not the issuer + } + } + Assert.assertTrue("Certificate " + cert.getSubjectX500Principal() + + " not issued by any certificate in the Certs array", verified); + } - CreateSignature signing = new CreateSignature(keystore, password.toCharArray()); + // Each CRL should be signed by one of the certificates in Certs + Set<X509CRL> crlSet = new HashSet<X509CRL>(); + COSArray crlArray = dssDict.getCOSArray(COSName.getPDFName("CRLs")); + for (int i = 0; i < crlArray.size(); ++i) + { + COSStream crlStream = (COSStream) crlArray.getObject(i); + COSInputStream is = crlStream.createInputStream(); + X509CRL cert = (X509CRL) certificateFactory.generateCRL(is); + is.close(); + crlSet.add(cert); + } + for (X509CRL crl : crlSet) + { + boolean crlVerified = false; + X509Certificate crlIssuerCert = null; + for (X509Certificate cert : certSet) + { + try + { + crl.verify(cert.getPublicKey(), SecurityProvider.getProvider().getName()); + crlVerified = true; + crlIssuerCert = cert; + } + catch (GeneralSecurityException ex) + { + // not the issuer + } + } + Assert.assertTrue("issuer of CRL not found in Certs array", crlVerified); + + byte[] crlSignatureHash = MessageDigest.getInstance("SHA-1").digest(crl.getSignature()); + String hexCrlSignatureHash = Hex.getString(crlSignatureHash); + System.out.println("hexCrlSignatureHash: " + hexCrlSignatureHash); + + // Check that the issueing certificate is in the VRI array + COSDictionary crlSigDict = vriDict.getCOSDictionary(COSName.getPDFName(hexCrlSignatureHash)); + COSArray certArray2 = crlSigDict.getCOSArray(COSName.getPDFName("Cert")); + COSStream certStream = (COSStream) certArray2.getObject(0); + COSInputStream is2 = certStream.createInputStream(); + X509CertificateHolder certHolder2 = new X509CertificateHolder(IOUtils.toByteArray(is2)); + is2.close(); + + Assert.assertEquals("CRL issuer certificate missing in VRI " + hexCrlSignatureHash, + certHolder2, new X509CertificateHolder(crlIssuerCert.getEncoded())); + } + + Set<OCSPResp> oscpSet = new HashSet<OCSPResp>(); + COSArray ocspArray = dssDict.getCOSArray(COSName.getPDFName("OCSPs")); + for (int i = 0; i < ocspArray.size(); ++i) + { + COSStream ocspStream = (COSStream) ocspArray.getObject(i); + COSInputStream is = ocspStream.createInputStream(); + OCSPResp ocspResp = new OCSPResp(is); + is.close(); + oscpSet.add(ocspResp); + } + for (OCSPResp ocspResp : oscpSet) + { + BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResp.getResponseObject(); + Assert.assertEquals(OCSPResponseStatus.SUCCESSFUL, ocspResp.getStatus()); + Assert.assertTrue("OCSP should have at least 1 certificate", basicResponse.getCerts().length >= 1); + byte[] ocspSignatureHash = MessageDigest.getInstance("SHA-1").digest(basicResponse.getSignature()); + String hexOcspSignatureHash = Hex.getString(ocspSignatureHash); + System.out.println("ocspSignatureHash: " + hexOcspSignatureHash); + long secondsOld = (System.currentTimeMillis() - basicResponse.getProducedAt().getTime()) / 1000; + Assert.assertTrue("OCSP answer is too old, is from " + secondsOld + " seconds ago", + secondsOld < 10); + + X509CertificateHolder ocspCertHolder = basicResponse.getCerts()[0]; + ContentVerifierProvider verifier = new JcaContentVerifierProviderBuilder().setProvider(SecurityProvider.getProvider()).build(ocspCertHolder); + Assert.assertTrue(basicResponse.isSignatureValid(verifier)); + + COSDictionary ocspSigDict = vriDict.getCOSDictionary(COSName.getPDFName(hexOcspSignatureHash)); + + // Check that the Cert is in the VRI array + COSArray certArray2 = ocspSigDict.getCOSArray(COSName.getPDFName("Cert")); + COSStream certStream = (COSStream) certArray2.getObject(0); + COSInputStream is2 = certStream.createInputStream(); + X509CertificateHolder certHolder2 = new X509CertificateHolder(IOUtils.toByteArray(is2)); + is2.close(); + + Assert.assertEquals("OCSP certificate is not in the VRI array", certHolder2, ocspCertHolder); + } + + doc.close(); + } + + private byte[] signEncrypted(SecureRandom secureRandom, Date signingTime) throws Exception + { + CreateSignature signing = new CreateSignature(keyStore, password.toCharArray()); signing.setExternalSigning(true); File inFile = new File(inDir + "sign_me_protected.pdf"); diff --git a/examples/src/test/resources/org/apache/pdfbox/examples/signature/hexsignature.txt b/examples/src/test/resources/org/apache/pdfbox/examples/signature/hexsignature.txt new file mode 100644 index 0000000..8cb145d --- /dev/null +++ b/examples/src/test/resources/org/apache/pdfbox/examples/signature/hexsignature.txt @@ -0,0 +1 @@ +308006092a864886f70d010702a0803080020101310b300906052b0e03021a0500308006092a864886f70d0107010000a0820edb308204a130820389a00302010202043e1cbd28300d06092a864886f70d01010505003069310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f74204341301e170d3033303130383233333732335a170d3233303130393030303732335a3069310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f7420434130820122300d06092a864886f70d01010105000382010f003082010a0282010100cc4f5484f7a7a2e733537f3f9c12886b2c9947677e0f1eb9ad1488f9c310d81df0f0d59f690a2f5935b0cc6ca94c9c15a09fce20bfa0cf54e2e02066453f3986387e9cc48e0722c624f60112b035df55ea6990b0db85371ee24e07b242a16a1369a066ea809111592a9b08795a20442dc9bd73388b3c2fe0431b5db30bf0af351a29feefa692dd814c9d3d598ead313c407e9b913606fce25c8dd18d26d55c45cfaf653fb1aad26296f4a838eaba6042f4f41c4a3515cef84e22560f9518c5f8969f9ffbb0b77825e9806bbdd60af0c674949df30f50db9a77ce4b7083238da0ca7820445c3c5464f1eaa230199fea4c064d06784b5e92df22d2c967b37ad2010203010001a382014f3082014b301106096086480186f842010104040302000730818e0603551d1f048186308183308180a07ea07ca47a3078310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f74204341310d300b0603550403130443524c31302b0603551d1004243022800f32303033303130383233333732335a810f32303233303130393030303732335a300b0603551d0f040403020106301f0603551d2304183016801482b7384a93aa9b10ef80bbd954e2f10ffb809cde301d0603551d0e0416041482b7384a93aa9b10ef80bbd954e2f10ffb809cde300c0603551d13040530030101ff301d06092a864886f67d0741000410300e1b0856362e303a342e3003020490300d06092a864886f70d0101050500038201010032da9f4375c1fa6fc96fdbab1d36373ebc611936b7023c1d2359986c9eee4d85e754c8201fa7d4bbe2bf00777d246b702f5cc13a7649b5d3e023842a716a22f3c127299815f63590e4044cc38dbc9f611ce7fd248cd144438c16ba9b4da5d4352fbc11cebdf751378d9f90e414f1183fbee9591235f93392f39ee0d56b9a719b994bc871c3e1b16109c4e5fa91f0423a377d34f972e8cdaa621c21e9d5f48210e37b05b62d68560b7e7e922c6f4d72820ced5674b29db9ab2d2b1d105fdb2775708ffd1dd7e202a079e51ce5ffaf6440512d9e9b47db42a57c1fc2a648b0d7be92694da4f62957c5781118dc8751ca13b2629d4f2b32bd31a5c1fa52ab0588c8308204cb308203b3a00302010202043e1cbdb5300d06092a864886f70d01010505003069310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f74204341301e170d3034303131373030303333395a170d3135303131353038303030305a3045310b300906035504061302555331163014060355040a130d47656f547275737420496e632e311e301c0603550403131547656f547275737420434120666f722041646f626530820122300d06092a864886f70d01010105000382010f003082010a0282010100a7e577e064785ae73985bcafd2f55b28130e54dd49face62f592fd9e987b9fe5f87ac16ce1ea4025c1712782a73ee407659d28c87514e1387bd2659880ac1deac12dd5a47a653efcf0dbebdce1f04d1c2e80dfc6e905ea854f5e3e9649385d486ec7f5a5c5cd1afa4b75c60aebe097e4300d32e22d85be38d17a0d7c2ddd3410b4a45e263389664e3a659d01bcc4ad2b205a781e83299b26888a3cf6ab28d77543648347db0d4abd7834ca00e2ce836d6ef8003c5ca6c27b08352d6495f42572f9f4c744499bd6d84172b7e463590202f733b2eaae57748cc7c851a5afb8708bca2bf2b51bb3ab5749ec4f2b9a27630c493f1bedbba5fd777cd67bbd1eccf8890203010001a382019d3082019930120603551d130101ff040830060101ff02010130500603551d2004493047304506092a864886f72f0102013038303606082b06010505070201162a68747470733a2f2f7777772e61646f62652e636f6d2f6d6973632f706b692f6364735f63702e68746d6c30140603551d25040d300b06092a864886f72f0101053081b20603551d1f0481aa3081a73022a020a01e861c687474703a2f2f63726c2e61646f62652e636f6d2f6364732e63726c308180a07ea07ca47a3078310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f74204341310d300b0603550403130443524c31300b0603551d0f040403020106301f0603551d2304183016801482b7384a93aa9b10ef80bbd954e2f10ffb809cde301d0603551d0e04160414ab8059c365836d1d7d13bd19c3ec1a8f0d476aa3301906092a864886f67d074100040c300a1b0456362e3003020490300d06092a864886f70d010105050003820101003f39592ea2008eb15e11612cd2e0b6f58c898645ce13d99fdd849a146b084c5f869758f4a5a8d38f21f8477e50af77e5a16daadc77cdcd3799f74267af50e1544443eb310c809a42b49a95ba4d2c8b1b972478214bf2a270a7f86172493f1f5ea6e211175b220519f39e717dd2cbe63893170297348c69d89f79d07e4e246928529c369869e6110c929cb225a197a4b9cdfc12ec683c8437b272484c40a2969e51b5090af0952162b0a09dba8a1dc11b5681fb7c931d778b228adec6da966eaf9d573ee67b9f4b8bc8f93c0c5e857adcda98692ae869f0569349cbe77cc65cf132f80ebd421cc456224a9f65a0aade60b7efa330c34899b44e6c8aa07e017556308205633082044ba00302010202020362300d06092a864886f70d01010505003045310b300906035504061302555331163014060355040a130d47656f547275737420496e632e311e301c0603550403131547656f547275737420434120666f722041646f6265301e170d3038303132333137333832305a170d3039303230353137333832305a3081893121301f06092a864886f70d0109011612706b69737570706f72744067706f2e676f76310b30090603550406130255533131302f060355040a1328556e697465642053746174657320476f7665726e6d656e74205072696e74696e67204f6666696365312430220603550403131b5375706572696e74656e64656e74206f6620446f63756d656e747330820122300d06092a864886f70d01010105000382010f003082010a0282010100b7e0b5ddfa0be181b52c8225ec8a65b82ec27c142c5197a9cd6ef63773ddf107d79cf2493db87b7a77fed6aba3d2fc4907f452abfc5b38effb9527c9fc4135f27b4ae1505073e1dd1cdb87acfe51a851664423057135cb50942c33d7220f4529eb21e40b75fe741c18f9cd897002bbfc83739129146877c9a52cd0b111a658ceb38775c0c94f98235c06d85f79b6b3add232efeae988ff57b20be0e4bc46689e8f8a18d020d1bf1975a02e03c82c1d76dc67e8b4f0cb1ee3c0c452f4e3ca0baa9d4529688e2cb3a15d2bb67dc0bbf22fb1e74a1e523420c2b0c3a50997fb3ee1f6b3ac30c022843aba78f83d2315f513d12692fce6f8a6fdc86b9e914725e6fd0203010001a382021630820212300e0603551d0f0101ff0404030205e03081e50603551d200101ff0481da3081d73081d406092a864886f72f0102013081c630819006082b060105050702023081831a81805468697320636572746966696361746520686173206265656e2069737375656420696e206163636f7264616e6365207769746820746865204163726f6261742043726564656e7469616c7320435053206c6f636174656420617420687474703a2f2f7777772e67656f74727573742e636f6d2f7265736f75726365732f637073303106082b060105050702011625687474703a2f2f7777772e67656f74727573742e636f6d2f7265736f75726365732f637073303a0603551d1f04333031302fa02da02b8629687474703a2f2f63726c2e67656f74727573742e636f6d2f63726c732f61646f62656361312e63726c301f0603551d23041830168014ab8059c365836d1d7d13bd19c3ec1a8f0d476aa3304406082b0601050507010104383036303406082b060105050730018628687474703a2f2f61646f62652d6f6373702e67656f74727573742e636f6d2f726573706f6e64657230140603551d25040d300b06092a864886f72f010105303c060a2a864886f72f01010901042e302c0201018627687474703a2f2f61646f62652d74696d657374616d702e67656f74727573742e636f6d2f7473613013060a2a864886f72f0101090204053003020101300c0603551d1304053003020100300d06092a864886f70d0101050500038201010009fca169c6200cc2cf85d865a49d20c0574add1be5dcd51d28ae0af1a04413bb447fa3cacc4e9e80bf953c92fa6bcba4b7e50381e0908c260a86ac85f70581ae5d589c59710107367bdcf38130193184fbe15bf17febea10615aa7efa5589693a68a6b23783b35c66d2e269cb61b946dc913759d5377b3f9cc5a5041258ec7ea201c678fe8bae9afab3fbf0f9b9b2948444be1a07edd365c60f3d0d69077a4f02cfad40e068311cb74663320076efa3aa9f0b145a6c20c980cec9295e6689b73dd3a60544618b8e4f6d3b976b2baebaaa0cf213494a49cbff9d1f2d1f8ab6f1381f46536050d28bddb6550e5d72d74221f70cec1f2c58c82a5fbcda0b11f5d203182182530821821020101304b3045310b300906035504061302555331163014060355040a130d47656f547275737420496e632e311e301c0603550403131547656f547275737420434120666f722041646f626502020362300906052b0e03021a0500a0820717301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3038313231343132343234385a302306092a864886f70d01090431160414dbd8830cdc3c1d4348cd570edfce745a7076c54b308206b606092a864886f72f010108318206a7308206a3a0820238308202343082023030820118020101300d06092a864886f70d01010505003069310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f74204341170d3038303932343137353030305a170d3039303932343030303030305a304a302302043e1cbdaa170d3034303131373031303930355a300c300a0603551d1504030a0104302302043e1cbda8170d3034303131373031333932395a300c300a0603551d1504030a0104a02f302d300a0603551d140403020112301f0603551d2304183016801482b7384a93aa9b10ef80bbd954e2f10ffb809cde300d06092a864886f70d01010505000382010100c19720f7c5a62781fcc9164db49d50695d261806da53d0930129767422b0d5231122f8ff27492ad4ecb2094ed7893a485e4a48e996e6959b3d296124b69540e69bd99d174286e0f6a83917efc428240f9f429b1ff3320b586135c0dd8d84052f46f1ddc7367581bae3df079c6b00e69136a26263abdbb5b5159279927720734bf07c1012f4d67116fd37e801b7dae376e20aa66f31078d1acef9824adb889d1d54e2dc30a442abcd02e66cb5096926f90a732ab49cb0405b1a549bfbd4c9e047e015d647b6831fd958bd1b871907f239eb17199bb1dc39ec38000aa6ba9889547cd78191423cb7358a6e844f4c67e6702e5095b17ef5901a9079cf3694fa11cea18204633082045f3082045b0a0100a08204543082045006092b0601050507300101048204413082043d3081b5a2160414bef04a7291f5dd978877cfb9bf3597dd90041011180f32303038313231333137353435315a30653063303b300906052b0e03021a0500041446f806e65891b8c18ce8d3e95780ff4d1f06f41a0414f77d34909e23a905b1c14a4cf8c6b89d957d1bb0020203628000180f32303038313231333137343331315aa011180f32303038313231343137343331315aa1233021301f06092b06010505073001020412041095bd43ba2f894858b625e384d09d3965300d06092a864886f70d0101050500038181004705e5e58d53d465d4d2b07b0ab3fcff5b7f93edc9ed4b3f8b48d407d6b24a78cfe5493eed0280d108cd64ae08d79fd5b6b25c6eb4668bc376a40515f70e6d1b1569c03c6799939ed299983175b74ed02ff825230198e5ccb05f4e7f32b5c6a2d662c2fd96426e0afae374f12627e3f87e1c79fb79538b37d3ea0822ce0102fca08202ee308202ea308202e6308201cea003020102020202a8300d06092a864886f70d01010505003045310b300906035504061302555331163014060355040a130d47656f547275737420496e632e311e301c0603550403131547656f547275737420434120666f722041646f6265301e170d3037303431333138303830345a170d3132303431313138303830345a3050310b300906035504061302555331153013060355040a0c0c47656f547275737420496e63312a302806035504030c2147656f5472757374204f43535020526573706f6e64657220666f722041646f626530819f300d06092a864886f70d010101050003818d0030818902818100e209cd621b70ed4e907955d93839c6847323f8e819d03077f77ac493a6b6abd92ad76f480367b4df22814df25f411f5a58a736423f7476c4aa3e3bd30c714642c6a09b6150ba19c0ddf6f551684fcde108766e9bc43b2743c9d6a8aab19d667310b65b769dc8f5b04b8b322aa1b668bf3b7ad0b34c8219ffba7befba1a5e552b0203010001a3593057301f0603551d23041830168014ab8059c365836d1d7d13bd19c3ec1a8f0d476aa3300e0603551d0f0101ff04040302078030130603551d25040c300a06082b06010505070309300f06092b060105050730010504020500300d06092a864886f70d010105050003820101004d93423d5c6322f7bbaee09a95b7f43bdc7bcbcb172c7a49bc612de0c0539a013e168cd2d3a9d1a5fbc9ce3371839b5944e1b46af2b8438030e5c23b493fafdd18140770be63b2f59e33277e2d656e9469201d77d72bd2505ef2418e9efbd25fd5e5014bbcd4c64c3aedf440a5f7c314bb05ea23443f478eb37c06973e67c8f62346ad8937cd03fbb8a751efc8e3e36ad4c395020222afd1b3a5cc8b96f1d3a806c9c84886b6f199d86b95e9571b563bb6e0658a9b45bbbcdc1a449aeb174e4ccc146bea30d9d81610e4df75fcde9af2a7c868670235fa348bea753af8e1d3715b9c45b73c0e290846953c46ba8e0f1912388e4f3ffc5c63e67b961136797d8a300d06092a864886f70d0101010500048201008c4a87c56a957779c9bad79b6af4a2864f0e55be460f77a8775a7fff909f6175babc38490e1bec09b1871c7b216a2f34c1c04908f7f150bb96b317e7ce1f193448f7dd8aa6cdaaab60d04d8b1bc5bf57b80c1ece7b3e2f420aed8a0c572da0dd48b50cd0764e9798a4576e68a8188baaa5a65ac4ba5cb61dc09f6bbf3e7e9b1e1220c55f15427bd40e9091518c688256bc7dbb45d61982db2b2f45e3816c8857158635eba280a8b9ee19fc810f70582862ece8d7644da6829326bf5b74a763e1d2e98e7b82e3576e3d7462522d10b618579f8a91baaf38632db778a371d0f32151da08223887c366efd449ecf4e6fb6e98f43e650b43c5e54dbdb9a532c3d15aa1820f9430820f90060b2a864886f70d010910020e31820f7f30820f7b06092a864886f70d010702a0820f6c30820f68020103310b300906052b0e03021a050030820103060b2a864886f70d0109100104a081f30481f03081ed020101060229023021300906052b0e03021a05000414614285c170372c0fa76c12a6c97a129018956300020302ea68180f32303038313231343132343231355a300302013c0101ff021004613effb49f32a01be0aa6f7d5557cca08190a4818d30818a310b3009060355040613025553311630140603550408130d4d6173736163687573657474733110300e060355040713074e65656468616d31153013060355040a130c47656f547275737420496e6331133011060355040b130a50726f64756374696f6e312530230603550403131c61646f62652d74696d657374616d702e67656f74727573742e636f6da0820cc93082035130820239a0030201020202008f300d06092a864886f70d01010505003045310b300906035504061302555331163014060355040a130d47656f547275737420496e632e311e301c0603550403131547656f547275737420434120666f722041646f6265301e170d3035303131303031323931305a170d3135303131353038303030305a30818a310b3009060355040613025553311630140603550408130d4d6173736163687573657474733110300e060355040713074e65656468616d31153013060355040a130c47656f547275737420496e6331133011060355040b130a50726f64756374696f6e312530230603550403131c61646f62652d74696d657374616d702e67656f74727573742e636f6d30819f300d06092a864886f70d010101050003818d0030818902818100d16f1268b4b14f590bae10384ab4e31a05425ea46c48bd6c5e6a131bab50d59aa9e4f3cfa3c3fe657cf2023f3419d681eb7132d200533b32bcf7a72c9029028ca94ca27a6cee1bfefb11250d7c135251f0053ddb47b83314f08b3c001567164eb72fec1d96fbc32252a4134f3e55328245d8e6b81565b4ca50e9070b150ce6010203010001a38188308185303a0603551d1f04333031302fa02da02b8629687474703a2f2f63726c2e67656f74727573742e636f6d2f63726c732f61646f62656361312e63726c301f0603551d23041830168014ab8059c365836d1d7d13bd19c3ec1a8f0d476aa3300e0603551d0f0101ff0404030206c030160603551d250101ff040c300a06082b06010505070308300d06092a864886f70d010105050003820101009a7c978ddb57f85efd35fd0a8204dde980b63100fdb34f487974ddf133f7ac19a2cdf33eedfde272081e0866a435f3d198850c2e86b454ce4ab77ed3d97d13ab3056bac7db2b1ec79d7e2fd6dfc6f0d249953e124a1487bfef1b8e3538954a02996da811c0b9bd7c0f47925f8c2ca88acd7a9e981c3b4e59053f8b2c35f27818a313aa969b02c37c4a4bc882f35519b2538204ee3be6ae06f7197c0352f777c905c64511db3c28779e3f610466eecf2b7b1a796a9c489edf0cc8c8c1b6113ff0bd35f9e0115728d7bb9432ef663fcc5ef77757b80c4d2556d2402ccca6a8e7a2dfad54e0a75557dd4050630990a9aa0053015ece287e8c4be35ba718268fc4ac308204a130820389a00302010202043e1cbd28300d06092a864886f70d01010505003069310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f74204341301e170d3033303130383233333732335a170d3233303130393030303732335a3069310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f7420434130820122300d06092a864886f70d01010105000382010f003082010a0282010100cc4f5484f7a7a2e733537f3f9c12886b2c9947677e0f1eb9ad1488f9c310d81df0f0d59f690a2f5935b0cc6ca94c9c15a09fce20bfa0cf54e2e02066453f3986387e9cc48e0722c624f60112b035df55ea6990b0db85371ee24e07b242a16a1369a066ea809111592a9b08795a20442dc9bd73388b3c2fe0431b5db30bf0af351a29feefa692dd814c9d3d598ead313c407e9b913606fce25c8dd18d26d55c45cfaf653fb1aad26296f4a838eaba6042f4f41c4a3515cef84e22560f9518c5f8969f9ffbb0b77825e9806bbdd60af0c674949df30f50db9a77ce4b7083238da0ca7820445c3c5464f1eaa230199fea4c064d06784b5e92df22d2c967b37ad2010203010001a382014f3082014b301106096086480186f842010104040302000730818e0603551d1f048186308183308180a07ea07ca47a3078310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f74204341310d300b0603550403130443524c31302b0603551d1004243022800f32303033303130383233333732335a810f32303233303130393030303732335a300b0603551d0f040403020106301f0603551d2304183016801482b7384a93aa9b10ef80bbd954e2f10ffb809cde301d0603551d0e0416041482b7384a93aa9b10ef80bbd954e2f10ffb809cde300c0603551d13040530030101ff301d06092a864886f67d0741000410300e1b0856362e303a342e3003020490300d06092a864886f70d0101050500038201010032da9f4375c1fa6fc96fdbab1d36373ebc611936b7023c1d2359986c9eee4d85e754c8201fa7d4bbe2bf00777d246b702f5cc13a7649b5d3e023842a716a22f3c127299815f63590e4044cc38dbc9f611ce7fd248cd144438c16ba9b4da5d4352fbc11cebdf751378d9f90e414f1183fbee9591235f93392f39ee0d56b9a719b994bc871c3e1b16109c4e5fa91f0423a377d34f972e8cdaa621c21e9d5f48210e37b05b62d68560b7e7e922c6f4d72820ced5674b29db9ab2d2b1d105fdb2775708ffd1dd7e202a079e51ce5ffaf6440512d9e9b47db42a57c1fc2a648b0d7be92694da4f62957c5781118dc8751ca13b2629d4f2b32bd31a5c1fa52ab0588c8308204cb308203b3a00302010202043e1cbdb5300d06092a864886f70d01010505003069310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f74204341301e170d3034303131373030303333395a170d3135303131353038303030305a3045310b300906035504061302555331163014060355040a130d47656f547275737420496e632e311e301c0603550403131547656f547275737420434120666f722041646f626530820122300d06092a864886f70d01010105000382010f003082010a0282010100a7e577e064785ae73985bcafd2f55b28130e54dd49face62f592fd9e987b9fe5f87ac16ce1ea4025c1712782a73ee407659d28c87514e1387bd2659880ac1deac12dd5a47a653efcf0dbebdce1f04d1c2e80dfc6e905ea854f5e3e9649385d486ec7f5a5c5cd1afa4b75c60aebe097e4300d32e22d85be38d17a0d7c2ddd3410b4a45e263389664e3a659d01bcc4ad2b205a781e83299b26888a3cf6ab28d77543648347db0d4abd7834ca00e2ce836d6ef8003c5ca6c27b08352d6495f42572f9f4c744499bd6d84172b7e463590202f733b2eaae57748cc7c851a5afb8708bca2bf2b51bb3ab5749ec4f2b9a27630c493f1bedbba5fd777cd67bbd1eccf8890203010001a382019d3082019930120603551d130101ff040830060101ff02010130500603551d2004493047304506092a864886f72f0102013038303606082b06010505070201162a68747470733a2f2f7777772e61646f62652e636f6d2f6d6973632f706b692f6364735f63702e68746d6c30140603551d25040d300b06092a864886f72f0101053081b20603551d1f0481aa3081a73022a020a01e861c687474703a2f2f63726c2e61646f62652e636f6d2f6364732e63726c308180a07ea07ca47a3078310b300906035504061302555331233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311d301b060355040b131441646f6265205472757374205365727669636573311630140603550403130d41646f626520526f6f74204341310d300b0603550403130443524c31300b0603551d0f040403020106301f0603551d2304183016801482b7384a93aa9b10ef80bbd954e2f10ffb809cde301d0603551d0e04160414ab8059c365836d1d7d13bd19c3ec1a8f0d476aa3301906092a864886f67d074100040c300a1b0456362e3003020490300d06092a864886f70d010105050003820101003f39592ea2008eb15e11612cd2e0b6f58c898645ce13d99fdd849a146b084c5f869758f4a5a8d38f21f8477e50af77e5a16daadc77cdcd3799f74267af50e1544443eb310c809a42b49a95ba4d2c8b1b972478214bf2a270a7f86172493f1f5ea6e211175b220519f39e717dd2cbe63893170297348c69d89f79d07e4e246928529c369869e6110c929cb225a197a4b9cdfc12ec683c8437b272484c40a2969e51b5090af0952162b0a09dba8a1dc11b5681fb7c931d778b228adec6da966eaf9d573ee67b9f4b8bc8f93c0c5e857adcda98692ae869f0569349cbe77cc65cf132f80ebd421cc456224a9f65a0aade60b7efa330c34899b44e6c8aa07e017556318201803082017c020101304b3045310b300906035504061302555331163014060355040a130d47656f547275737420496e632e311e301c0603550403131547656f547275737420434120666f722041646f62650202008f300906052b0e03021a0500a0818c301a06092a864886f70d010903310d060b2a864886f70d0109100104301c06092a864886f70d010905310f170d3038313231343132343231355a302306092a864886f70d01090431160414b3a065f2bc93eda0a354f9df4bad44381e6d9e4d302b060b2a864886f70d010910020c311c301a301830160414d5d1f87994cd860c5d892001adab7acec3ddb61d300d06092a864886f70d0101010500048180b3adfd55b774589125eee5efaea10811f9b1502c0008066733ffd5beeb47a2a44f344bffcfaafce06c1ebb2dc22623f788ebc84267a4cfe985ba07443f77a134ce1df14a54102e6063926b29f2ec7524749431b0332eab86c4fc70519bcb4d993326778e62884fc2fc438845b3af9dc2a589ea3dfbd37d38ae3f78f7f18d665300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/fontbox/pom.xml b/fontbox/pom.xml index 6c94bf9..dc1410f 100644 --- a/fontbox/pom.xml +++ b/fontbox/pom.xml @@ -21,7 +21,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>../parent/pom.xml</relativePath> </parent> diff --git a/fontbox/src/main/java/org/apache/fontbox/cmap/CMapParser.java b/fontbox/src/main/java/org/apache/fontbox/cmap/CMapParser.java index abe4b86..86a59ab 100644 --- a/fontbox/src/main/java/org/apache/fontbox/cmap/CMapParser.java +++ b/fontbox/src/main/java/org/apache/fontbox/cmap/CMapParser.java @@ -16,6 +16,7 @@ */ package org.apache.fontbox.cmap; +import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -121,27 +122,27 @@ public class CMapParser if (previousToken != null) { - if (op.op.equals("usecmap")) + if (op.op.equals("usecmap") && previousToken instanceof LiteralName) { parseUsecmap((LiteralName) previousToken, result); } - else if (op.op.equals("begincodespacerange")) + else if (op.op.equals("begincodespacerange") && previousToken instanceof Number) { parseBegincodespacerange((Number) previousToken, cmapStream, result); } - else if (op.op.equals("beginbfchar")) + else if (op.op.equals("beginbfchar") && previousToken instanceof Number) { parseBeginbfchar((Number) previousToken, cmapStream, result); } - else if (op.op.equals("beginbfrange")) + else if (op.op.equals("beginbfrange") && previousToken instanceof Number) { parseBeginbfrange((Number) previousToken, cmapStream, result); } - else if (op.op.equals("begincidchar")) + else if (op.op.equals("begincidchar") && previousToken instanceof Number) { parseBegincidchar((Number) previousToken, cmapStream, result); } - else if (op.op.equals("begincidrange")) + else if (op.op.equals("begincidrange") && previousToken instanceof Integer) { parseBegincidrange((Integer) previousToken, cmapStream, result); } @@ -446,12 +447,7 @@ public class CMapParser */ protected InputStream getExternalCMap(String name) throws IOException { - InputStream is = getClass().getResourceAsStream(name); - if (is == null) - { - throw new IOException("Error: Could not find referenced cmap stream " + name); - } - return is; + return new BufferedInputStream(getClass().getResourceAsStream(name)); } private Object parseNextToken(PushbackInputStream is) throws IOException diff --git a/fontbox/src/main/java/org/apache/fontbox/cmap/CodespaceRange.java b/fontbox/src/main/java/org/apache/fontbox/cmap/CodespaceRange.java index c0bd686..b092984 100644 --- a/fontbox/src/main/java/org/apache/fontbox/cmap/CodespaceRange.java +++ b/fontbox/src/main/java/org/apache/fontbox/cmap/CodespaceRange.java @@ -37,19 +37,26 @@ public class CodespaceRange * <8140> to <9FFC> defines a rectangular range. The high byte has to be within 0x81 and 0x9F and the * low byte has to be within 0x40 and 0xFC * + * @param startBytes + * @param endBytes */ public CodespaceRange(byte[] startBytes, byte[] endBytes) { - if (startBytes.length != endBytes.length) + byte[] correctedStartBytes = startBytes; + if (startBytes.length != endBytes.length && startBytes.length == 1 && startBytes[0] == 0) + { + correctedStartBytes = new byte[endBytes.length]; + } + else if (startBytes.length != endBytes.length) { throw new IllegalArgumentException( "The start and the end values must not have different lengths."); } - start = new int[startBytes.length]; + start = new int[correctedStartBytes.length]; end = new int[endBytes.length]; - for (int i = 0; i < startBytes.length; i++) + for (int i = 0; i < correctedStartBytes.length; i++) { - start[i] = startBytes[i] & 0xFF; + start[i] = correctedStartBytes[i] & 0xFF; end[i] = endBytes[i] & 0xFF; } codeLength = endBytes.length; diff --git a/fontbox/src/main/java/org/apache/fontbox/pfb/PfbParser.java b/fontbox/src/main/java/org/apache/fontbox/pfb/PfbParser.java index 9774e8e..e4c0eca 100644 --- a/fontbox/src/main/java/org/apache/fontbox/pfb/PfbParser.java +++ b/fontbox/src/main/java/org/apache/fontbox/pfb/PfbParser.java @@ -140,11 +140,21 @@ public class PfbParser size += in.read() << 8; size += in.read() << 16; size += in.read() << 24; + if (size < 0) + { + throw new IOException("PFB record size is negative: " + size); + } lengths[records] = size; if (pointer >= pfbdata.length) { throw new EOFException("attempted to read past EOF"); } + if (size > pfbdata.length - pointer) + { + throw new IOException("PFB record size (" + size + + ") doesn't fit in buffer, position: " + pointer + + ", total length: " + pfbdata.length); + } int got = in.read(pfbdata, pointer, size); if (got < 0) { diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/BufferedRandomAccessFile.java b/fontbox/src/main/java/org/apache/fontbox/ttf/BufferedRandomAccessFile.java index 84b914a..bc2473d 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/BufferedRandomAccessFile.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/BufferedRandomAccessFile.java @@ -148,24 +148,37 @@ public class BufferedRandomAccessFile extends RandomAccessFile @Override public int read(byte[] b, int off, int len) throws IOException { - int leftover = bufend - bufpos; - if (len <= leftover) - { - System.arraycopy(buffer, bufpos, b, off, len); - bufpos += len; - return len; - } - System.arraycopy(buffer, bufpos, b, off, leftover); - bufpos += leftover; - if (fillBuffer() > 0) + int curLen = len; // length of what is left to read (shrinks) + int curOff = off; // offset where to put read data (grows) + int totalRead = 0; + + while (true) { - int bytesRead = read(b, off + leftover, len - leftover); - if (bytesRead > 0) + int leftover = bufend - bufpos; + if (curLen <= leftover) + { + System.arraycopy(buffer, bufpos, b, curOff, curLen); + bufpos += curLen; + return totalRead + curLen; + } + // curLen > leftover, we need to read more than what remains in buffer + System.arraycopy(buffer, bufpos, b, curOff, leftover); + totalRead += leftover; + bufpos += leftover; + if (fillBuffer() > 0) + { + curOff += leftover; + curLen -= leftover; + } + else { - leftover += bytesRead; + if (totalRead == 0) + { + return -1; + } + return totalRead; } } - return leftover > 0 ? leftover : -1; } /** diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphSubstitutionTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphSubstitutionTable.java index e5ce6c0..607723e 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphSubstitutionTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphSubstitutionTable.java @@ -405,7 +405,7 @@ public class GlyphSubstitutionTable extends TTFTable * * @param langSysTables The {@code LangSysTable}s indicating {@code FeatureRecord}s to search * for - * @param enabledFeatures An optional whitelist of feature tags ({@code null} to allow all) + * @param enabledFeatures An optional list of feature tags ({@code null} to allow all) * @return The indicated {@code FeatureRecord}s */ private List<FeatureRecord> getFeatureRecords(Collection<LangSysTable> langSysTables, @@ -513,8 +513,8 @@ public class GlyphSubstitutionTable extends TTFTable /** * Apply glyph substitutions to the supplied gid. The applicable substitutions are determined by - * the {@code scriptTags} which indicate the language of the gid, and by the - * {@code enabledFeatures} which acts as a whitelist. + * the {@code scriptTags} which indicate the language of the gid, and by the list of + * {@code enabledFeatures}. * * To ensure that a single gid isn't mapped to multiple substitutions, subsequent invocations * with the same gid will return the same result as the first, regardless of script or enabled @@ -522,7 +522,7 @@ public class GlyphSubstitutionTable extends TTFTable * * @param gid GID * @param scriptTags Script tags applicable to the gid (see {@link OpenTypeScript}) - * @param enabledFeatures Whitelist of features to apply + * @param enabledFeatures list of features to apply */ public int getSubstitution(int gid, String[] scriptTags, List<String> enabledFeatures) { diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/OS2WindowsMetricsTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/OS2WindowsMetricsTable.java index cccc0cb..6cc465c 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/OS2WindowsMetricsTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/OS2WindowsMetricsTable.java @@ -152,7 +152,7 @@ public class OS2WindowsMetricsTable extends TTFTable * <p>For Restricted License embedding to take effect, it must be the only level of embedding * selected. */ - public static final short FSTYPE_RESTRICTED = 0x0001; + public static final short FSTYPE_RESTRICTED = 0x0002; /** * Preview and Print embedding: the font may be embedded, and temporarily loaded on the diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/OpenTypeScript.java b/fontbox/src/main/java/org/apache/fontbox/ttf/OpenTypeScript.java index 68726c9..8eb781a 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/OpenTypeScript.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/OpenTypeScript.java @@ -16,6 +16,7 @@ */ package org.apache.fontbox.ttf; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -221,15 +222,8 @@ public final class OpenTypeScript InputStream input = null; try { - input = OpenTypeScript.class.getResourceAsStream(path); - if (input != null) - { - parseScriptsFile(input); - } - else - { - LOG.warn("Could not find '" + path + "', mirroring char map will be empty: "); - } + input = new BufferedInputStream(OpenTypeScript.class.getResourceAsStream(path)); + parseScriptsFile(input); } catch (IOException e) { diff --git a/fontbox/src/test/java/org/apache/fontbox/cmap/TestCodespaceRange.java b/fontbox/src/test/java/org/apache/fontbox/cmap/TestCodespaceRange.java index 8b60cfa..ffba1a8 100644 --- a/fontbox/src/test/java/org/apache/fontbox/cmap/TestCodespaceRange.java +++ b/fontbox/src/test/java/org/apache/fontbox/cmap/TestCodespaceRange.java @@ -46,11 +46,17 @@ public class TestCodespaceRange extends TestCase */ public void testConstructor() { + // PDFBOX-4923 "1 begincodespacerange <00> <ffff> endcodespacerange" case is accepted byte[] startBytes1 = new byte[] { 0x00 }; - byte[] endBytes2 = new byte[] { 0x01, 0x20 }; + byte[] endBytes2 = new byte[] { -1, -1 }; + new CodespaceRange(startBytes1, endBytes2); + + // other cases of different lengths are not + byte[] startBytes3 = new byte[] { 0x01 }; + byte[] endBytes4 = new byte[] { 0x01, 0x20 }; try { - new CodespaceRange(startBytes1, endBytes2); + new CodespaceRange(startBytes3, endBytes4); fail("The constructor should have thrown an IllegalArgumentException exception."); } catch (IllegalArgumentException exception) diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/BufferedRandomAccessFileTest.java b/fontbox/src/test/java/org/apache/fontbox/ttf/BufferedRandomAccessFileTest.java index 2c2b5b6..f620db4 100644 --- a/fontbox/src/test/java/org/apache/fontbox/ttf/BufferedRandomAccessFileTest.java +++ b/fontbox/src/test/java/org/apache/fontbox/ttf/BufferedRandomAccessFileTest.java @@ -26,6 +26,7 @@ import org.junit.Test; /** * @author Cameron Rollhieser + * @author Tilman Hausherr */ public class BufferedRandomAccessFileTest { @@ -47,14 +48,108 @@ public class BufferedRandomAccessFileTest outputStream.close(); final byte[] readBuffer = new byte[2]; - final BufferedRandomAccessFile buffer = new BufferedRandomAccessFile(file, "r", 4); + final BufferedRandomAccessFile braf = new BufferedRandomAccessFile(file, "r", 4); int amountRead; int totalAmountRead = 0; - while ((amountRead = buffer.read(readBuffer, 0, 2)) != -1) + while ((amountRead = braf.read(readBuffer, 0, 2)) != -1) { totalAmountRead += amountRead; } Assert.assertEquals(10, totalAmountRead); + braf.close(); + file.delete(); + } + + /** + * Test several reading patterns, both reading within a buffer and across buffer. + * + * @throws IOException + */ + @Test + public void testReadBuffer() throws IOException + { + final File file = File.createTempFile("apache-pdfbox", ".dat"); + + OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file)); + final String content = "012345678A012345678B012345678C012345678D"; + outputStream.write(content.getBytes("UTF-8")); + outputStream.flush(); + outputStream.close(); + + final byte[] readBuffer = new byte[40]; + final BufferedRandomAccessFile braf = new BufferedRandomAccessFile(file, "r", 10); + + int count = 4; + int bytesRead = braf.read(readBuffer, 0, count); + Assert.assertEquals(4, braf.getFilePointer()); + Assert.assertEquals(count, bytesRead); + Assert.assertEquals("0123", new String(readBuffer, 0, count)); + + count = 6; + bytesRead = braf.read(readBuffer, 0, count); + Assert.assertEquals(10, braf.getFilePointer()); + Assert.assertEquals(count, bytesRead); + Assert.assertEquals("45678A", new String(readBuffer, 0, count)); + + count = 10; + bytesRead = braf.read(readBuffer, 0, count); + Assert.assertEquals(20, braf.getFilePointer()); + Assert.assertEquals(count, bytesRead); + Assert.assertEquals("012345678B", new String(readBuffer, 0, count)); + + count = 10; + bytesRead = braf.read(readBuffer, 0, count); + Assert.assertEquals(30, braf.getFilePointer()); + Assert.assertEquals(count, bytesRead); + Assert.assertEquals("012345678C", new String(readBuffer, 0, count)); + + count = 10; + bytesRead = braf.read(readBuffer, 0, count); + Assert.assertEquals(40, braf.getFilePointer()); + Assert.assertEquals(count, bytesRead); + Assert.assertEquals("012345678D", new String(readBuffer, 0, count)); + + Assert.assertEquals(-1, braf.read()); + + braf.seek(0); + braf.read(readBuffer, 0, 7); + Assert.assertEquals(7, braf.getFilePointer()); + + count = 16; + bytesRead = braf.read(readBuffer, 0, count); + Assert.assertEquals(23, braf.getFilePointer()); + Assert.assertEquals(count, bytesRead); + Assert.assertEquals("78A012345678B012", new String(readBuffer, 0, count)); + + bytesRead = braf.read(readBuffer, 0, 99); + Assert.assertEquals(40, braf.getFilePointer()); + Assert.assertEquals(17, bytesRead); + Assert.assertEquals("345678C012345678D", new String(readBuffer, 0, 17)); + + Assert.assertEquals(-1, braf.read()); + + braf.seek(0); + braf.read(readBuffer, 0, 7); + Assert.assertEquals(7, braf.getFilePointer()); + + count = 23; + bytesRead = braf.read(readBuffer, 0, count); + Assert.assertEquals(30, braf.getFilePointer()); + Assert.assertEquals(count, bytesRead); + Assert.assertEquals("78A012345678B012345678C", new String(readBuffer, 0, count)); + + braf.seek(0); + braf.read(readBuffer, 0, 10); + Assert.assertEquals(10, braf.getFilePointer()); + count = 23; + bytesRead = braf.read(readBuffer, 0, count); + Assert.assertEquals(33, braf.getFilePointer()); + Assert.assertEquals(count, bytesRead); + Assert.assertEquals("012345678B012345678C012", new String(readBuffer, 0, count)); + + braf.close(); + + file.delete(); } } diff --git a/parent/pom.xml b/parent/pom.xml index 161c5c1..06dc486 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -33,7 +33,7 @@ <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <packaging>pom</packaging> <name>PDFBox parent</name> @@ -236,12 +236,10 @@ <executable>${jdk.path}/bin/javac</executable> <fork>true</fork> <!-- enable these when getting CompilationFailureException without explanation: --> - <compilerArgs> - <arg>-verbose</arg> - <arg>-J-Xmx1g</arg> - <arg>-J-XX:PermSize=256m</arg> - <arg>-J-XX:MaxPermSize=512m</arg> - </compilerArgs> + <!-- + <compilerArgument>-verbose</compilerArgument> + <verbose>true</verbose> + --> </configuration> </plugin> <plugin> @@ -536,8 +534,8 @@ </developers> <scm> - <connection>scm:svn:http://svn.apache.org/repos/asf/maven/pom/tags/2.0.20/pdfbox-parent</connection> - <developerConnection>scm:svn:https://svn.apache.org/repos/asf/maven/pom/tags/2.0.20/pdfbox-parent</developerConnection> - <url>http://svn.apache.org/viewvc/maven/pom/tags/2.0.20/pdfbox-parent</url> + <connection>scm:svn:http://svn.apache.org/repos/asf/maven/pom/tags/2.0.21/pdfbox-parent</connection> + <developerConnection>scm:svn:https://svn.apache.org/repos/asf/maven/pom/tags/2.0.21/pdfbox-parent</developerConnection> + <url>http://svn.apache.org/viewvc/maven/pom/tags/2.0.21/pdfbox-parent</url> </scm> </project> diff --git a/pdfbox/pom.xml b/pdfbox/pom.xml index 523bc49..c24270f 100644 --- a/pdfbox/pom.xml +++ b/pdfbox/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>../parent/pom.xml</relativePath> </parent> diff --git a/pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java b/pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java index 741e4b8..fad8a89 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java @@ -405,8 +405,13 @@ public abstract class PDFStreamEngine // clip to bounding box clipToRect(tilingPattern.getBBox()); + // save text matrices (pattern stream may contain BT/ET, see PDFBOX-4896) + Matrix textMatrixSave = textMatrix; + Matrix textLineMatrixSave = textLineMatrix; processStreamOperators(tilingPattern); - + textMatrix = textMatrixSave; + textLineMatrix = textLineMatrixSave; + initialMatrix = parentMatrix; restoreGraphicsStack(savedStack); popResources(parent); @@ -735,13 +740,7 @@ public abstract class PDFStreamEngine Vector w = font.getDisplacement(code); // process the decoded glyph - saveGraphicsState(); - Matrix textMatrixOld = textMatrix; - Matrix textLineMatrixOld = textLineMatrix; showGlyph(textRenderingMatrix, font, code, w); - textMatrix = textMatrixOld; - textLineMatrix = textLineMatrixOld; - restoreGraphicsState(); // calculate the combined displacements float tx; @@ -802,8 +801,8 @@ public abstract class PDFStreamEngine protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vector displacement) throws IOException { - // call deprecated method to ensure binary compatibility - showGlyph(textRenderingMatrix, font, code, null, displacement); + // call deprecated method to ensure binary compatibility if not overridden + showGlyph(textRenderingMatrix, font, code, font.toUnicode(code), displacement); } /** @@ -840,7 +839,7 @@ public abstract class PDFStreamEngine { // overridden in subclasses // call deprecated method to ensure binary compatibility if not overridden - showFontGlyph(textRenderingMatrix, font, code, null, displacement); + showFontGlyph(textRenderingMatrix, font, code, font.toUnicode(code), displacement); } /** @@ -853,6 +852,8 @@ public abstract class PDFStreamEngine * @param unicode the Unicode text for this glyph, or null if the PDF does provide it * @param displacement the displacement (i.e. advance) of the glyph in text space * @throws IOException if the glyph cannot be processed + * + * @deprecated use {@link #showType3Glyph(Matrix, PDType3Font, int, Vector)} instead */ protected void showType3Glyph(Matrix textRenderingMatrix, PDType3Font font, int code, String unicode, Vector displacement) throws IOException @@ -878,7 +879,7 @@ public abstract class PDFStreamEngine Vector displacement) throws IOException { // call deprecated method to ensure binary compatibility if not overridden - showType3Glyph(textRenderingMatrix, font, code, null, displacement); + showType3Glyph(textRenderingMatrix, font, code, font.toUnicode(code), displacement); } /** diff --git a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSFloat.java b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSFloat.java index f276431..1c774ea 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSFloat.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSFloat.java @@ -107,13 +107,13 @@ public class COSFloat extends COSNumber // check for very small values else if (floatValue == 0 && doubleValue != 0 && Math.abs(doubleValue) < Float.MIN_NORMAL) { - floatValue = Float.MIN_NORMAL; - floatValue *= doubleValue >= 0 ? 1 : -1; + // values smaller than the smallest possible float value are converted to 0 + // see PDF spec, chapter 2 of Appendix C Implementation Limits valueReplaced = true; } if (valueReplaced) { - value = new BigDecimal(floatValue); + value = BigDecimal.valueOf(floatValue); valueAsString = removeNullDigits(value.toPlainString()); } } @@ -146,6 +146,8 @@ public class COSFloat extends COSNumber * The value of the double object that this one wraps. * * @return The double of this object. + * + * @deprecated will be removed in a future release */ @Override public double doubleValue() diff --git a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSInteger.java b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSInteger.java index c10b0aa..bb4e2d8 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSInteger.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSInteger.java @@ -143,6 +143,8 @@ public final class COSInteger extends COSNumber * polymorphic access to value as float. * * @return The double value of this object. + * + * @deprecated will be removed in a future release */ @Override public double doubleValue() diff --git a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSNumber.java b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSNumber.java index 0ad8d10..ac9ee09 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSNumber.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSNumber.java @@ -49,6 +49,8 @@ public abstract class COSNumber extends COSBase * This will get the double value of this number. * * @return The double value of this number. + * + * @deprecated will be removed in a future release */ public abstract double doubleValue(); @@ -77,12 +79,12 @@ public abstract class COSNumber extends COSBase */ public static COSNumber get( String number ) throws IOException { - if (number.length() == 1) + if (number.length() == 1) { char digit = number.charAt(0); if ('0' <= digit && digit <= '9') { - return COSInteger.get(digit - '0'); + return COSInteger.get((long) digit - '0'); } else if (digit == '-' || digit == '.') { @@ -94,25 +96,43 @@ public abstract class COSNumber extends COSBase throw new IOException("Not a number: " + number); } } - else if (number.indexOf('.') == -1 && (number.toLowerCase().indexOf('e') == -1)) + if (isFloat(number)) { - try + return new COSFloat(number); + } + try + { + if (number.charAt(0) == '+') { - if (number.charAt(0) == '+') - { - return COSInteger.get(Long.parseLong(number.substring(1))); - } - return COSInteger.get(Long.parseLong(number)); + // PDFBOX-2569: some numbers start with "+" + return COSInteger.get(Long.parseLong(number.substring(1))); } - catch( NumberFormatException e ) + return COSInteger.get(Long.parseLong(number)); + } + catch (NumberFormatException e) + { + // check if the given string could be a number at all + String numberString = number.startsWith("+") || number.startsWith("-") + ? number.substring(1) : number; + if (!numberString.matches("[0-9]*")) { - // might be a huge number, see PDFBOX-3116 - return new COSFloat(number); + throw new IOException("Not a number: " + number); } - } - else + return null; + } + } + + private static boolean isFloat( String number ) + { + int length = number.length(); + for (int i = 0; i < length; i++) { - return new COSFloat(number); + char digit = number.charAt(i); + if (digit == '.' || digit == 'e') + { + return true; + } } + return false; } } diff --git a/pdfbox/src/main/java/org/apache/pdfbox/filter/Predictor.java b/pdfbox/src/main/java/org/apache/pdfbox/filter/Predictor.java index 2726600..9aa627c 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/filter/Predictor.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/filter/Predictor.java @@ -17,13 +17,11 @@ package org.apache.pdfbox.filter; import java.io.FilterOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.io.IOUtils; /** * Helper class to contain predictor decoding used by Flate and LZW filter. @@ -202,53 +200,6 @@ public final class Predictor break; } } - - static void decodePredictor(int predictor, int colors, int bitsPerComponent, int columns, InputStream in, OutputStream out) - throws IOException - { - if (predictor == 1) - { - // no prediction - IOUtils.copy(in, out); - } - else - { - // calculate sizes - final int rowlength = calculateRowLength(colors, bitsPerComponent, columns); - byte[] actline = new byte[rowlength]; - byte[] lastline = new byte[rowlength]; - - int linepredictor = predictor; - - while (in.available() > 0) - { - // test for PNG predictor; each value >= 10 (not only 15) indicates usage of PNG predictor - if (predictor >= 10) - { - // PNG predictor; each row starts with predictor type (0, 1, 2, 3, 4) - // read per line predictor - linepredictor = in.read(); - if (linepredictor == -1) - { - return; - } - // add 10 to tread value 0 as 10, 1 as 11, ... - linepredictor += 10; - } - - // read line - int i, offset = 0; - while (offset < rowlength && ((i = in.read(actline, offset, rowlength - offset)) != -1)) - { - offset += i; - } - - 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) { diff --git a/pdfbox/src/main/java/org/apache/pdfbox/multipdf/Overlay.java b/pdfbox/src/main/java/org/apache/pdfbox/multipdf/Overlay.java index a86e2a0..77deb57 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/multipdf/Overlay.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/multipdf/Overlay.java @@ -127,7 +127,8 @@ public class Overlay implements Closeable } /** - * This will add overlays documents to a document. + * This will add overlays documents to a document. If you created the overlay documents with + * subsetted fonts, you need to save them first so that the subsetting gets done. * * @param specificPageOverlayDocuments Optional map of overlay documents for specific pages. The * page numbers are 1-based. The map must be empty (but not null) if no specific mappings are @@ -632,7 +633,8 @@ public class Overlay implements Closeable } /** - * Sets the default overlay PDF. + * Sets the default overlay PDF. If you created the overlay document with + * subsetted fonts, you need to save it first so that the subsetting gets done. * * @param defaultOverlayPDF the default overlay PDF */ @@ -662,7 +664,8 @@ public class Overlay implements Closeable } /** - * Sets the first page overlay PDF. + * Sets the first page overlay PDF. If you created the overlay document with + * subsetted fonts, you need to save it first so that the subsetting gets done. * * @param firstPageOverlayPDF the first page overlay PDF */ @@ -682,7 +685,8 @@ public class Overlay implements Closeable } /** - * Sets the last page overlay PDF. + * Sets the last page overlay PDF. If you created the overlay document with + * subsetted fonts, you need to save it first so that the subsetting gets done. * * @param lastPageOverlayPDF the last page overlay PDF */ @@ -702,7 +706,8 @@ public class Overlay implements Closeable } /** - * Sets the all pages overlay PDF. + * Sets the all pages overlay PDF. If you created the overlay document with + * subsetted fonts, you need to save it first so that the subsetting gets done. * * @param allPagesOverlayPDF the all pages overlay PDF. This should not be a PDDocument that you * created on the fly, it should be saved first, if it contains any fonts that are subset. @@ -723,7 +728,8 @@ public class Overlay implements Closeable } /** - * Sets the odd page overlay PDF. + * Sets the odd page overlay PDF. If you created the overlay document with + * subsetted fonts, you need to save it first so that the subsetting gets done. * * @param oddPageOverlayPDF the odd page overlay PDF */ @@ -743,7 +749,8 @@ public class Overlay implements Closeable } /** - * Sets the even page overlay PDF. + * Sets the even page overlay PDF. If you created the overlay document with + * subsetted fonts, you need to save it first so that the subsetting gets done. * * @param evenPageOverlayPDF the even page overlay PDF */ diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/BaseParser.java b/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/BaseParser.java index b4209ec..10379f7 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/BaseParser.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/BaseParser.java @@ -485,7 +485,6 @@ public abstract class BaseParser case '5': case '6': case '7': - { StringBuilder octal = new StringBuilder(); octal.append( next ); c = seqSource.read(); @@ -520,13 +519,10 @@ public abstract class BaseParser } out.write(character); break; - } default: - { // dropping the backslash // see 7.3.4.2 Literal Strings for further information out.write(next); - } } } else @@ -847,15 +843,11 @@ public abstract class BaseParser */ protected COSBase parseDirObject() throws IOException { - COSBase retval = null; - skipSpaces(); - int nextByte = seqSource.peek(); - char c = (char)nextByte; + char c = (char)seqSource.peek(); switch(c) { case '<': - { // pull off first left bracket int leftBracket = seqSource.read(); // check for second left bracket @@ -864,92 +856,57 @@ public abstract class BaseParser if(c == '<') { - retval = parseCOSDictionary(); + COSDictionary retval = parseCOSDictionary(); skipSpaces(); + return retval; } else { - retval = parseCOSString(); + return parseCOSString(); } - break; - } case '[': - { // array - retval = parseCOSArray(); - break; - } + return parseCOSArray(); case '(': - retval = parseCOSString(); - break; + return parseCOSString(); case '/': // name - retval = parseCOSName(); - break; + return parseCOSName(); case 'n': - { // null readExpectedString(NULL); - retval = COSNull.NULL; - break; - } + return COSNull.NULL; case 't': - { String trueString = new String( seqSource.readFully(4), ISO_8859_1 ); if( trueString.equals( TRUE ) ) { - retval = COSBoolean.TRUE; + return COSBoolean.TRUE; } else { throw new IOException( "expected true actual='" + trueString + "' " + seqSource + "' at offset " + seqSource.getPosition()); } - break; - } case 'f': - { String falseString = new String( seqSource.readFully(5), ISO_8859_1 ); if( falseString.equals( FALSE ) ) { - retval = COSBoolean.FALSE; + return COSBoolean.FALSE; } else { throw new IOException( "expected false actual='" + falseString + "' " + seqSource + "' at offset " + seqSource.getPosition()); } - break; - } case 'R': seqSource.read(); - retval = new COSObject(null); - break; + return new COSObject(null); case (char)-1: return null; default: - { if( Character.isDigit(c) || c == '-' || c == '+' || c == '.') { - StringBuilder buf = new StringBuilder(); - int ic = seqSource.read(); - c = (char)ic; - while( Character.isDigit( c )|| - c == '-' || - c == '+' || - c == '.' || - c == 'E' || - c == 'e' ) - { - buf.append( c ); - ic = seqSource.read(); - c = (char)ic; - } - if( ic != -1 ) - { - seqSource.unread(ic); - } - retval = COSNumber.get( buf.toString() ); + return parseCOSNumber(); } else { @@ -973,8 +930,25 @@ public abstract class BaseParser } } } + return null; + } + + private COSNumber parseCOSNumber() throws IOException + { + StringBuilder buf = new StringBuilder(); + int ic = seqSource.read(); + char c = (char) ic; + while (Character.isDigit(c) || c == '-' || c == '+' || c == '.' || c == 'E' || c == 'e') + { + buf.append(c); + ic = seqSource.read(); + c = (char) ic; } - return retval; + if (ic != -1) + { + seqSource.unread(ic); + } + return COSNumber.get(buf.toString()); } /** diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/COSParser.java b/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/COSParser.java index a89ca2a..26f7d8a 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/COSParser.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/COSParser.java @@ -2747,9 +2747,13 @@ public class COSParser extends BaseParser try { long currOffset = Long.parseLong(splitString[0]); - int currGenID = Integer.parseInt(splitString[1]); - COSObjectKey objKey = new COSObjectKey(currObjID, currGenID); - xrefTrailerResolver.setXRef(objKey, currOffset); + // skip 0 offsets + if (currOffset > 0) + { + int currGenID = Integer.parseInt(splitString[1]); + COSObjectKey objKey = new COSObjectKey(currObjID, currGenID); + xrefTrailerResolver.setXRef(objKey, currOffset); + } } catch (NumberFormatException e) { diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParser.java b/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParser.java index 69dcd0a..ac52542 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParser.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParser.java @@ -19,11 +19,15 @@ package org.apache.pdfbox.pdfparser; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDocument; +import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSObject; import org.apache.pdfbox.cos.COSStream; @@ -41,7 +45,8 @@ public class PDFObjectStreamParser extends BaseParser private static final Log LOG = LogFactory.getLog(PDFObjectStreamParser.class); private List<COSObject> streamObjects = null; - private final COSStream stream; + private final int numberOfObjects; + private final int firstObject; /** * Constructor. @@ -53,8 +58,19 @@ public class PDFObjectStreamParser extends BaseParser public PDFObjectStreamParser(COSStream stream, COSDocument document) throws IOException { super(new InputStreamSource(stream.createInputStream())); - this.stream = stream; this.document = document; + // get mandatory number of objects + numberOfObjects = stream.getInt(COSName.N); + if (numberOfObjects == -1) + { + throw new IOException("/N entry missing in object stream"); + } + // get mandatory stream offset of the first object + firstObject = stream.getInt(COSName.FIRST); + if (firstObject == -1) + { + throw new IOException("/First entry missing in object stream"); + } } /** @@ -67,47 +83,19 @@ public class PDFObjectStreamParser extends BaseParser { try { - //need to first parse the header. - int numberOfObjects = stream.getInt( "N" ); - if (numberOfObjects == -1) - { - throw new IOException("/N entry missing in object stream"); - } - List<Long> objectNumbers = new ArrayList<Long>( numberOfObjects ); + Map<Integer, Long> offsets = readOffsets(); streamObjects = new ArrayList<COSObject>( numberOfObjects ); - for( int i=0; i<numberOfObjects; i++ ) + for (Entry<Integer, Long> offset : offsets.entrySet()) { - long objectNumber = readObjectNumber(); - // skip offset - readLong(); - objectNumbers.add( objectNumber); - } - COSObject object; - COSBase cosObject; - int objectCounter = 0; - while( (cosObject = parseDirObject()) != null ) - { - object = new COSObject(cosObject); + COSBase cosObject = parseObject(offset.getKey()); + COSObject object = new COSObject(cosObject); object.setGenerationNumber(0); - if (objectCounter >= objectNumbers.size()) - { - LOG.error("/ObjStm (object stream) has more objects than /N " + numberOfObjects); - break; - } - object.setObjectNumber( objectNumbers.get( objectCounter) ); - streamObjects.add( object ); - if(LOG.isDebugEnabled()) - { - LOG.debug( "parsed=" + object ); - } - // According to the spec objects within an object stream shall not be enclosed - // by obj/endobj tags, but there are some pdfs in the wild using those tags - // skip endobject marker if present - if (!seqSource.isEOF() && seqSource.peek() == 'e') + object.setObjectNumber(offset.getValue()); + streamObjects.add(object); + if (LOG.isDebugEnabled()) { - readLine(); + LOG.debug("parsed=" + object); } - objectCounter++; } } finally @@ -125,4 +113,32 @@ public class PDFObjectStreamParser extends BaseParser { return streamObjects; } + + private Map<Integer, Long> readOffsets() throws IOException + { + // according to the pdf spec the offsets shall be sorted ascending + // but we can't rely on that, so that we have to sort the offsets + // as the sequential parsers relies on it, see PDFBOX-4927 + Map<Integer, Long> objectNumbers = new TreeMap<Integer, Long>(); + for (int i = 0; i < numberOfObjects; i++) + { + long objectNumber = readObjectNumber(); + int offset = (int) readLong(); + objectNumbers.put(offset, objectNumber); + } + return objectNumbers; + } + + private COSBase parseObject(int offset) throws IOException + { + long currentPosition = seqSource.getPosition(); + int finalPosition = firstObject + offset; + if (finalPosition > 0 && currentPosition < finalPosition) + { + // jump to the offset of the object to be parsed + seqSource.readFully(finalPosition - (int) currentPosition); + } + return parseDirObject(); + } + } diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFStreamParser.java b/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFStreamParser.java index 73a5fe4..8a3e78a 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFStreamParser.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFStreamParser.java @@ -137,19 +137,15 @@ public class PDFStreamParser extends BaseParser */ public Object parseNextToken() throws IOException { - Object retval; - skipSpaces(); - int nextByte = seqSource.peek(); - if( ((byte)nextByte) == -1 ) + if (seqSource.isEOF()) { return null; } - char c = (char)nextByte; + char c = (char) seqSource.peek(); switch (c) { case '<': - { // pull off first left bracket int leftBracket = seqSource.read(); @@ -161,74 +157,57 @@ public class PDFStreamParser extends BaseParser if (c == '<') { - retval = parseCOSDictionary(); + return parseCOSDictionary(); } else { - retval = parseCOSString(); + return parseCOSString(); } - break; - } case '[': - { // array - retval = parseCOSArray(); - break; - } + return parseCOSArray(); case '(': // string - retval = parseCOSString(); - break; + return parseCOSString(); case '/': // name - retval = parseCOSName(); - break; + return parseCOSName(); case 'n': - { // null String nullString = readString(); if( nullString.equals( "null") ) { - retval = COSNull.NULL; + return COSNull.NULL; } else { - retval = Operator.getOperator(nullString); + return Operator.getOperator(nullString); } - break; - } case 't': case 'f': - { String next = readString(); if( next.equals( "true" ) ) { - retval = COSBoolean.TRUE; - break; + return COSBoolean.TRUE; } else if( next.equals( "false" ) ) { - retval = COSBoolean.FALSE; + return COSBoolean.FALSE; } else { - retval = Operator.getOperator(next); + return Operator.getOperator(next); } - break; - } case 'R': - { String line = readString(); if( line.equals( "R" ) ) { - retval = new COSObject( null ); + return new COSObject(null); } else { - retval = Operator.getOperator(line); + return Operator.getOperator(line); } - break; - } case '0': case '1': case '2': @@ -242,7 +221,6 @@ public class PDFStreamParser extends BaseParser case '-': case '+': case '.': - { /* We will be filling buf with the rest of the number. Only * allow 1 "." and "-" and "+" at start of number. */ StringBuilder buf = new StringBuilder(); @@ -270,39 +248,35 @@ public class PDFStreamParser extends BaseParser dotNotRead = false; } } - retval = COSNumber.get( buf.toString() ); - break; - } + return COSNumber.get(buf.toString()); case 'B': - { - String next = readString(); - retval = Operator.getOperator(next); - if (next.equals(OperatorName.BEGIN_INLINE_IMAGE)) + String nextOperator = readString(); + Operator beginImageOP = Operator.getOperator(nextOperator); + if (nextOperator.equals(OperatorName.BEGIN_INLINE_IMAGE)) { - Operator beginImageOP = (Operator)retval; COSDictionary imageParams = new COSDictionary(); - beginImageOP.setImageParameters( imageParams ); + beginImageOP.setImageParameters(imageParams); Object nextToken = null; - while( (nextToken = parseNextToken()) instanceof COSName ) + while ((nextToken = parseNextToken()) instanceof COSName) { Object value = parseNextToken(); - imageParams.setItem( (COSName)nextToken, (COSBase)value ); + imageParams.setItem((COSName) nextToken, (COSBase) value); } - //final token will be the image data, maybe?? + // final token will be the image data, maybe?? if (nextToken instanceof Operator) { Operator imageData = (Operator) nextToken; - if (imageData.getImageData() == null || imageData.getImageData().length == 0) + if (imageData.getImageData() == null + || imageData.getImageData().length == 0) { - LOG.warn("empty inline image at stream offset " + seqSource.getPosition()); + LOG.warn("empty inline image at stream offset " + + seqSource.getPosition()); } beginImageOP.setImageData(imageData.getImageData()); } } - break; - } + return beginImageOP; case 'I': - { //Special case for ID operator String id = Character.toString((char) seqSource.read()) + (char) seqSource.read(); if (!id.equals(OperatorName.BEGIN_INLINE_IMAGE_DATA)) @@ -333,37 +307,26 @@ public class PDFStreamParser extends BaseParser currentByte = seqSource.read(); } // the EI operator isn't unread, as it won't be processed anyway - retval = Operator.getOperator(OperatorName.BEGIN_INLINE_IMAGE_DATA); + Operator beginImageDataOP = Operator + .getOperator(OperatorName.BEGIN_INLINE_IMAGE_DATA); // save the image data to the operator, so that it can be accessed later - ((Operator)retval).setImageData( imageData.toByteArray() ); - break; - } + beginImageDataOP.setImageData(imageData.toByteArray()); + return beginImageDataOP; case ']': - { // some ']' around without its previous '[' // this means a PDF is somewhat corrupt but we will continue to parse. seqSource.read(); - // must be a better solution than null... - retval = COSNull.NULL; - break; - } + return COSNull.NULL; default: - { - //we must be an operator + // we must be an operator String operator = readOperator(); - if( operator.trim().length() == 0 ) - { - //we have a corrupt stream, stop reading here - retval = null; - } - else + if (operator.trim().length() > 0) { - retval = Operator.getOperator(operator); + return Operator.getOperator(operator); } - } } - return retval; + return null; } /** diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFXrefStreamParser.java b/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFXrefStreamParser.java index bd7d18e..b8e6af7 100755 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFXrefStreamParser.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/PDFXrefStreamParser.java @@ -38,8 +38,9 @@ import org.apache.pdfbox.cos.COSObjectKey; */ public class PDFXrefStreamParser extends BaseParser { - private final COSStream stream; private final XrefTrailerResolver xrefTrailerResolver; + private final int[] w = new int[3]; + private final List<Long> objNums = new ArrayList<Long>(); /** * Constructor. @@ -54,22 +55,29 @@ public class PDFXrefStreamParser extends BaseParser throws IOException { super(new InputStreamSource(stream.createInputStream())); - this.stream = stream; this.document = document; this.xrefTrailerResolver = resolver; + try + { + initParserValues(stream); + } + catch (IOException exception) + { + close(); + } } - /** - * Parses through the unfiltered stream and populates the xrefTable HashMap. - * @throws IOException If there is an error while parsing the stream. - */ - public void parse() throws IOException + private void initParserValues(COSStream stream) throws IOException { COSArray wArray = stream.getCOSArray(COSName.W); if (wArray == null) { throw new IOException("/W array is missing in Xref stream"); } + for (int i = 0; i < 3; i++) + { + w[i] = wArray.getInt(i, 0); + } COSArray indexArray = stream.getCOSArray(COSName.INDEX); if (indexArray == null) @@ -80,8 +88,6 @@ public class PDFXrefStreamParser extends BaseParser indexArray.add(COSInteger.get(stream.getInt(COSName.SIZE, 0))); } - List<Long> objNums = new ArrayList<Long>(); - /* * Populates objNums with all object numbers available */ @@ -109,87 +115,68 @@ public class PDFXrefStreamParser extends BaseParser objNums.add(objID + i); } } + } + + private void close() throws IOException + { + if (seqSource != null) + { + seqSource.close(); + } + document = null; + objNums.clear(); + } + + /** + * Parses through the unfiltered stream and populates the xrefTable HashMap. + * @throws IOException If there is an error while parsing the stream. + */ + public void parse() throws IOException + { Iterator<Long> objIter = objNums.iterator(); - /* - * Calculating the size of the line in bytes - */ - int w0 = wArray.getInt(0, 0); - int w1 = wArray.getInt(1, 0); - int w2 = wArray.getInt(2, 0); - int lineSize = w0 + w1 + w2; + byte[] currLine = new byte[w[0] + w[1] + w[2]]; while (!seqSource.isEOF() && objIter.hasNext()) { - byte[] currLine = new byte[lineSize]; seqSource.read(currLine); - int type; - if (w0 == 0) + // get the current objID + Long objID = objIter.next(); + + // default value is 1 if w[0] == 0, otherwise parse first field + int type = w[0] == 0 ? 1 : (int) parseValue(currLine, 0, w[0]); + // Skip free objects (type 0) and invalid types + if (type == 0) { - // "If the first element is zero, - // the type field shall not be present, and shall default to type 1" - type = 1; + continue; } - else + // second field holds the offset (type 1) or the object stream number (type 2) + long offset = parseValue(currLine, w[0], w[1]); + // third field holds the generation number for type 1 entries + int genNum = type == 1 ? (int) parseValue(currLine, w[0] + w[1], w[2]) : 0; + COSObjectKey objKey = new COSObjectKey(objID, genNum); + if (type == 1) { - type = 0; - /* - * Grabs the number of bytes specified for the first column in - * the W array and stores it. - */ - for (int i = 0; i < w0; i++) - { - type += (currLine[i] & 0x00ff) << ((w0 - i - 1) * 8); - } + xrefTrailerResolver.setXRef(objKey, offset); } - //Need to remember the current objID - Long objID = objIter.next(); - /* - * 3 different types of entries. - */ - switch(type) + else { - case 0: - /* - * Skipping free objects - */ - break; - case 1: - int offset = 0; - for(int i = 0; i < w1; i++) - { - offset += (currLine[i + w0] & 0x00ff) << ((w1 - i - 1) * 8); - } - int genNum = 0; - for(int i = 0; i < w2; i++) - { - genNum += (currLine[i + w0 + w1] & 0x00ff) << ((w2 - i - 1) * 8); - } - COSObjectKey objKey = new COSObjectKey(objID, genNum); - xrefTrailerResolver.setXRef(objKey, offset); - break; - case 2: - /* - * object stored in object stream: - * 2nd argument is object number of object stream - * 3rd argument is index of object within object stream - * - * For XRef aware parsers we have to know which objects contain - * object streams. We will store this information in normal xref mapping - * table but add object stream number with minus sign in order to - * distinguish from file offsets - */ - int objstmObjNr = 0; - for(int i = 0; i < w1; i++) - { - objstmObjNr += (currLine[i + w0] & 0x00ff) << ((w1 - i - 1) * 8); - } - objKey = new COSObjectKey( objID, 0 ); - xrefTrailerResolver.setXRef( objKey, -objstmObjNr ); - break; - default: - break; + // For XRef aware parsers we have to know which objects contain object streams. We will store this + // information in normal xref mapping table but add object stream number with minus sign in order to + // distinguish from file offsets + xrefTrailerResolver.setXRef(objKey, -offset); } } + close(); + } + + private long parseValue(byte[] data, int start, int length) + { + long value = 0; + for (int i = 0; i < length; i++) + { + value += ((long) data[i + start] & 0x00ff) << ((length - i - 1) * 8); + } + return value; } } diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/COSArrayList.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/COSArrayList.java index 5dd5156..3370551 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/COSArrayList.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/COSArrayList.java @@ -60,7 +60,7 @@ public class COSArrayList<E> implements List<E> } /** - * Create the COSArrayList specifing the List and the backing COSArray. + * Create the COSArrayList specifying the List and the backing COSArray. * * <p>User of this constructor need to ensure that the entries in the List and * the backing COSArray are matching i.e. the COSObject of the List entry is diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNameTreeNode.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNameTreeNode.java index a880865..21bd24e 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNameTreeNode.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDNameTreeNode.java @@ -114,7 +114,7 @@ public abstract class PDNameTreeNode<T extends COSObjectable> implements COSObje public List<PDNameTreeNode<T>> getKids() { List<PDNameTreeNode<T>> retval = null; - COSArray kids = (COSArray)node.getDictionaryObject( COSName.KIDS ); + COSArray kids = node.getCOSArray(COSName.KIDS); if( kids != null ) { List<PDNameTreeNode<T>> pdObjects = new ArrayList<PDNameTreeNode<T>>(); @@ -257,13 +257,18 @@ public abstract class PDNameTreeNode<T extends COSObjectable> implements COSObje */ public Map<String, T> getNames() throws IOException { - COSArray namesArray = (COSArray)node.getDictionaryObject( COSName.NAMES ); + COSArray namesArray = node.getCOSArray(COSName.NAMES); if( namesArray != null ) { Map<String, T> names = new LinkedHashMap<String, T>(); for( int i=0; i<namesArray.size(); i+=2 ) { - COSString key = (COSString)namesArray.getObject(i); + COSBase base = namesArray.getObject(i); + if (!(base instanceof COSString)) + { + throw new IOException("Expected string, found " + base + " in name tree at index " + i); + } + COSString key = (COSString) base; COSBase cosValue = namesArray.getObject( i+1 ); names.put( key.getString(), convertCOSToPD(cosValue) ); } @@ -330,7 +335,7 @@ public abstract class PDNameTreeNode<T extends COSObjectable> implements COSObje public String getUpperLimit() { String retval = null; - COSArray arr = (COSArray)node.getDictionaryObject( COSName.LIMITS ); + COSArray arr = node.getCOSArray(COSName.LIMITS); if( arr != null ) { retval = arr.getString( 1 ); @@ -345,7 +350,7 @@ public abstract class PDNameTreeNode<T extends COSObjectable> implements COSObje */ private void setUpperLimit( String upper ) { - COSArray arr = (COSArray)node.getDictionaryObject( COSName.LIMITS ); + COSArray arr = node.getCOSArray(COSName.LIMITS); if( arr == null ) { arr = new COSArray(); @@ -364,7 +369,7 @@ public abstract class PDNameTreeNode<T extends COSObjectable> implements COSObje public String getLowerLimit() { String retval = null; - COSArray arr = (COSArray)node.getDictionaryObject( COSName.LIMITS ); + COSArray arr = node.getCOSArray(COSName.LIMITS); if( arr != null ) { retval = arr.getString( 0 ); @@ -379,7 +384,7 @@ public abstract class PDNameTreeNode<T extends COSObjectable> implements COSObje */ private void setLowerLimit( String lower ) { - COSArray arr = (COSArray)node.getDictionaryObject( COSName.LIMITS ); + COSArray arr = node.getCOSArray(COSName.LIMITS); if( arr == null ) { arr = new COSArray(); diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunction.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunction.java index b44ef18..6c32739 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunction.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunction.java @@ -303,7 +303,7 @@ public abstract class PDFunction implements COSObjectable { COSArray rangesArray = getRangeValues(); float[] result; - if (rangesArray != null) + if (rangesArray != null && rangesArray.size() > 0) { float[] rangeValues = rangesArray.toFloatArray(); int numberOfRanges = rangeValues.length/2; diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java index d07c8d5..b73ec34 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java @@ -17,6 +17,8 @@ package org.apache.pdfbox.pdmodel.common.function; import java.io.IOException; +import java.io.InputStream; + import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.MemoryCacheImageInputStream; @@ -394,7 +396,8 @@ public class PDFunctionType0 extends PDFunction // PDF spec 1.7 p.171: // Each sample value is represented as a sequence of BitsPerSample bits. // Successive values are adjacent in the bit stream; there is no padding at byte boundaries. - ImageInputStream mciis = new MemoryCacheImageInputStream(getPDStream().createInputStream()); + InputStream inputStream = getPDStream().createInputStream(); + ImageInputStream mciis = new MemoryCacheImageInputStream(inputStream); for (int i = 0; i < arraySize; i++) { for (int k = 0; k < nOut; k++) @@ -405,6 +408,7 @@ public class PDFunctionType0 extends PDFunction index++; } mciis.close(); + inputStream.close(); } catch (IOException exception) { diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java index e191a48..64850a5 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java @@ -1084,7 +1084,7 @@ public final class StandardSecurityHandler extends SecurityHandler { try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); + MessageDigest md = MessageDigests.getSHA256(); byte[] k = md.digest(input); byte[] e = null; @@ -1153,22 +1153,14 @@ public final class StandardSecurityHandler extends SecurityHandler } } - private static byte[] computeSHA256(byte[] input, byte[] password, byte[] userKey) - throws IOException + private static byte[] computeSHA256(byte[] input, byte[] password, byte[] userKey) { - try - { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(input); - md.update(password); - return userKey == null ? md.digest() : md.digest(userKey); - } - catch (NoSuchAlgorithmException e) - { - throw new IOException(e); - } + MessageDigest md = MessageDigests.getSHA256(); + md.update(input); + md.update(password); + return userKey == null ? md.digest() : md.digest(userKey); } - + private static byte[] concat(byte[] a, byte[] b) { byte[] o = new byte[a.length + b.length]; diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java index ce365e9..2b5d448 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java @@ -16,6 +16,7 @@ */ package org.apache.pdfbox.pdmodel.font; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -112,11 +113,8 @@ final class FontMapperImpl implements FontMapper try { String ttfName = "/org/apache/pdfbox/resources/ttf/LiberationSans-Regular.ttf"; - InputStream ttfStream = FontMapper.class.getResourceAsStream(ttfName); - if (ttfStream == null) - { - throw new IOException("Error loading resource: " + ttfName); - } + InputStream ttfStream = + new BufferedInputStream(FontMapper.class.getResourceAsStream(ttfName)); TTFParser ttfParser = new TTFParser(); lastResortFont = ttfParser.parse(ttfStream); } diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java index 296a880..1ec00a2 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java @@ -391,7 +391,7 @@ public abstract class PDFont implements COSObjectable, PDFontLike { float totalWidth = 0.0f; float characterCount = 0.0f; - COSArray widths = (COSArray) dict.getDictionaryObject(COSName.WIDTHS); + COSArray widths = dict.getCOSArray(COSName.WIDTHS); if (widths != null) { for (int i = 0; i < widths.size(); i++) @@ -501,7 +501,7 @@ public abstract class PDFont implements COSObjectable, PDFontLike { if (widths == null) { - COSArray array = (COSArray) dict.getDictionaryObject(COSName.WIDTHS); + COSArray array = dict.getCOSArray(COSName.WIDTHS); if (array != null) { widths = COSArrayList.convertFloatCOSArrayToList(array); diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFontDescriptor.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFontDescriptor.java index d723a17..b8d832f 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFontDescriptor.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFontDescriptor.java @@ -429,7 +429,7 @@ public final class PDFontDescriptor implements COSObjectable */ public PDRectangle getFontBoundingBox() { - COSArray rect = (COSArray)dic.getDictionaryObject( COSName.FONT_BBOX ); + COSArray rect = dic.getCOSArray(COSName.FONT_BBOX); PDRectangle retval = null; if( rect != null ) { diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType0Font.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType0Font.java index ab40c46..142e10d 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType0Font.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType0Font.java @@ -78,7 +78,7 @@ public class PDType0Font extends PDFont implements PDVectorFont */ public static PDType0Font load(PDDocument doc, InputStream input) throws IOException { - return new PDType0Font(doc, new TTFParser().parse(input), true, true, false); + return load(doc, input, true); } /** diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1Font.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1Font.java index df115d9..663b78a 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1Font.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1Font.java @@ -174,15 +174,7 @@ public class PDType1Font extends PDSimpleFont */ public PDType1Font(PDDocument doc, InputStream pfbIn) throws IOException { - PDType1FontEmbedder embedder = new PDType1FontEmbedder(doc, dict, pfbIn, null); - encoding = embedder.getFontEncoding(); - glyphList = embedder.getGlyphList(); - type1font = embedder.getType1Font(); - genericFont = embedder.getType1Font(); - isEmbedded = true; - isDamaged = false; - fontMatrixTransform = new AffineTransform(); - codeToBytesMap = new HashMap<Integer,byte[]>(); + this(doc, pfbIn, null); } /** @@ -196,7 +188,7 @@ public class PDType1Font extends PDSimpleFont public PDType1Font(PDDocument doc, InputStream pfbIn, Encoding encoding) throws IOException { PDType1FontEmbedder embedder = new PDType1FontEmbedder(doc, dict, pfbIn, encoding); - this.encoding = encoding; + this.encoding = encoding == null ? embedder.getFontEncoding() : encoding; glyphList = embedder.getGlyphList(); type1font = embedder.getType1Font(); genericFont = embedder.getType1Font(); diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/Standard14Fonts.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/Standard14Fonts.java index 96ee106..8f13daf 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/Standard14Fonts.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/Standard14Fonts.java @@ -17,11 +17,11 @@ package org.apache.pdfbox.pdmodel.font; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.fontbox.afm.AFMParser; @@ -35,133 +35,189 @@ import org.apache.fontbox.afm.FontMetrics; */ final class Standard14Fonts { - private static final Set<String> STANDARD_14_NAMES = new HashSet<String>(34); - private static final Map<String, String> STANDARD_14_MAPPING = new HashMap<String, String>(34); - private static final Map<String, FontMetrics> STANDARD14_AFM_MAP = new HashMap<String, FontMetrics>(34); + /** + * Contains all base names and alias names for the known fonts. + * For base fonts both the key and the value will be the base name. + * For aliases, the key is an alias, and the value is a base name. + * We want a single lookup in the map to find the font both by a base name or an alias. + */ + private static final Map<String, String> ALIASES = new HashMap<String, String>(38); + + /** + * Contains the font metrics for the base fonts. + * The key is a base font name, value is a FontMetrics instance. + * Metrics are loaded into this map on demand, only if needed. + * @see #getAFM + */ + private static final Map<String, FontMetrics> FONTS = new HashMap<String, FontMetrics>(14); + static { - try - { - addAFM("Courier-Bold"); - addAFM("Courier-BoldOblique"); - addAFM("Courier"); - addAFM("Courier-Oblique"); - addAFM("Helvetica"); - addAFM("Helvetica-Bold"); - addAFM("Helvetica-BoldOblique"); - addAFM("Helvetica-Oblique"); - addAFM("Symbol"); - addAFM("Times-Bold"); - addAFM("Times-BoldItalic"); - addAFM("Times-Italic"); - addAFM("Times-Roman"); - addAFM("ZapfDingbats"); - - // alternative names from Adobe Supplement to the ISO 32000 - addAFM("CourierCourierNew", "Courier"); - addAFM("CourierNew", "Courier"); - addAFM("CourierNew,Italic", "Courier-Oblique"); - addAFM("CourierNew,Bold", "Courier-Bold"); - addAFM("CourierNew,BoldItalic", "Courier-BoldOblique"); - addAFM("Arial", "Helvetica"); - addAFM("Arial,Italic", "Helvetica-Oblique"); - addAFM("Arial,Bold", "Helvetica-Bold"); - addAFM("Arial,BoldItalic", "Helvetica-BoldOblique"); - addAFM("TimesNewRoman", "Times-Roman"); - addAFM("TimesNewRoman,Italic", "Times-Italic"); - addAFM("TimesNewRoman,Bold", "Times-Bold"); - addAFM("TimesNewRoman,BoldItalic", "Times-BoldItalic"); - - // Acrobat treats these fonts as "standard 14" too (at least Acrobat preflight says so) - addAFM("Symbol,Italic", "Symbol"); - addAFM("Symbol,Bold", "Symbol"); - addAFM("Symbol,BoldItalic", "Symbol"); - addAFM("Times", "Times-Roman"); - 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) - { - throw new RuntimeException(e); - } - } + // the 14 standard fonts + mapName("Courier-Bold"); + mapName("Courier-BoldOblique"); + mapName("Courier"); + mapName("Courier-Oblique"); + mapName("Helvetica"); + mapName("Helvetica-Bold"); + mapName("Helvetica-BoldOblique"); + mapName("Helvetica-Oblique"); + mapName("Symbol"); + mapName("Times-Bold"); + mapName("Times-BoldItalic"); + mapName("Times-Italic"); + mapName("Times-Roman"); + mapName("ZapfDingbats"); - private Standard14Fonts() - { + // alternative names from Adobe Supplement to the ISO 32000 + mapName("CourierCourierNew", "Courier"); + mapName("CourierNew", "Courier"); + mapName("CourierNew,Italic", "Courier-Oblique"); + mapName("CourierNew,Bold", "Courier-Bold"); + mapName("CourierNew,BoldItalic", "Courier-BoldOblique"); + mapName("Arial", "Helvetica"); + mapName("Arial,Italic", "Helvetica-Oblique"); + mapName("Arial,Bold", "Helvetica-Bold"); + mapName("Arial,BoldItalic", "Helvetica-BoldOblique"); + mapName("TimesNewRoman", "Times-Roman"); + mapName("TimesNewRoman,Italic", "Times-Italic"); + mapName("TimesNewRoman,Bold", "Times-Bold"); + mapName("TimesNewRoman,BoldItalic", "Times-BoldItalic"); + + // Acrobat treats these fonts as "standard 14" too (at least Acrobat preflight says so) + mapName("Symbol,Italic", "Symbol"); + mapName("Symbol,Bold", "Symbol"); + mapName("Symbol,BoldItalic", "Symbol"); + mapName("Times", "Times-Roman"); + mapName("Times,Italic", "Times-Italic"); + mapName("Times,Bold", "Times-Bold"); + mapName("Times,BoldItalic", "Times-BoldItalic"); + + // PDFBOX-3457: PDF.js file bug864847.pdf + mapName("ArialMT", "Helvetica"); + mapName("Arial-ItalicMT", "Helvetica-Oblique"); + mapName("Arial-BoldMT", "Helvetica-Bold"); + mapName("Arial-BoldItalicMT", "Helvetica-BoldOblique"); } - private static void addAFM(String fontName) throws IOException + private Standard14Fonts() { - addAFM(fontName, fontName); } - private static void addAFM(String fontName, String afmName) throws IOException + /** + * Loads the metrics for the base font specified by name. Metric file must exist in the pdfbox + * jar under /org/apache/pdfbox/resources/afm/ + * + * @param fontName one of the standard 14 font names for which to lod the metrics. + * @throws IOException if no metrics exist for that font. + */ + private static void loadMetrics(String fontName) throws IOException { - STANDARD_14_NAMES.add(fontName); - STANDARD_14_MAPPING.put(fontName, afmName); - - if (STANDARD14_AFM_MAP.containsKey(afmName)) - { - STANDARD14_AFM_MAP.put(fontName, STANDARD14_AFM_MAP.get(afmName)); - } - - String resourceName = "/org/apache/pdfbox/resources/afm/" + afmName + ".afm"; - InputStream afmStream = PDType1Font.class.getResourceAsStream(resourceName); - if (afmStream == null) - { - throw new IOException(resourceName + " not found"); - } + String resourceName = "/org/apache/pdfbox/resources/afm/" + fontName + ".afm"; + InputStream afmStream = + new BufferedInputStream(PDType1Font.class.getResourceAsStream(resourceName)); try { AFMParser parser = new AFMParser(afmStream); FontMetrics metric = parser.parse(true); - STANDARD14_AFM_MAP.put(fontName, metric); + FONTS.put(fontName, metric); } finally { afmStream.close(); - } + } } /** - * Returns the AFM for the given font. - * @param baseName base name of font + * Adds a standard font name to the map of known aliases, to simplify the logic of finding + * font metrics by name. We want a single lookup in the map to find the font both by a base name or + * an alias. + * + * @see #getAFM + * @param baseName the base name of the font; must be one of the 14 standard fonts */ - public static FontMetrics getAFM(String baseName) + private static void mapName(String baseName) { - return STANDARD14_AFM_MAP.get(baseName); + ALIASES.put(baseName, baseName); + } + + /** + * Adds an alias name for a standard font to the map of known aliases to the map of aliases + * (alias as key, standard name as value). We want a single lookup in the map to find the font + * both by a base name or an alias. + * + * @param alias an alias for the font + * @param baseName the base name of the font; must be one of the 14 standard fonts + */ + private static void mapName(String alias, String baseName) + { + ALIASES.put(alias, baseName); + } + + /** + * Returns the metrics for font specified by fontName. Loads the font metrics if not already + * loaded. + * + * @param fontName name of font; either a base name or alias + * @return the font metrics or null if the name is not one of the known names + * @throws IllegalArgumentException if no metrics exist for that font. + */ + public static FontMetrics getAFM(String fontName) + { + String baseName = ALIASES.get(fontName); + if (baseName == null) + { + return null; + } + + if (FONTS.get(baseName) == null) + { + synchronized (FONTS) + { + if (FONTS.get(baseName) == null) + { + try + { + loadMetrics(baseName); + } + catch (IOException ex) + { + throw new IllegalArgumentException(ex); + } + } + } + } + + return FONTS.get(baseName); } /** - * Returns true if the given font name a Standard 14 font. - * @param baseName base name of font + * Returns true if the given font name is one of the known names, including alias. + * + * @param fontName the name of font, either a base name or alias + * @return true if the name is one of the known names */ - public static boolean containsName(String baseName) + public static boolean containsName(String fontName) { - return STANDARD_14_NAMES.contains(baseName); + return ALIASES.containsKey(fontName); } /** - * Returns the set of Standard 14 font names, including additional names. + * Returns the set of known font names, including aliases. */ public static Set<String> getNames() { - return Collections.unmodifiableSet(STANDARD_14_NAMES); + return Collections.unmodifiableSet(ALIASES.keySet()); } /** - * Returns the name of the actual font which the given font name maps to. - * @param baseName base name of font + * Returns the base name of the font which the given font name maps to. + * + * @param fontName name of font, either a base name or an alias + * @return the base name or null if this is not one of the known names */ - public static String getMappedFontName(String baseName) + public static String getMappedFontName(String fontName) { - return STANDARD_14_MAPPING.get(baseName); + return ALIASES.get(fontName); } } diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/TrueTypeEmbedder.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/TrueTypeEmbedder.java index 31b453b..92f853c 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/TrueTypeEmbedder.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/TrueTypeEmbedder.java @@ -90,7 +90,26 @@ abstract class TrueTypeEmbedder implements Subsetter if (!embedSubset) { // full embedding - PDStream stream = new PDStream(document, ttf.getOriginalData(), COSName.FLATE_DECODE); + + // TrueType collections are not supported + InputStream is = ttf.getOriginalData(); + byte[] b = new byte[4]; + is.mark(b.length); + if (is.read(b) == b.length && new String(b).equals("ttcf")) + { + is.close(); + throw new IOException("Full embedding of TrueType font collections not supported"); + } + if (is.markSupported()) + { + is.reset(); + } + else + { + is.close(); + is = ttf.getOriginalData(); + } + PDStream stream = new PDStream(document, is, COSName.FLATE_DECODE); stream.getCOSObject().setLong(COSName.LENGTH1, ttf.getOriginalDataSize()); fontDescriptor.setFontFile2(stream); } @@ -137,15 +156,13 @@ abstract class TrueTypeEmbedder implements Subsetter 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) == + if ((fsType & OS2WindowsMetricsTable.FSTYPE_RESTRICTED) == OS2WindowsMetricsTable.FSTYPE_RESTRICTED) { // restricted License embedding return false; } - else if ((exclusive & OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) == + else if ((fsType & OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) == OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) { // bitmap embedding only @@ -181,7 +198,15 @@ abstract class TrueTypeEmbedder implements Subsetter fd.setFontName(ttf.getName()); OS2WindowsMetricsTable os2 = ttf.getOS2Windows(); + if (os2 == null) + { + throw new IOException("os2 table is missing in font " + ttf.getName()); + } PostScriptTable post = ttf.getPostScript(); + if (post == null) + { + throw new IOException("post table is missing in font " + ttf.getName()); + } // Flags fd.setFixedPitch(post.getIsFixedPitch() > 0 || diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java index 2e20120..856a13c 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java @@ -180,6 +180,11 @@ public class DictionaryEncoding extends Encoding @Override public String getEncodingName() { + if (baseEncoding == null) + { + // In type 3 the /Differences array shall specify the complete character encoding + return "differences"; + } return baseEncoding.getEncodingName() + " with differences"; } } diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/MacRomanEncoding.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/MacRomanEncoding.java index b895662..33c3976 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/MacRomanEncoding.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/MacRomanEncoding.java @@ -242,7 +242,7 @@ public class MacRomanEncoding extends Encoding {0172, "z"}, {060, "zero"}, // adding an additional mapping as defined in Appendix D of the pdf spec - {0312, "space"} + {0312, "nbspace"} }; /** diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/WinAnsiEncoding.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/WinAnsiEncoding.java index fab011a..12fc80f 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/WinAnsiEncoding.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/WinAnsiEncoding.java @@ -251,7 +251,7 @@ public class WinAnsiEncoding extends Encoding {0236, "zcaron"}, {060, "zero"}, // adding some additional mappings as defined in Appendix D of the pdf spec - {0240, "space"}, + {0240, "nbspace"}, {0255, "hyphen"} }; diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java index 1bb2bff..351b8e8 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java @@ -25,6 +25,7 @@ import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; @@ -96,11 +97,7 @@ public class PDDeviceCMYK extends PDDeviceColorSpace String name = "/org/apache/pdfbox/resources/icc/ISOcoated_v2_300_bas.icc"; - InputStream is = PDDeviceCMYK.class.getResourceAsStream(name); - if (is == null) - { - throw new IOException("Error loading resource: " + name); - } + InputStream is = new BufferedInputStream(PDDeviceCMYK.class.getResourceAsStream(name)); ICC_Profile iccProfile = ICC_Profile.getInstance(is); is.close(); diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java index 75adfb4..3167668 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java @@ -523,6 +523,11 @@ public final class PDImageXObject extends PDXObject implements PDImage // see PDF specification 1.7, 11.6.5.3 Soft-Mask Images matte = ((COSArray) base).toFloatArray(); // convert to RGB + if (matte.length < getColorSpace().getNumberOfComponents()) + { + LOG.error("Image /Matte entry not long enough for colorspace, skipped"); + return null; + } matte = getColorSpace().toRGB(matte); } return matte; diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java index 86350b1..2fa3720 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java @@ -224,7 +224,11 @@ public class PDOptionalContentProperties implements COSObjectable */ public String[] getGroupNames() { - COSArray ocgs = (COSArray)dict.getDictionaryObject(COSName.OCGS); + COSArray ocgs = dict.getCOSArray(COSName.OCGS); + if (ocgs == null) + { + return new String[0]; + } int size = ocgs.size(); String[] groups = new String[size]; for (int i = 0; i < size; i++) diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java index 29160d8..1d69570 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java @@ -798,7 +798,7 @@ class AppearanceGeneratorHelper { float width = contentRect.getWidth() - contentRect.getLowerLeftX(); float fs = MINIMUM_FONT_SIZE; - while (fs <= MAXIMUM_FONT_SIZE) + while (fs <= DEFAULT_FONT_SIZE) { // determine the number of lines needed for this font and contentRect int numLines = 0; @@ -818,7 +818,7 @@ class AppearanceGeneratorHelper } fs++; } - return Math.min(fs, MAXIMUM_FONT_SIZE); + return Math.min(fs, DEFAULT_FONT_SIZE); } // Acrobat defaults to 12 for multiline text with size 0 diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java index 8f53f59..9154c0c 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java @@ -318,9 +318,7 @@ public final class PDAcroForm implements COSObjectable { annotations.add(annotation); } - else if (!annotation.isInvisible() && !annotation.isHidden() && - annotation.getNormalAppearanceStream() != null && - annotation.getNormalAppearanceStream().getBBox() != null) + else if (isVisibleAnnotation(annotation)) { contentStream = new PDPageContentStream(document, page, AppendMode.APPEND, true, !isContentStreamWrapped); isContentStreamWrapped = true; @@ -394,8 +392,22 @@ public final class PDAcroForm implements COSObjectable // remove XFA for hybrid forms dictionary.removeItem(COSName.XFA); - - } + } + + private boolean isVisibleAnnotation(PDAnnotation annotation) + { + if (annotation.isInvisible() || annotation.isHidden()) + { + return false; + } + PDAppearanceStream normalAppearanceStream = annotation.getNormalAppearanceStream(); + if (normalAppearanceStream == null) + { + return false; + } + PDRectangle bbox = normalAppearanceStream.getBBox(); + return bbox != null && bbox.getWidth() > 0 && bbox.getHeight() > 0; + } /** * Refreshes the appearance streams and appearance dictionaries for diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDSignatureField.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDSignatureField.java index 5e9eb47..9280eb7 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDSignatureField.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDSignatureField.java @@ -76,16 +76,13 @@ public class PDSignatureField extends PDTerminalField private String generatePartialName() { String fieldName = "Signature"; - Set<String> sigNames = new HashSet<String>(); + Set<String> nameSet = new HashSet<String>(); for (PDField field : getAcroForm().getFieldTree()) { - if(field instanceof PDSignatureField) - { - sigNames.add(field.getPartialName()); - } + nameSet.add(field.getPartialName()); } int i = 1; - while(sigNames.contains(fieldName+i)) + while (nameSet.contains(fieldName + i)) { ++i; } diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PlainText.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PlainText.java index af66d62..c6d8de4 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PlainText.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PlainText.java @@ -172,6 +172,9 @@ class PlainText { String word = textContent.substring(start,end); float wordWidth = font.getStringWidth(word) * scale; + + boolean wordNeedsSplit = false; + int splitOffset = end - start; lineWidth = lineWidth + wordWidth; @@ -182,7 +185,7 @@ class PlainText lineWidth = lineWidth - whitespaceWidth; } - if (lineWidth >= width) + if (lineWidth >= width && !textLine.getWords().isEmpty()) { textLine.setWidth(textLine.calculateWidth(font, fontSize)); textLines.add(textLine); @@ -190,13 +193,40 @@ class PlainText lineWidth = font.getStringWidth(word) * scale; } + if (wordWidth > width && textLine.getWords().isEmpty()) + { + // single word does not fit into width + wordNeedsSplit = true; + while (true) + { + splitOffset--; + String substring = word.trim().substring(0, splitOffset); + float substringWidth = font.getStringWidth(substring) * scale; + if (substringWidth < width) + { + word = substring; + wordWidth = font.getStringWidth(word) * scale; + lineWidth = wordWidth; + break; + } + } + } + AttributedString as = new AttributedString(word); as.addAttribute(TextAttribute.WIDTH, wordWidth); Word wordInstance = new Word(word); wordInstance.setAttributes(as); textLine.addWord(wordInstance); - start = end; - end = iterator.next(); + + if (wordNeedsSplit) + { + start = start + splitOffset; + } + else + { + start = end; + end = iterator.next(); + } } textLine.setWidth(textLine.calculateWidth(font, fontSize)); textLines.add(textLine); diff --git a/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java b/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java index ce0fb33..25c7207 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java @@ -82,6 +82,7 @@ import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray; import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased; import org.apache.pdfbox.pdmodel.graphics.color.PDPattern; +import org.apache.pdfbox.pdmodel.graphics.color.PDSeparation; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.form.PDTransparencyGroup; import org.apache.pdfbox.pdmodel.graphics.image.PDImage; @@ -332,7 +333,14 @@ public class PageDrawer extends PDFGraphicsStreamEngine protected Paint getPaint(PDColor color) throws IOException { PDColorSpace colorSpace = color.getColorSpace(); - if (!(colorSpace instanceof PDPattern)) + if (colorSpace instanceof PDSeparation && + "None".equals(((PDSeparation) colorSpace).getColorantName())) + { + // PDFBOX-4900: "The special colorant name None shall not produce any visible output" + //TODO better solution needs to be found for all occurences where toRGB is called + return new Color(0, 0, 0, 0); + } + else if (!(colorSpace instanceof PDPattern)) { float[] rgb = colorSpace.toRGB(color.getComponents()); return new Color(clampColor(rgb[0]), clampColor(rgb[1]), clampColor(rgb[2])); @@ -1031,11 +1039,13 @@ public class PageDrawer extends PDFGraphicsStreamEngine if (!pdImage.getInterpolate()) { - boolean isScaledUp = pdImage.getWidth() < Math.round(at.getScaleX()) || - pdImage.getHeight() < Math.round(at.getScaleY()); - // if the image is scaled down, we use smooth interpolation, eg PDFBOX-2364 // only when scaled up do we use nearest neighbour, eg PDFBOX-2302 / mori-cvpr01.pdf + // PDFBOX-4930: we use the sizes of the ARGB image. These can be different + // than the original sizes of the base image, when the mask is bigger. + boolean isScaledUp = pdImage.getImage().getWidth() < Math.round(at.getScaleX()) || + pdImage.getImage().getHeight() < Math.round(at.getScaleY()); + if (isScaledUp) { graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, @@ -1080,6 +1090,7 @@ public class PageDrawer extends PDFGraphicsStreamEngine Graphics2D g = (Graphics2D) renderedPaint.getGraphics(); g.translate(-bounds.getMinX(), -bounds.getMinY()); g.setPaint(paint); + g.setRenderingHints(graphics.getRenderingHints()); g.fill(bounds); g.dispose(); @@ -1091,6 +1102,7 @@ public class PageDrawer extends PDFGraphicsStreamEngine AffineTransform imageTransform = new AffineTransform(at); imageTransform.scale(1.0 / mask.getWidth(), -1.0 / mask.getHeight()); imageTransform.translate(0, -mask.getHeight()); + g.setRenderingHints(graphics.getRenderingHints()); g.drawImage(mask, imageTransform, null); g.dispose(); @@ -1190,18 +1202,23 @@ public class PageDrawer extends PDFGraphicsStreamEngine graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); setClip(); AffineTransform imageTransform = new AffineTransform(at); + int width = image.getWidth(); + int height = image.getHeight(); + imageTransform.scale(1.0 / width, -1.0 / height); + imageTransform.translate(0, -height); + PDSoftMask softMask = getGraphicsState().getSoftMask(); if( softMask != null ) { - imageTransform.scale(1, -1); - imageTransform.translate(0, -1); - Paint awtPaint = new TexturePaint(image, - new Rectangle2D.Double(imageTransform.getTranslateX(), imageTransform.getTranslateY(), - imageTransform.getScaleX(), imageTransform.getScaleY())); + Rectangle2D rectangle = new Rectangle2D.Float(0, 0, width, height); + Paint awtPaint = new TexturePaint(image, rectangle); awtPaint = applySoftMaskToPaint(awtPaint, softMask); graphics.setPaint(awtPaint); - Rectangle2D unitRect = new Rectangle2D.Float(0, 0, 1, 1); - graphics.fill(at.createTransformedShape(unitRect)); + + AffineTransform originalTransform = graphics.getTransform(); + graphics.transform(imageTransform); + graphics.fill(rectangle); + graphics.setTransform(originalTransform); } else { @@ -1211,12 +1228,7 @@ public class PageDrawer extends PDFGraphicsStreamEngine image = applyTransferFunction(image, transfer); } - int width = image.getWidth(); - int height = image.getHeight(); - imageTransform.scale(1.0 / width, -1.0 / height); - imageTransform.translate(0, -height); - - // PDFBOX-4516, PDFBOX-4527, PDFBOX-4815: + // PDFBOX-4516, PDFBOX-4527, PDFBOX-4815, PDFBOX-4886, PDFBOX-4863: // graphics.drawImage() has terrible quality when scaling down, even when // RenderingHints.VALUE_INTERPOLATION_BICUBIC, VALUE_ALPHA_INTERPOLATION_QUALITY, // VALUE_COLOR_RENDER_QUALITY and VALUE_RENDER_QUALITY are all set. @@ -1225,41 +1237,37 @@ public class PageDrawer extends PDFGraphicsStreamEngine // (partly because the method needs integer parameters), only smaller scalings // will trigger the workaround. Because of the slowness we only do it if the user // expects quality rendering and interpolation. - Matrix m = new Matrix(imageTransform); - float scaleX = Math.abs(m.getScalingFactorX()); - float scaleY = Math.abs(m.getScalingFactorY()); - Image imageToDraw = image; + Matrix imageTransformMatrix = new Matrix(imageTransform); + AffineTransform graphicsTransformA = graphics.getTransform(); + Matrix graphicsTransformMatrix = new Matrix(graphicsTransformA); + float scaleX = Math.abs(imageTransformMatrix.getScalingFactorX() * graphicsTransformMatrix.getScalingFactorX()); + float scaleY = Math.abs(imageTransformMatrix.getScalingFactorY() * graphicsTransformMatrix.getScalingFactorY()); - if ((scaleX < 0.25f || scaleY < 0.25f) && + if ((scaleX < 0.5 || scaleY < 0.5) && RenderingHints.VALUE_RENDER_QUALITY.equals(graphics.getRenderingHint(RenderingHints.KEY_RENDERING)) && RenderingHints.VALUE_INTERPOLATION_BICUBIC.equals(graphics.getRenderingHint(RenderingHints.KEY_INTERPOLATION))) { - // PDFBOX-4516, PDFBOX-4527, PDFBOX-4815: - // graphics.drawImage() has terrible quality when scaling down, even when - // RenderingHints.VALUE_INTERPOLATION_BICUBIC, VALUE_ALPHA_INTERPOLATION_QUALITY, - // VALUE_COLOR_RENDER_QUALITY and VALUE_RENDER_QUALITY are all set. - // A workaround is to get a pre-scaled image with Image.getScaledInstance() - // and then draw that one. To reduce differences in testing - // (partly because the method needs integer parameters), only smaller scalings - // will trigger the workaround. Because of the slowness we only do it if the user - // expects quality rendering and interpolation. int w = Math.round(image.getWidth() * scaleX); int h = Math.round(image.getHeight() * scaleY); - if (w < 1) - { - w = 1; - } - if (h < 1) + if (w < 1 || h < 1) { - h = 1; + graphics.drawImage(image, imageTransform, null); + return; } - imageToDraw = image.getScaledInstance( - w, - h, - Image.SCALE_SMOOTH); - imageTransform.scale(1 / scaleX, 1 / scaleY); // remove the scale + Image imageToDraw = image.getScaledInstance(w, h, Image.SCALE_SMOOTH); + // remove the scale (extracted from w and h, to have it from the rounded values + // hoping to reverse the rounding: without this, we get an horizontal line + // when rendering PDFJS-8860-Pattern-Size1.pdf at 100% ) + imageTransform.scale(1f / w * image.getWidth(), 1f / h * image.getHeight()); + imageTransform.preConcatenate(graphicsTransformA); + graphics.setTransform(new AffineTransform()); + graphics.drawImage(imageToDraw, imageTransform, null); + graphics.setTransform(graphicsTransformA); + } + else + { + graphics.drawImage(image, imageTransform, null); } - graphics.drawImage(imageToDraw, imageTransform, null); } } diff --git a/pdfbox/src/main/java/org/apache/pdfbox/text/LegacyPDFStreamEngine.java b/pdfbox/src/main/java/org/apache/pdfbox/text/LegacyPDFStreamEngine.java index 726d227..eb8b6c0 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/text/LegacyPDFStreamEngine.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/text/LegacyPDFStreamEngine.java @@ -17,8 +17,16 @@ package org.apache.pdfbox.text; import java.io.InputStream; +import java.io.IOException; +import java.util.Map; +import java.util.WeakHashMap; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.fontbox.util.BoundingBox; + import org.apache.pdfbox.contentstream.PDFStreamEngine; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.font.encoding.GlyphList; @@ -31,11 +39,6 @@ import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont; import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType3Font; import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState; - -import java.io.IOException; - -import org.apache.fontbox.ttf.TrueTypeFont; -import org.apache.fontbox.util.BoundingBox; import org.apache.pdfbox.util.Matrix; import org.apache.pdfbox.util.Vector; import org.apache.pdfbox.contentstream.operator.DrawObject; @@ -60,6 +63,7 @@ import org.apache.pdfbox.contentstream.operator.text.SetTextRenderingMode; import org.apache.pdfbox.contentstream.operator.text.SetTextRise; import org.apache.pdfbox.contentstream.operator.text.SetWordSpacing; import org.apache.pdfbox.contentstream.operator.text.ShowText; +import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.pdmodel.font.PDFontDescriptor; /** @@ -80,6 +84,7 @@ class LegacyPDFStreamEngine extends PDFStreamEngine private PDRectangle pageSize; private Matrix translateMatrix; private final GlyphList glyphList; + private final Map<COSDictionary, Float> fontHeightMap = new WeakHashMap<COSDictionary, Float>(); /** * Constructor. @@ -144,7 +149,9 @@ class LegacyPDFStreamEngine extends PDFStreamEngine * written by Ben Litchfield for PDFStreamEngine. */ @Override - protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vector displacement) + protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, + String unicode, + Vector displacement) throws IOException { // @@ -160,48 +167,6 @@ class LegacyPDFStreamEngine extends PDFStreamEngine float horizontalScaling = state.getTextState().getHorizontalScaling() / 100f; Matrix textMatrix = getTextMatrix(); - BoundingBox bbox = font.getBoundingBox(); - if (bbox.getLowerLeftY() < Short.MIN_VALUE) - { - // PDFBOX-2158 and PDFBOX-3130 - // files by Salmat eSolutions / ClibPDF Library - bbox.setLowerLeftY(- (bbox.getLowerLeftY() + 65536)); - } - // 1/2 the bbox is used as the height todo: why? - float glyphHeight = bbox.getHeight() / 2; - - // sometimes the bbox has very high values, but CapHeight is OK - PDFontDescriptor fontDescriptor = font.getFontDescriptor(); - if (fontDescriptor != null) - { - float capHeight = fontDescriptor.getCapHeight(); - if (Float.compare(capHeight, 0) != 0 && - (capHeight < glyphHeight || Float.compare(glyphHeight, 0) == 0)) - { - glyphHeight = capHeight; - } - // PDFBOX-3464, PDFBOX-4480, PDFBOX-4553: - // sometimes even CapHeight has very high value, but Ascent and Descent are ok - float ascent = fontDescriptor.getAscent(); - float descent = fontDescriptor.getDescent(); - if (capHeight > ascent && ascent > 0 && descent < 0 && - ((ascent - descent) / 2 < glyphHeight || Float.compare(glyphHeight, 0) == 0)) - { - glyphHeight = (ascent - descent) / 2; - } - } - - // transformPoint from glyph space -> text space - float height; - if (font instanceof PDType3Font) - { - height = font.getFontMatrix().transformPoint(0, glyphHeight).y; - } - else - { - height = glyphHeight / 1000; - } - float displacementX = displacement.getX(); // the sorting algorithm is based on the width of the character. As the displacement // for vertical characters doesn't provide any suitable value for it, we have to @@ -251,7 +216,13 @@ class LegacyPDFStreamEngine extends PDFStreamEngine // (modified) width and height calculations float dxDisplay = nextX - textRenderingMatrix.getTranslateX(); - float dyDisplay = height * textRenderingMatrix.getScalingFactorY(); + Float fontHeight = fontHeightMap.get(font.getCOSObject()); + if (fontHeight == null) + { + fontHeight = computeFontHeight(font); + fontHeightMap.put(font.getCOSObject(), fontHeight); + } + float dyDisplay = fontHeight * textRenderingMatrix.getScalingFactorY(); // // start of the original method @@ -295,17 +266,17 @@ class LegacyPDFStreamEngine extends PDFStreamEngine float spaceWidthDisplay = spaceWidthText * textRenderingMatrix.getScalingFactorX(); // use our additional glyph list for Unicode mapping - String unicode = font.toUnicode(code, glyphList); + String unicodeMapping = font.toUnicode(code, glyphList); // when there is no Unicode mapping available, Acrobat simply coerces the character code // into Unicode, so we do the same. Subclasses of PDFStreamEngine don't necessarily want // this, which is why we leave it until this point in PDFTextStreamEngine. - if (unicode == null) + if (unicodeMapping == null) { if (font instanceof PDSimpleFont) { char c = (char) code; - unicode = new String(new char[] { c }); + unicodeMapping = new String(new char[] { c }); } else { @@ -331,10 +302,66 @@ class LegacyPDFStreamEngine extends PDFStreamEngine processTextPosition(new TextPosition(pageRotation, pageSize.getWidth(), pageSize.getHeight(), translatedTextRenderingMatrix, nextX, nextY, Math.abs(dyDisplay), dxDisplay, - Math.abs(spaceWidthDisplay), unicode, new int[] { code } , font, fontSize, + Math.abs(spaceWidthDisplay), unicodeMapping, new int[] { code }, font, + fontSize, (int)(fontSize * textMatrix.getScalingFactorX()))); } + /** + * Compute the font height. Override this if you want to use own calculations. + * + * @param font the font. + * @return the font height. + * + * @throws IOException if there is an error while getting the font bounding box. + */ + protected float computeFontHeight(PDFont font) throws IOException + { + BoundingBox bbox = font.getBoundingBox(); + if (bbox.getLowerLeftY() < Short.MIN_VALUE) + { + // PDFBOX-2158 and PDFBOX-3130 + // files by Salmat eSolutions / ClibPDF Library + bbox.setLowerLeftY(- (bbox.getLowerLeftY() + 65536)); + } + // 1/2 the bbox is used as the height todo: why? + float glyphHeight = bbox.getHeight() / 2; + + // sometimes the bbox has very high values, but CapHeight is OK + PDFontDescriptor fontDescriptor = font.getFontDescriptor(); + if (fontDescriptor != null) + { + float capHeight = fontDescriptor.getCapHeight(); + if (Float.compare(capHeight, 0) != 0 && + (capHeight < glyphHeight || Float.compare(glyphHeight, 0) == 0)) + { + glyphHeight = capHeight; + } + // PDFBOX-3464, PDFBOX-4480, PDFBOX-4553: + // sometimes even CapHeight has very high value, but Ascent and Descent are ok + float ascent = fontDescriptor.getAscent(); + float descent = fontDescriptor.getDescent(); + if (capHeight > ascent && ascent > 0 && descent < 0 && + ((ascent - descent) / 2 < glyphHeight || Float.compare(glyphHeight, 0) == 0)) + { + glyphHeight = (ascent - descent) / 2; + } + } + + // transformPoint from glyph space -> text space + float height; + if (font instanceof PDType3Font) + { + height = font.getFontMatrix().transformPoint(0, glyphHeight).y; + } + else + { + height = glyphHeight / 1000; + } + + return height; + } + /** * A method provided as an event interface to allow a subclass to perform some specific * functionality when text needs to be processed. @@ -345,5 +372,4 @@ class LegacyPDFStreamEngine extends PDFStreamEngine { // subclasses can override to provide specific functionality } - } diff --git a/pdfbox/src/main/java/org/apache/pdfbox/text/PDFTextStripper.java b/pdfbox/src/main/java/org/apache/pdfbox/text/PDFTextStripper.java index e3f8452..3202fc7 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/text/PDFTextStripper.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/text/PDFTextStripper.java @@ -16,6 +16,7 @@ */ package org.apache.pdfbox.text; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -216,6 +217,11 @@ public class PDFTextStripper extends LegacyPDFStreamEngine /** * This will return the text of a document. See writeText. <br> * NOTE: The document must not be encrypted when coming into this method. + * + * <p>IMPORTANT: By default, text extraction is done in the same sequence as the text in the PDF page content stream. + * PDF is a graphic format, not a text format, and unlike HTML, it has no requirements that text one on page + * be rendered in a certain order. The order is the one that was determined by the software that created the + * PDF. To get text sorted from left to right and top to botton, use {@link #setSortByPosition(boolean)}. * * @param doc The document to get the text from. * @return The text of the PDF document. @@ -1842,17 +1848,10 @@ public class PDFTextStripper extends LegacyPDFStreamEngine static { String path = "/org/apache/pdfbox/resources/text/BidiMirroring.txt"; - InputStream input = PDFTextStripper.class.getResourceAsStream(path); + InputStream input = new BufferedInputStream(PDFTextStripper.class.getResourceAsStream(path)); try { - if (input != null) - { - parseBidiFile(input); - } - else - { - LOG.warn("Could not find '" + path + "', mirroring char map will be empty: "); - } + parseBidiFile(input); } catch (IOException e) { diff --git a/pdfbox/src/main/java/org/apache/pdfbox/util/Matrix.java b/pdfbox/src/main/java/org/apache/pdfbox/util/Matrix.java index 35e7398..351d58e 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/util/Matrix.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/util/Matrix.java @@ -32,32 +32,42 @@ import org.apache.pdfbox.cos.COSBase; */ public final class Matrix implements Cloneable { - static final float[] DEFAULT_SINGLE = - { - 1,0,0, // a b 0 sx hy 0 note: hx and hy are reversed vs. the PDF spec as we use - 0,1,0, // c d 0 = hx sy 0 AffineTransform's definition x and y shear - 0,0,1 // tx ty 1 tx ty 1 - }; - - private final float[] single; + public static final int SIZE = 9; + private float[] single; + private static final float MAX_FLOAT_VALUE = 3.4028235E38f; /** * Constructor. This produces an identity matrix. */ public Matrix() { - single = new float[DEFAULT_SINGLE.length]; - System.arraycopy(DEFAULT_SINGLE, 0, single, 0, DEFAULT_SINGLE.length); + // a b 0 + // c d 0 + // tx ty 1 + // note: hx and hy are reversed vs.the PDF spec as we use AffineTransform's definition x and y shear + // sx hy 0 + // hx sy 0 + // tx ty 1 + single = new float[] { 1, 0, 0, 0, 1, 0, 0, 0, 1 }; + } + + /** + * Constructor. This produces a matrix with the given array as data. + * The source array is not copied or cloned. + */ + private Matrix(float[] src) + { + single = src; } /** * Creates a matrix from a 6-element (a b c d e f) COS array. * - * @param array + * @param array source array, elements must be or extend COSNumber */ public Matrix(COSArray array) { - single = new float[DEFAULT_SINGLE.length]; + single = new float[SIZE]; single[0] = ((COSNumber)array.getObject(0)).floatValue(); single[1] = ((COSNumber)array.getObject(1)).floatValue(); single[3] = ((COSNumber)array.getObject(2)).floatValue(); @@ -73,6 +83,11 @@ public final class Matrix implements Cloneable * specification. For simple purposes (rotate, scale, translate) it is recommended to use the * static methods below. * + * Produces the following matrix: + * a b 0 + * c d 0 + * e f 1 + * * @see Matrix#getRotateInstance(double, float, float) * @see Matrix#getScaleInstance(float, float) * @see Matrix#getTranslateInstance(float, float) @@ -86,7 +101,7 @@ public final class Matrix implements Cloneable */ public Matrix(float a, float b, float c, float d, float e, float f) { - single = new float[DEFAULT_SINGLE.length]; + single = new float[SIZE]; single[0] = a; single[1] = b; single[3] = c; @@ -98,18 +113,23 @@ public final class Matrix implements Cloneable /** * Creates a matrix with the same elements as the given AffineTransform. - * @param at + * @param at matrix elements will be initialize with the values from this affine transformation, as follows: + * + * scaleX shearY 0 + * shearX scaleY 0 + * transX transY 1 + * */ public Matrix(AffineTransform at) { - single = new float[DEFAULT_SINGLE.length]; - System.arraycopy(DEFAULT_SINGLE, 0, single, 0, DEFAULT_SINGLE.length); + single = new float[SIZE]; single[0] = (float)at.getScaleX(); single[1] = (float)at.getShearY(); single[3] = (float)at.getShearX(); single[4] = (float)at.getScaleY(); single[6] = (float)at.getTranslateX(); single[7] = (float)at.getTranslateY(); + single[8] = 1; } /** @@ -151,7 +171,10 @@ public final class Matrix implements Cloneable @Deprecated public void reset() { - System.arraycopy(DEFAULT_SINGLE, 0, single, 0, DEFAULT_SINGLE.length); + Arrays.fill(single, 0); + single[0] = 1; + single[4] = 1; + single[8] = 1; } /** @@ -268,8 +291,7 @@ public final class Matrix implements Cloneable */ public void translate(Vector vector) { - Matrix m = Matrix.getTranslateInstance(vector.getX(), vector.getY()); - concatenate(m); + concatenate(Matrix.getTranslateInstance(vector.getX(), vector.getY())); } /** @@ -280,8 +302,7 @@ public final class Matrix implements Cloneable */ public void translate(float tx, float ty) { - Matrix m = Matrix.getTranslateInstance(tx, ty); - concatenate(m); + concatenate(Matrix.getTranslateInstance(tx, ty)); } /** @@ -292,8 +313,7 @@ public final class Matrix implements Cloneable */ public void scale(float sx, float sy) { - Matrix m = Matrix.getScaleInstance(sx, sy); - concatenate(m); + concatenate(Matrix.getScaleInstance(sx, sy)); } /** @@ -303,113 +323,81 @@ public final class Matrix implements Cloneable */ public void rotate(double theta) { - Matrix m = Matrix.getRotateInstance(theta, 0, 0); - concatenate(m); + concatenate(Matrix.getRotateInstance(theta, 0, 0)); } /** - * This will take the current matrix and multiply it with a matrix that is passed in. - * - * @param b The matrix to multiply by. + * This method multiplies this Matrix with the specified other Matrix, storing the product in a new instance. It is + * allowed to have (other == this). * - * @return The result of the two multiplied matrices. + * @param other the second operand Matrix in the multiplication; required + * @return the product of the two matrices. */ - public Matrix multiply( Matrix b ) + public Matrix multiply(Matrix other) { - return this.multiply(b, new Matrix()); + return multiply(other, new Matrix()); } /** - * This method multiplies this Matrix with the specified other Matrix, storing the product in the specified - * result Matrix. By reusing Matrix instances like this, multiplication chains can be executed without having - * to create many temporary Matrix objects. - * <p> - * It is allowed to have (other == this) or (result == this) or indeed (other == result) but if this is done, - * the backing float[] matrix values may be copied in order to ensure a correct product. + * This method multiplies this Matrix with the specified other Matrix, storing the product in the specified result + * Matrix. It is allowed to have (other == this) or (result == this) or indeed (other == result). + * + * See {@link #multiply(Matrix)} if you need a version with a single operator. * - * @param other the second operand Matrix in the multiplication - * @param result the Matrix instance into which the result should be stored. If result is null, a new Matrix - * instance is created. - * @return the product of the two matrices. + * @param other the second operand Matrix in the multiplication; required + * @param result the Matrix instance into which the result should be stored. If result is null, a new Matrix instance is + * created. + * @return the result. + * */ + @Deprecated public Matrix multiply( Matrix other, Matrix result ) { + float[] c = result != null && result != other && result != this ? result.single + : new float[SIZE]; + + multiplyArrays(single, other.single, c); + + if (!Matrix.isFinite(c[0]) // + || !Matrix.isFinite(c[1]) // + || !Matrix.isFinite(c[2]) // + || !Matrix.isFinite(c[3]) // + || !Matrix.isFinite(c[4]) // + || !Matrix.isFinite(c[5]) // + || !Matrix.isFinite(c[6]) // + || !Matrix.isFinite(c[7]) // + || !Matrix.isFinite(c[8])) + throw new IllegalArgumentException("Multiplying two matrices produces illegal values"); + if (result == null) { - result = new Matrix(); + return new Matrix(c); } - - if (other != null && other.single != null) + else { - // the operands - float[] thisOperand = this.single; - float[] otherOperand = other.single; - - // We're multiplying 2 sets of floats together to produce a third, but we allow - // any of these float[] instances to be the same objects. - // There is the possibility then to overwrite one of the operands with result values - // and therefore corrupt the result. - - // If either of these operands are the same float[] instance as the result, then - // they need to be copied. - - if (this == result) - { - final float[] thisOrigVals = new float[this.single.length]; - System.arraycopy(this.single, 0, thisOrigVals, 0, this.single.length); - - thisOperand = thisOrigVals; - } - if (other == result) - { - final float[] otherOrigVals = new float[other.single.length]; - System.arraycopy(other.single, 0, otherOrigVals, 0, other.single.length); - - otherOperand = otherOrigVals; - } - - result.single[0] = thisOperand[0] * otherOperand[0] - + thisOperand[1] * otherOperand[3] - + thisOperand[2] * otherOperand[6]; - result.single[1] = thisOperand[0] * otherOperand[1] - + thisOperand[1] * otherOperand[4] - + thisOperand[2] * otherOperand[7]; - result.single[2] = thisOperand[0] * otherOperand[2] - + thisOperand[1] * otherOperand[5] - + thisOperand[2] * otherOperand[8]; - result.single[3] = thisOperand[3] * otherOperand[0] - + thisOperand[4] * otherOperand[3] - + thisOperand[5] * otherOperand[6]; - result.single[4] = thisOperand[3] * otherOperand[1] - + thisOperand[4] * otherOperand[4] - + thisOperand[5] * otherOperand[7]; - result.single[5] = thisOperand[3] * otherOperand[2] - + thisOperand[4] * otherOperand[5] - + thisOperand[5] * otherOperand[8]; - result.single[6] = thisOperand[6] * otherOperand[0] - + thisOperand[7] * otherOperand[3] - + thisOperand[8] * otherOperand[6]; - result.single[7] = thisOperand[6] * otherOperand[1] - + thisOperand[7] * otherOperand[4] - + thisOperand[8] * otherOperand[7]; - result.single[8] = thisOperand[6] * otherOperand[2] - + thisOperand[7] * otherOperand[5] - + thisOperand[8] * otherOperand[8]; + result.single = c; + return result; } - if (Float.isInfinite(result.single[0]) || Float.isNaN(result.single[0]) // - || Float.isInfinite(result.single[1]) || Float.isNaN(result.single[1]) // - || Float.isInfinite(result.single[2]) || Float.isNaN(result.single[2]) // - || Float.isInfinite(result.single[3]) || Float.isNaN(result.single[3]) // - || Float.isInfinite(result.single[4]) || Float.isNaN(result.single[4]) // - || Float.isInfinite(result.single[5]) || Float.isNaN(result.single[5]) // - || Float.isInfinite(result.single[6]) || Float.isNaN(result.single[6]) // - || Float.isInfinite(result.single[7]) || Float.isNaN(result.single[7]) // - || Float.isInfinite(result.single[8]) || Float.isNaN(result.single[8])) - throw new IllegalArgumentException( - "Multiplying two matrices produces illegal values"); - return result; } + private static boolean isFinite(float f) + { + // this is faster than the combination of "isNaN" and "isInfinite" and Float.isFinite isn't available in java 6 + return Math.abs(f) <= MAX_FLOAT_VALUE; + } + + private void multiplyArrays(float[] a, float[] b, float[] c) + { + c[0] = a[0] * b[0] + a[1] * b[3] + a[2] * b[6]; + c[1] = a[0] * b[1] + a[1] * b[4] + a[2] * b[7]; + c[2] = a[0] * b[2] + a[1] * b[5] + a[2] * b[8]; + c[3] = a[3] * b[0] + a[4] * b[3] + a[5] * b[6]; + c[4] = a[3] * b[1] + a[4] * b[4] + a[5] * b[7]; + c[5] = a[3] * b[2] + a[4] * b[5] + a[5] * b[8]; + c[6] = a[6] * b[0] + a[7] * b[3] + a[8] * b[6]; + c[7] = a[6] * b[1] + a[7] * b[4] + a[8] * b[7]; + c[8] = a[6] * b[2] + a[7] * b[5] + a[8] * b[8]; + } /** * Transforms the given point by this matrix. * @@ -481,16 +469,18 @@ public final class Matrix implements Cloneable /** * Convenience method to create a scaled instance. * - * @param sx The xscale operator. - * @param sy The yscale operator. + * Produces the following matrix: + * x 0 0 + * 0 y 0 + * 0 0 1 + * + * @param x The xscale operator. + * @param y The yscale operator. * @return A new matrix with just the x/y scaling */ - public static Matrix getScaleInstance(float sx, float sy) + public static Matrix getScaleInstance(float x, float y) { - Matrix matrix = new Matrix(); - matrix.single[0] = sx; - matrix.single[4] = sy; - return matrix; + return new Matrix(x, 0, 0, y, 0, 0); } /** @@ -511,30 +501,34 @@ public final class Matrix implements Cloneable /** * Convenience method to create a translating instance. * - * @param tx The x translating operator. - * @param ty The y translating operator. + * Produces the following matrix: + * 1 0 0 + * 0 1 0 + * x y 1 + * + * @param x The x translating operator. + * @param y The y translating operator. * @return A new matrix with just the x/y translating. * @deprecated Use {@link #getTranslateInstance} instead. */ @Deprecated - public static Matrix getTranslatingInstance(float tx, float ty) + public static Matrix getTranslatingInstance(float x, float y) { - return getTranslateInstance(tx, ty); + return new Matrix(1, 0, 0, 1, x, y); } /** * Convenience method to create a translating instance. * - * @param tx The x translating operator. - * @param ty The y translating operator. + * Produces the following matrix: 1 0 0 0 1 0 x y 1 + * + * @param x The x translating operator. + * @param y The y translating operator. * @return A new matrix with just the x/y translating. */ - public static Matrix getTranslateInstance(float tx, float ty) + public static Matrix getTranslateInstance(float x, float y) { - Matrix matrix = new Matrix(); - matrix.single[6] = tx; - matrix.single[7] = ty; - return matrix; + return new Matrix(1, 0, 0, 1, x, y); } /** @@ -550,14 +544,7 @@ public final class Matrix implements Cloneable float cosTheta = (float)Math.cos(theta); float sinTheta = (float)Math.sin(theta); - Matrix matrix = new Matrix(); - matrix.single[0] = cosTheta; - matrix.single[1] = sinTheta; - matrix.single[3] = -sinTheta; - matrix.single[4] = cosTheta; - matrix.single[6] = tx; - matrix.single[7] = ty; - return matrix; + return new Matrix(cosTheta, sinTheta, -sinTheta, cosTheta, tx, ty); } /** @@ -568,9 +555,7 @@ public final class Matrix implements Cloneable */ public static Matrix concatenate(Matrix a, Matrix b) { - Matrix copy = a.clone(); - copy.concatenate(b); - return copy; + return b.multiply(a); } /** @@ -580,9 +565,7 @@ public final class Matrix implements Cloneable @Override public Matrix clone() { - Matrix clone = new Matrix(); - System.arraycopy( single, 0, clone.single, 0, 9 ); - return clone; + return new Matrix(single.clone()); } /** @@ -592,8 +575,6 @@ public final class Matrix implements Cloneable */ public float getScalingFactorX() { - float xScale = single[0]; - /** * BM: if the trm is rotated, the calculation is a little more complicated * @@ -611,12 +592,12 @@ public final class Matrix implements Cloneable * sqrt(x2) = * abs(x) */ - if( !(single[1]==0.0f && single[3]==0.0f) ) + if (single[1] != 0.0f) { - xScale = (float)Math.sqrt(Math.pow(single[0], 2)+ + return (float) Math.sqrt(Math.pow(single[0], 2) + Math.pow(single[1], 2)); } - return xScale; + return single[0]; } /** @@ -626,13 +607,12 @@ public final class Matrix implements Cloneable */ public float getScalingFactorY() { - float yScale = single[4]; - if( !(single[1]==0.0f && single[3]==0.0f) ) + if (single[3] != 0.0f) { - yScale = (float)Math.sqrt(Math.pow(single[3], 2)+ + return (float) Math.sqrt(Math.pow(single[3], 2) + Math.pow(single[4], 2)); } - return yScale; + return single[4]; } /** diff --git a/pdfbox/src/main/java/org/apache/pdfbox/util/Version.java b/pdfbox/src/main/java/org/apache/pdfbox/util/Version.java index f914167..f29271b 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/util/Version.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/util/Version.java @@ -17,6 +17,7 @@ package org.apache.pdfbox.util; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Properties; @@ -44,11 +45,7 @@ public final class Version InputStream is = null; try { - is = Version.class.getResourceAsStream(PDFBOX_VERSION_PROPERTIES); - if (is == null) - { - return null; - } + is = new BufferedInputStream(Version.class.getResourceAsStream(PDFBOX_VERSION_PROPERTIES)); Properties properties = new Properties(); properties.load(is); return properties.getProperty("pdfbox.version", null); diff --git a/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSFloat.java b/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSFloat.java index 3f4f497..d72e288 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSFloat.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSFloat.java @@ -204,6 +204,66 @@ public class TestCOSFloat extends TestCOSNumber } + public void testVerySmallValues() throws IOException + { + double smallValue = Float.MIN_VALUE / 10d; + + assertEquals("Test must be performed with a value smaller than Float.MIN_VALUE.", -1, + Double.compare(smallValue, Float.MIN_VALUE)); + + // 1.4012984643248171E-46 + String asString = String.valueOf(smallValue); + COSFloat cosFloat = new COSFloat(asString); + assertEquals(0.0f, cosFloat.floatValue()); + + // 0.00000000000000000000000000000000000000000000014012984643248171 + asString = new BigDecimal(asString).toPlainString(); + cosFloat = new COSFloat(asString); + assertEquals(0.0f, cosFloat.floatValue()); + + smallValue *= -1; + + // -1.4012984643248171E-46 + asString = String.valueOf(smallValue); + cosFloat = new COSFloat(asString); + assertEquals(0.0f, cosFloat.floatValue()); + + // -0.00000000000000000000000000000000000000000000014012984643248171 + asString = new BigDecimal(asString).toPlainString(); + cosFloat = new COSFloat(asString); + assertEquals(0.0f, cosFloat.floatValue()); + } + + public void testVeryLargeValues() throws IOException + { + double largeValue = Float.MAX_VALUE * 10d; + + assertEquals("Test must be performed with a value larger than Float.MAX_VALUE.", 1, + Double.compare(largeValue, Float.MIN_VALUE)); + + // 1.4012984643248171E-46 + String asString = String.valueOf(largeValue); + COSFloat cosFloat = new COSFloat(asString); + assertEquals(Float.MAX_VALUE, cosFloat.floatValue()); + + // 0.00000000000000000000000000000000000000000000014012984643248171 + asString = new BigDecimal(asString).toPlainString(); + cosFloat = new COSFloat(asString); + assertEquals(Float.MAX_VALUE, cosFloat.floatValue()); + + largeValue *= -1; + + // -1.4012984643248171E-46 + asString = String.valueOf(largeValue); + cosFloat = new COSFloat(asString); + assertEquals(-Float.MAX_VALUE, cosFloat.floatValue()); + + // -0.00000000000000000000000000000000000000000000014012984643248171 + asString = new BigDecimal(asString).toPlainString(); + cosFloat = new COSFloat(asString); + assertEquals(-Float.MAX_VALUE, cosFloat.floatValue()); + } + @Override public void testIntValue() { diff --git a/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSNumber.java b/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSNumber.java index 86d4b08..181379a 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSNumber.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSNumber.java @@ -78,11 +78,37 @@ public abstract class TestCOSNumber extends TestCOSBase { // PASS } - + // PDFBOX-2569: some numbers start with "+" + assertEquals(COSNumber.get("1"), COSNumber.get("+1")); + assertEquals(COSNumber.get("123"), COSNumber.get("+123")); } catch (IOException e) { fail("Failed to convert a number " + e.getMessage()); } } + + /** + * PDFBOX-4895: large number, too big for a long leads to a null value. + * + * @throws IOException + */ + public void testLargeNumber() throws IOException + { + assertNull(COSNumber.get("18446744073307448448")); + assertNull(COSNumber.get("-18446744073307448448")); + } + + public void testInvalidNumber() + { + try + { + COSNumber.get("18446744073307F448448"); + fail("Was expecting an IOException"); + } + catch (IOException e) + { + } + } + } diff --git a/pdfbox/src/test/java/org/apache/pdfbox/filter/TestFilters.java b/pdfbox/src/test/java/org/apache/pdfbox/filter/TestFilters.java index 6fdb57f..a4f25e6 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/filter/TestFilters.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/filter/TestFilters.java @@ -130,16 +130,16 @@ public class TestFilters extends TestCase } /** - * This will test the LZW filter with the sequence that failed in PDFBOX-1777. + * This will test the LZW filter with the sequence that failed in PDFBOX-1977. * To check that the test itself is legit, revert LZWFilter.java to rev 1571801, * which should fail this test. * * @throws IOException */ - public void testPDFBOX1777() throws IOException + public void testPDFBOX1977() throws IOException { Filter lzwFilter = FilterFactory.INSTANCE.getFilter(COSName.LZW_DECODE); - byte[] byteArray = IOUtils.toByteArray(this.getClass().getResourceAsStream("PDFBOX-1777.bin")); + byte[] byteArray = IOUtils.toByteArray(this.getClass().getResourceAsStream("PDFBOX-1977.bin")); checkEncodeDecode(lzwFilter, byteArray); } diff --git a/pdfbox/src/test/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParserTest.java b/pdfbox/src/test/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParserTest.java new file mode 100644 index 0000000..7b984b6 --- /dev/null +++ b/pdfbox/src/test/java/org/apache/pdfbox/pdfparser/PDFObjectStreamParserTest.java @@ -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.apache.pdfbox.pdfparser; + +import java.io.IOException; +import java.io.OutputStream; +import org.apache.pdfbox.cos.COSBoolean; +import org.apache.pdfbox.cos.COSInteger; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSStream; +import org.junit.Assert; +import org.junit.Test; + +/** + * Test for PDFObjectStreamParser. + */ +public class PDFObjectStreamParserTest +{ + @Test + public void testOffsetParsing() throws IOException + { + COSStream stream = new COSStream(); + stream.setItem(COSName.N, COSInteger.ONE); + stream.setItem(COSName.FIRST, COSInteger.ZERO); + OutputStream outputStream = stream.createOutputStream(); + outputStream.write("0 7 -1 true".getBytes()); + outputStream.close(); + PDFObjectStreamParser objectStreamParser = new PDFObjectStreamParser(stream, null); + objectStreamParser.parse(); + Assert.assertEquals(COSBoolean.TRUE, objectStreamParser.getObjects().get(0).getObject()); + } +} diff --git a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/COSArrayListTest.java b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/COSArrayListTest.java index d2ba400..095b2d8 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/COSArrayListTest.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/COSArrayListTest.java @@ -46,7 +46,7 @@ public class COSArrayListTest { // next two entries are to be used for comparison with // COSArrayList behaviour in order to ensure that the - // intented object is now at the correct position. + // intended object is now at the correct position. // Will also be used for Collection/Array based setting // and comparison static List<PDAnnotation> tbcAnnotationsList; @@ -63,7 +63,7 @@ public class COSArrayListTest { private static final File OUT_DIR = new File("target/test-output/pdmodel/common"); /* - * Create thre new different annotations an add them to the Java List/Array as + * Create three new different annotations and add them to the Java List/Array as * well as PDFBox List/Array implementations. */ @Before diff --git a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestPDNumberTreeNode.java b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestPDNumberTreeNode.java index fa72e78..f34e742 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestPDNumberTreeNode.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/common/TestPDNumberTreeNode.java @@ -22,7 +22,6 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; import junit.framework.TestCase; -import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSInteger; import org.junit.Assert; @@ -56,7 +55,7 @@ public class TestPDNumberTreeNode extends TestCase } @Override - public COSBase getCOSObject() + public COSInteger getCOSObject() { return COSInteger.get( value ); } diff --git a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDFontTest.java b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDFontTest.java index bc90f1a..e0fa39e 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDFontTest.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDFontTest.java @@ -24,10 +24,15 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import org.apache.fontbox.ttf.TTFParser; +import org.apache.fontbox.ttf.TrueTypeCollection; import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.fontbox.util.autodetect.FontFileFinder; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; @@ -220,6 +225,53 @@ public class PDFontTest } } + @Test + public void testFullEmbeddingTTC() throws IOException + { + FontFileFinder fff = new FontFileFinder(); + TrueTypeCollection ttc = null; + for (URI uri : fff.find()) + { + if (uri.getPath().endsWith(".ttc")) + { + File file = new File(uri); + System.out.println("TrueType collection file: " + file); + ttc = new TrueTypeCollection(file); + break; + } + } + if (ttc == null) + { + System.out.println("testFullEmbeddingTTC skipped, no .ttc files available"); + return; + } + + final List<String> names = new ArrayList<String>(); + ttc.processAllFonts(new TrueTypeCollection.TrueTypeFontProcessor() + { + @Override + public void process(TrueTypeFont ttf) throws IOException + { + System.out.println("TrueType font in collection: " + ttf.getName()); + names.add(ttf.getName()); + } + }); + + TrueTypeFont ttf = ttc.getFontByName(names.get(0)); // take the first one + System.out.println("TrueType font used for test: " + ttf.getName()); + + try + { + PDType0Font.load(new PDDocument(), ttf, false); + } + catch (IOException ex) + { + Assert.assertEquals("Full embedding of TrueType font collections not supported", ex.getMessage()); + return; + } + Assert.fail("should have thrown IOException"); + } + private void testPDFBox3826checkFonts(byte[] byteArray, File fontFile) throws IOException { PDDocument doc = PDDocument.load(byteArray); diff --git a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java index f7cc786..4e9c7e2 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java @@ -17,9 +17,19 @@ package org.apache.pdfbox.pdmodel.interactive.form; +import static org.junit.Assert.assertEquals; + import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSNumber; +import org.apache.pdfbox.cos.COSString; +import org.apache.pdfbox.pdfparser.PDFStreamParser; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import org.apache.pdfbox.rendering.TestPDFToImage; import org.junit.After; import org.junit.Before; @@ -94,6 +104,101 @@ public class MultilineFieldsTest System.err.println ("Rendering of " + file + " failed or is not identical to expected rendering in " + IN_DIR + " directory"); } } + + // Test for PDFBOX-3812 + @Test + public void testMultilineAuto() throws IOException + { + PDDocument document = PDDocument.load(new File(IN_DIR, "PDFBOX3812-acrobat-multiline-auto.pdf")); + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + + // Get and store the field sizes in the original PDF + PDTextField fieldMultiline = (PDTextField) acroForm.getField("Multiline"); + float fontSizeMultiline = getFontSizeFromAppearanceStream(fieldMultiline); + + PDTextField fieldSingleline = (PDTextField) acroForm.getField("Singleline"); + float fontSizeSingleline = getFontSizeFromAppearanceStream(fieldSingleline); + + PDTextField fieldMultilineAutoscale = (PDTextField) acroForm.getField("MultilineAutoscale"); + float fontSizeMultilineAutoscale = getFontSizeFromAppearanceStream(fieldMultilineAutoscale); + + PDTextField fieldSinglelineAutoscale = (PDTextField) acroForm.getField("SinglelineAutoscale"); + float fontSizeSinglelineAutoscale = getFontSizeFromAppearanceStream(fieldSinglelineAutoscale); + + fieldMultiline.setValue("Multiline - Fixed"); + fieldSingleline.setValue("Singleline - Fixed"); + fieldMultilineAutoscale.setValue("Multiline - auto"); + fieldSinglelineAutoscale.setValue("Singleline - auto"); + + assertEquals(fontSizeMultiline, getFontSizeFromAppearanceStream(fieldMultiline), 0.001f); + assertEquals(fontSizeSingleline, getFontSizeFromAppearanceStream(fieldSingleline), 0.001f); + assertEquals(fontSizeMultilineAutoscale, getFontSizeFromAppearanceStream(fieldMultilineAutoscale), 0.001f); + assertEquals(fontSizeSinglelineAutoscale, getFontSizeFromAppearanceStream(fieldSinglelineAutoscale), 0.025f); + } + + // Test for PDFBOX-3812 + @Test + public void testMultilineBreak() throws IOException + { + final String TEST_PDF = "PDFBOX-3835-input-acrobat-wrap.pdf"; + PDDocument document = PDDocument.load(new File(IN_DIR, TEST_PDF)); + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + + // Get and store the field sizes in the original PDF + PDTextField fieldInput = (PDTextField) acroForm.getField("filled"); + String fieldValue = fieldInput.getValue(); + List<String> acrobatLines = getTextLinesFromAppearanceStream(fieldInput); + fieldInput.setValue(fieldValue); + List<String> pdfboxLines = getTextLinesFromAppearanceStream(fieldInput); + assertEquals("Number of lines generated by PDFBox shall match Acrobat", acrobatLines.size(),pdfboxLines.size()); + for (int i = 0; i < acrobatLines.size(); i++) + { + assertEquals("Number of characters per lines generated by PDFBox shall match Acrobat", acrobatLines.get(i).length(), pdfboxLines.get(i).length()); + } + document.close(); + } + + private float getFontSizeFromAppearanceStream(PDField field) throws IOException + { + PDAnnotationWidget widget = field.getWidgets().get(0); + PDFStreamParser parser = new PDFStreamParser(widget.getNormalAppearanceStream()); + + Object token = parser.parseNextToken(); + + while (token != null) + { + if (token instanceof COSName && ((COSName) token).getName().equals("Helv")) + { + token = parser.parseNextToken(); + if (token != null && token instanceof COSNumber) + { + return ((COSNumber) token).floatValue(); + } + } + token = parser.parseNextToken(); + } + return 0; + } + + private List<String> getTextLinesFromAppearanceStream(PDField field) throws IOException + { + PDAnnotationWidget widget = field.getWidgets().get(0); + PDFStreamParser parser = new PDFStreamParser(widget.getNormalAppearanceStream()); + + Object token = parser.parseNextToken(); + + List<String> lines = new ArrayList<String>(); + + while (token != null) + { + if (token instanceof COSString) + { + lines.add(((COSString) token).getString()); + } + token = parser.parseNextToken(); + } + return lines; + } @After public void tearDown() throws IOException diff --git a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java index 88710c9..b5c094a 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java @@ -273,6 +273,20 @@ public class PDAcroFormFlattenTest flattenAndCompare(sourceUrl, targetFileName); } + /** + * PDFBOX-4889: appearance streams with empty /BBox. + * + * @throws IOException + */ + @Test + public void testFlattenPDFBox4889() throws IOException + { + String sourceUrl = "https://issues.apache.org/jira/secure/attachment/13005793/f1040sb%20test.pdf"; + String targetFileName = "PDFBOX-4889.pdf"; + + flattenAndCompare(sourceUrl, targetFileName); + } + /* * Flatten and compare with generated image samples. */ diff --git a/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestPDFToImage.java b/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestPDFToImage.java index 4c17e2d..df01a3c 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestPDFToImage.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestPDFToImage.java @@ -325,6 +325,7 @@ public class TestPDFToImage LOG.info("*** TEST OK *** for file: " + inFile.getName()); LOG.info("Deleting: " + outFile.getName()); outFile.delete(); + outFile.deleteOnExit(); } } else @@ -332,6 +333,7 @@ public class TestPDFToImage LOG.info("*** TEST OK *** for file: " + inFile.getName()); LOG.info("Deleting: " + outFile.getName()); outFile.delete(); + outFile.deleteOnExit(); } } } diff --git a/pdfbox/src/test/java/org/apache/pdfbox/text/TestTextStripper.java b/pdfbox/src/test/java/org/apache/pdfbox/text/TestTextStripper.java index 77718e6..614dbf8 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/text/TestTextStripper.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/text/TestTextStripper.java @@ -41,12 +41,17 @@ import java.util.List; import junit.framework.Test; import junit.framework.TestCase; +import static junit.framework.TestCase.assertFalse; import junit.framework.TestSuite; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.fontbox.util.BoundingBox; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.TestPDPageTree; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDFontDescriptor; +import org.apache.pdfbox.pdmodel.font.PDType3Font; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; @@ -249,7 +254,6 @@ public class TestTextStripper extends TestCase } } - //System.out.println(" " + inFile + (bSort ? " (sorted)" : "")); PDDocument document = PDDocument.load(inFile); try { @@ -312,116 +316,117 @@ public class TestTextStripper extends TestCase return; } - boolean localFail = false; + compareResult(expectedFile, outFile, inFile, bSort, diffFile); + } + finally + { + document.close(); + } + } - LineNumberReader expectedReader = + private void compareResult(File expectedFile, File outFile, File inFile, boolean bSort, File diffFile) + throws IOException + { + boolean localFail = false; + + LineNumberReader expectedReader = new LineNumberReader(new InputStreamReader(new FileInputStream(expectedFile), ENCODING)); - LineNumberReader actualReader = + LineNumberReader actualReader = new LineNumberReader(new InputStreamReader(new FileInputStream(outFile), ENCODING)); - - while (true) + + while (true) + { + String expectedLine = expectedReader.readLine(); + while( expectedLine != null && expectedLine.trim().length() == 0 ) { - String expectedLine = expectedReader.readLine(); - while( expectedLine != null && expectedLine.trim().length() == 0 ) - { - expectedLine = expectedReader.readLine(); - } - String actualLine = actualReader.readLine(); - while( actualLine != null && actualLine.trim().length() == 0 ) + expectedLine = expectedReader.readLine(); + } + String actualLine = actualReader.readLine(); + while( actualLine != null && actualLine.trim().length() == 0 ) + { + actualLine = actualReader.readLine(); + } + if (!stringsEqual(expectedLine, actualLine)) + { + this.bFail = true; + localFail = true; + log.error("FAILURE: Line mismatch for file " + inFile.getName() + + " (sort = "+bSort+")" + + " at expected line: " + expectedReader.getLineNumber() + + " at actual line: " + actualReader.getLineNumber() + + "\nexpected line was: \"" + expectedLine + "\"" + + "\nactual line was: \"" + actualLine + "\"" + "\n"); + + //lets report all lines, even though this might produce some verbose logging + //break; + } + + if (expectedLine == null || actualLine == null) + { + break; + } + } + expectedReader.close(); + actualReader.close(); + if (!localFail) + { + outFile.delete(); + } + else + { + // https://code.google.com/p/java-diff-utils/wiki/SampleUsage + List<String> original = fileToLines(expectedFile); + List<String> revised = fileToLines(outFile); + + // Compute diff. Get the Patch object. Patch is the container for computed deltas. + Patch patch = DiffUtils.diff(original, revised); + + PrintStream diffPS = new PrintStream(diffFile, ENCODING); + for (Object delta : patch.getDeltas()) + { + if (delta instanceof ChangeDelta) { - actualLine = actualReader.readLine(); + ChangeDelta cdelta = (ChangeDelta) delta; + diffPS.println("Org: " + cdelta.getOriginal()); + diffPS.println("New: " + cdelta.getRevised()); + diffPS.println(); } - if (!stringsEqual(expectedLine, actualLine)) + else if (delta instanceof DeleteDelta) { - this.bFail = true; - localFail = true; - log.error("FAILURE: Line mismatch for file " + inFile.getName() + - " (sort = "+bSort+")" + - " at expected line: " + expectedReader.getLineNumber() + - " at actual line: " + actualReader.getLineNumber() + - "\nexpected line was: \"" + expectedLine + "\"" + - "\nactual line was: \"" + actualLine + "\"" + "\n"); - - //lets report all lines, even though this might produce some verbose logging - //break; + DeleteDelta ddelta = (DeleteDelta) delta; + diffPS.println("Org: " + ddelta.getOriginal()); + diffPS.println("New: " + ddelta.getRevised()); + diffPS.println(); } - - if( expectedLine == null || actualLine==null) + else if (delta instanceof InsertDelta) { - break; + InsertDelta idelta = (InsertDelta) delta; + diffPS.println("Org: " + idelta.getOriginal()); + diffPS.println("New: " + idelta.getRevised()); + diffPS.println(); } - } - expectedReader.close(); - actualReader.close(); - if (!localFail) - { - outFile.delete(); - } - else - { - // https://code.google.com/p/java-diff-utils/wiki/SampleUsage - List<String> original = fileToLines(expectedFile); - List<String> revised = fileToLines(outFile); - - // Compute diff. Get the Patch object. Patch is the container for computed deltas. - Patch patch = DiffUtils.diff(original, revised); - - PrintStream diffPS = new PrintStream(diffFile, ENCODING); - for (Object delta : patch.getDeltas()) + else { - if (delta instanceof ChangeDelta) - { - ChangeDelta cdelta = (ChangeDelta) delta; - diffPS.println("Org: " + cdelta.getOriginal()); - diffPS.println("New: " + cdelta.getRevised()); - diffPS.println(); - } - else if (delta instanceof DeleteDelta) - { - DeleteDelta ddelta = (DeleteDelta) delta; - diffPS.println("Org: " + ddelta.getOriginal()); - diffPS.println("New: " + ddelta.getRevised()); - diffPS.println(); - } - else if (delta instanceof InsertDelta) - { - InsertDelta idelta = (InsertDelta) delta; - diffPS.println("Org: " + idelta.getOriginal()); - diffPS.println("New: " + idelta.getRevised()); - diffPS.println(); - } - else - { - diffPS.println(delta); - } + diffPS.println(delta); } - diffPS.close(); } - } - finally - { - document.close(); + diffPS.close(); } } // Helper method for get the file content - private static List<String> fileToLines(File file) + private static List<String> fileToLines(File file) throws IOException { List<String> lines = new LinkedList<String>(); - String line = ""; - try - { - BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), ENCODING)); - while ((line = in.readLine()) != null) - { - lines.add(line); - } - in.close(); - } - catch (IOException e) + String line; + + BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), ENCODING)); + while ((line = in.readLine()) != null) { - e.printStackTrace(); + lines.add(line); } + in.close(); + return lines; } @@ -447,6 +452,7 @@ public class TestTextStripper extends TestCase * must be empty. * * @throws IOException + * @throws URISyntaxException */ public void testStripByOutlineItems() throws IOException, URISyntaxException { @@ -610,6 +616,94 @@ public class TestTextStripper extends TestCase } } + public void testTabula() throws IOException + { + File pdfFile = new File("src/test/resources/input", "eu-001.pdf"); + File outFile = new File("target/test-output", "eu-001.pdf-tabula.txt"); + File expectedOutFile = new File("src/test/resources/input", "eu-001.pdf-tabula.txt"); + File diffFile = new File("target/test-output", "eu-001.pdf-tabula-diff.txt"); + PDDocument tabulaDocument = PDDocument.load(pdfFile); + PDFTextStripper tabulaStripper = new PDFTabulaTextStripper(); + + OutputStream os = new FileOutputStream(outFile); + + os.write(0xEF); + os.write(0xBB); + os.write(0xBF); + + Writer writer = new BufferedWriter(new OutputStreamWriter(os, ENCODING)); + try + { + tabulaStripper.writeText(tabulaDocument, writer); + } + finally + { + writer.close(); + } + + os.close(); + + compareResult(expectedOutFile, outFile, pdfFile, false, diffFile); + + assertFalse(bFail); + } + + private class PDFTabulaTextStripper extends PDFTextStripper + { + PDFTabulaTextStripper() throws IOException + { + // empty + } + + @Override + protected float computeFontHeight(PDFont font) throws IOException + { + BoundingBox bbox = font.getBoundingBox(); + if (bbox.getLowerLeftY() < Short.MIN_VALUE) + { + // PDFBOX-2158 and PDFBOX-3130 + // files by Salmat eSolutions / ClibPDF Library + bbox.setLowerLeftY(-(bbox.getLowerLeftY() + 65536)); + } + // 1/2 the bbox is used as the height todo: why? + float glyphHeight = bbox.getHeight() / 2; + + // sometimes the bbox has very high values, but CapHeight is OK + PDFontDescriptor fontDescriptor = font.getFontDescriptor(); + if (fontDescriptor != null) + { + float capHeight = fontDescriptor.getCapHeight(); + if (Float.compare(capHeight, 0) != 0 + && (capHeight < glyphHeight || Float.compare(glyphHeight, 0) == 0)) + { + glyphHeight = capHeight; + } + // PDFBOX-3464, PDFBOX-448: + // sometimes even CapHeight has very high value, but Ascent and Descent are ok + float ascent = fontDescriptor.getAscent(); + float descent = fontDescriptor.getDescent(); + if (ascent > 0 && descent < 0 + && ((ascent - descent) / 2 < glyphHeight || Float.compare(glyphHeight, 0) == 0)) + { + glyphHeight = (ascent - descent) / 2; + } + } + + // transformPoint from glyph space -> text space + float height; + if (font instanceof PDType3Font) + { + height = font.getFontMatrix().transformPoint(0, glyphHeight).y; + } + else + { + height = glyphHeight / 1000; + } + + return height; + } + } + /** * Set the tests in the suite for this test class. * diff --git a/pdfbox/src/test/java/org/apache/pdfbox/util/MatrixTest.java b/pdfbox/src/test/java/org/apache/pdfbox/util/MatrixTest.java index 144f8b7..db1e2fb 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/util/MatrixTest.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/util/MatrixTest.java @@ -17,6 +17,7 @@ package org.apache.pdfbox.util; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSFloat; +import org.apache.pdfbox.cos.COSName; import org.junit.Test; import static org.junit.Assert.*; @@ -40,7 +41,105 @@ public class MatrixTest } @Test - public void testMultiplication() throws Exception + public void testGetScalingFactor() + { + // check scaling factor of an initial matrix + Matrix m1 = new Matrix(); + assertEquals(1, m1.getScalingFactorX(), 0); + assertEquals(1, m1.getScalingFactorY(), 0); + + // check scaling factor of an initial matrix + Matrix m2 = new Matrix(2, 4, 4, 2, 0, 0); + assertEquals((float) Math.sqrt(20), m2.getScalingFactorX(), 0); + assertEquals((float) Math.sqrt(20), m2.getScalingFactorY(), 0); + } + + @Test + public void testCreateMatrixUsingInvalidInput() + { + // anything but a COSArray is invalid and leads to an initial matrix + Matrix createMatrix = Matrix.createMatrix(COSName.A); + assertMatrixIsPristine(createMatrix); + + // a COSArray with fewer than 6 entries leads to an initial matrix + COSArray cosArray = new COSArray(); + cosArray.add(COSName.A); + createMatrix = Matrix.createMatrix(cosArray); + assertMatrixIsPristine(createMatrix); + + // a COSArray containing other kind of objects than COSNumber leads to an initial matrix + cosArray = new COSArray(); + for (int i = 0; i < 6; i++) + { + cosArray.add(COSName.A); + } + createMatrix = Matrix.createMatrix(cosArray); + assertMatrixIsPristine(createMatrix); + } + + @Test + public void testMultiplication() + { + // These matrices will not change - we use it to drive the various multiplications. + final Matrix const1 = new Matrix(); + final Matrix const2 = new Matrix(); + + // Create matrix with values + // [ 0, 1, 2 + // 1, 2, 3 + // 2, 3, 4] + for (int x = 0; x < 3; x++) + { + for (int y = 0; y < 3; y++) + { + const1.setValue(x, y, x + y); + const2.setValue(x, y, 8 + x + y); + } + } + + float[] m1MultipliedByM1 = new float[] { 5, 8, 11, 8, 14, 20, 11, 20, 29 }; + float[] m1MultipliedByM2 = new float[] { 29, 32, 35, 56, 62, 68, 83, 92, 101 }; + float[] m2MultipliedByM1 = new float[] { 29, 56, 83, 32, 62, 92, 35, 68, 101 }; + + Matrix var1 = const1.clone(); + Matrix var2 = const2.clone(); + + // Multiply two matrices together producing a new result matrix. + Matrix result = var1.multiply(var2); + assertEquals(const1, var1); + assertEquals(const2, var2); + assertMatrixValuesEqualTo(m1MultipliedByM2, result); + + // Multiply two matrices together with the result being written to a third matrix + // (Any existing values there will be overwritten). + result = var1.multiply(var2); + assertEquals(const1, var1); + assertEquals(const2, var2); + assertMatrixValuesEqualTo(m1MultipliedByM2, result); + + // Multiply two matrices together with the result being written into 'this' matrix + var1 = const1.clone(); + var2 = const2.clone(); + var1.concatenate(var2); + assertEquals(const2, var2); + assertMatrixValuesEqualTo(m2MultipliedByM1, var1); + + var1 = const1.clone(); + var2 = const2.clone(); + result = Matrix.concatenate(var1, var2); + assertEquals(const1, var1); + assertEquals(const2, var2); + assertMatrixValuesEqualTo(m2MultipliedByM1, result); + + // Multiply the same matrix with itself with the result being written into 'this' matrix + var1 = const1.clone(); + result = var1.multiply(var1); + assertEquals(const1, var1); + assertMatrixValuesEqualTo(m1MultipliedByM1, result); + } + + @Test + public void testOldMultiplication() throws Exception { // This matrix will not change - we use it to drive the various multiplications. final Matrix testMatrix = new Matrix(); @@ -158,6 +257,64 @@ public class MatrixTest } + @Test + public void testGetValues() + { + Matrix m = new Matrix(2, 4, 4, 2, 15, 30); + float[][] values = m.getValues(); + assertEquals(2, values[0][0], 0); + assertEquals(4, values[0][1], 0); + assertEquals(0, values[0][2], 0); + assertEquals(4, values[1][0], 0); + assertEquals(2, values[1][1], 0); + assertEquals(0, values[1][2], 0); + assertEquals(15, values[2][0], 0); + assertEquals(30, values[2][1], 0); + assertEquals(1, values[2][2], 0); + } + + @Test + public void testScaling() + { + Matrix m = new Matrix(2, 4, 4, 2, 15, 30); + m.scale(2, 3); + // first row, multiplication with 2 + assertEquals(4, m.getValue(0, 0), 0); + assertEquals(8, m.getValue(0, 1), 0); + assertEquals(0, m.getValue(0, 2), 0); + + // second row, multiplication with 3 + assertEquals(12, m.getValue(1, 0), 0); + assertEquals(6, m.getValue(1, 1), 0); + assertEquals(0, m.getValue(1, 2), 0); + + // third row, no changes at all + assertEquals(15, m.getValue(2, 0), 0); + assertEquals(30, m.getValue(2, 1), 0); + assertEquals(1, m.getValue(2, 2), 0); + } + + @Test + public void testTranslation() + { + Matrix m = new Matrix(2, 4, 4, 2, 15, 30); + m.translate(2, 3); + // first row, no changes at all + assertEquals(2, m.getValue(0, 0), 0); + assertEquals(4, m.getValue(0, 1), 0); + assertEquals(0, m.getValue(0, 2), 0); + + // second row, no changes at all + assertEquals(4, m.getValue(1, 0), 0); + assertEquals(2, m.getValue(1, 1), 0); + assertEquals(0, m.getValue(1, 2), 0); + + // third row, translated values + assertEquals(31, m.getValue(2, 0), 0); + assertEquals(44, m.getValue(2, 1), 0); + assertEquals(1, m.getValue(2, 2), 0); + } + /** * This method asserts that the matrix values for the given {@link Matrix} object are equal to the pristine, or * original, values. @@ -190,4 +347,19 @@ public class MatrixTest } } + //Uncomment annotation to run the test + // @Test + public void testMultiplicationPerformance() { + long start = System.currentTimeMillis(); + Matrix c; + Matrix d; + for (int i=0; i<100000000; i++) { + c = new Matrix(15, 3, 235, 55, 422, 1); + d = new Matrix(45, 345, 23, 551, 66, 832); + c.multiply(d); + c.concatenate(d); + } + long stop = System.currentTimeMillis(); + System.out.println("Matrix multiplication took " + (stop - start) + "ms."); + } } diff --git a/pdfbox/src/test/java/org/apache/pdfbox/util/TestDateUtil.java b/pdfbox/src/test/java/org/apache/pdfbox/util/TestDateUtil.java index 90a5ed3..602761d 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/util/TestDateUtil.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/util/TestDateUtil.java @@ -167,6 +167,10 @@ public class TestDateUtil extends TestCase // PDFBOX-1219 checkParse(2001, 1,31,10,33, 0, +1, 0, "2001-01-31T10:33+01:00 "); + + // Same with milliseconds + checkParse(2001, 1,31,10,33, 0, +1, 0, "2001-01-31T10:33.123+01:00"); + // PDFBOX-465 checkParse(2002, 5,12, 9,47, 0, 0, 0, "9:47 5/12/2002"); // PDFBOX-465 @@ -215,6 +219,7 @@ public class TestDateUtil extends TestCase checkParse(2000, 2,29, 0, 0, 0, 0, 0, "2000 Feb 29"); // valid date checkParse(2000, 2,29, 0, 0, 0,+11, 0, " 2000 Feb 29 GMT + 11:00"); // valid date + checkParse(2000, 2,29, 0, 0, 0,+11, 0, " 2000 Feb 29 UTC + 11:00"); // valid date checkParse(BAD, 0, 0, 0, 0, 0, 0, 0, "2100 Feb 29 GMT+11"); // invalid date checkParse(2012, 2,29, 0, 0, 0,+11, 0, "2012 Feb 29 GMT+11"); // valid date checkParse(BAD, 0, 0, 0, 0, 0, 0, 0, "2012 Feb 30 GMT+11"); // invalid date diff --git a/pdfbox/src/test/java/org/apache/pdfbox/util/TestHexUtil.java b/pdfbox/src/test/java/org/apache/pdfbox/util/TestHexUtil.java index fe286d9..ce02d57 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/util/TestHexUtil.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/util/TestHexUtil.java @@ -15,9 +15,12 @@ */ package org.apache.pdfbox.util; +import java.io.IOException; +import java.util.Locale; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; +import static org.junit.Assert.assertArrayEquals; /** * @@ -48,17 +51,34 @@ public class TestHexUtil extends TestCase assertArrayEquals(new char[]{'5','E','2','E','5','2','A','9'}, Hex.getCharsUTF16BE("帮助")); } - private void assertArrayEquals(char[] expected, char[] actual) + /** + * Test getBytes() and getString() and decodeHex() + */ + public void testMisc() throws IOException { - assertEquals("Length of char array not equal", expected.length, actual.length); - for (int idx = 0; idx < expected.length; idx++) + byte[] byteSrcArray = new byte[256]; + for (int i = 0; i < 256; ++i) { - if (expected[idx] != actual[idx]) - { - fail(String.format("Character at index %d not equal. Expected '%c' but got '%c'", - idx, expected[idx], actual[idx])); - } + byteSrcArray[i] = (byte) i; + + byte[] bytes = Hex.getBytes((byte) i); + assertEquals(2, bytes.length); + String s2 = String.format(Locale.US, "%02X", i); + assertArrayEquals(s2.getBytes(Charsets.US_ASCII), bytes); + s2 = Hex.getString((byte) i); + assertArrayEquals(s2.getBytes(Charsets.US_ASCII), bytes); + + assertArrayEquals(new byte[]{(byte) i}, Hex.decodeHex(s2)); } + byte[] byteDstArray = Hex.getBytes(byteSrcArray); + assertEquals(byteDstArray.length, byteSrcArray.length * 2); + + String dstString = Hex.getString(byteSrcArray); + assertEquals(dstString.length(), byteSrcArray.length * 2); + + assertArrayEquals(dstString.getBytes(Charsets.US_ASCII), byteDstArray); + + assertArrayEquals(byteSrcArray, Hex.decodeHex(dstString)); } /** diff --git a/pdfbox/src/test/resources/input/eu-001.pdf b/pdfbox/src/test/resources/input/eu-001.pdf new file mode 100644 index 0000000000000000000000000000000000000000..20680bda43d38aa7dc96093c1980207e21875582 GIT binary patch literal 68143 zcmeFZb#$D&vM<<XW@ct)wqs^HW@ct)O5()K%#N8UX0~IBnVFfHvB%lx>~r>hbMJez zW@gR1v)29o=<X$zr245!`byPRqWCB#!N9`E4ok7MJw6S~PRvZ~U~Ekc%g+zXB;w#~ zYUWI=!~9;ztOv`a;%4mX<!DCCBxhxB1Ir}(?;7EMG*DBPCZ@J@b#>%rVls1cX0SE$ zWPJa2a5Q@VVv;lSRCBgcF>`kR%TGpA2O&3SD*;6rUO8S?W@b)aW;QlnW@cuV_YW2> zUU3E9cMi!MjV#OrxM|*bAZB_W1y)cHmPz#wUWD!K9b93Vl+8?BiFMdmSc$n<c!=3K zd5F2#IP`wgnc18Efr#UOKQuO;e?ep8`6smhB_<YTZemUz4q_HA=67r?f5XK2zaP{8 zBXMza5OZ>~5wmk~zC+~s8#J!}e?wzoW+Ue0Vj*VXWO;|k^*3nT|COP!|1TAimE|20 zJ0~$K$Gc*3vi=Pk&wpiTEUf=aZ0y`z#GGvG#2l<V#GD-Oo8DhG5SHKj;D6c8@&5w2 zKXj6nm6(O)H%RWkgJb<GI2B=Hb0b?9vp+XkaZguC6;~tI_W~7pVrE8W*1t{6-`*Mu z#B8uk5>~dZ@2?If3ETHZVrC`|rteLKf0x<67Z5XvnYmk;m?=w&z`nl=>@8d^-?fR0 z<9DYnuFhsgcCb>R8A)EMDjt{v0S<#%F^SbZCI~v8sv%=IY>i?Gz?Gn-p+QhZq!u8A z#URkd#lDs%vd-DLN+^nFGFF;YAkO(6O@ZAqA02SlVa`WoVsT4mDp(XwpA((m^W1x! zpRcaB-)CL}zEc&H*3UA-0V3zVkIO{#VPc>g=feQNV2MFd0IXgWj0}7vP#bamvenuv zFfZX*`cN@e?WA%;UCbB2P=@(K1i%LzZYb)6dgly2r^-^U1Gzh#46lMj${;1Q<4bU- z2KE~3toD81J)Wgr*huxywKNu)B6-Z0Rv8-eIEmb3CEOmfC5gA(27QYKL&ZwlFDvL} zN<-El9}X#7g=v@u7LhDk1gT`dT!R9Bxz6(GJ|W^IAV5wRkk7VAv}>!`cdhIarckr1 zMo`ckmnA?3GK*4HB^+X;<ZI`}Tj7DEbY5q07cLol#s-S#ba#sc5KZ!Zd#XzB{8+d) zHXT3GoF)7M=U^wauOR^Y$rH0`^r^97iUAN&1>SYFr;|SX3}C+OAn14)^Lr@tqT#}J zHR|ZNy}cE*E?5~*o#1-PM(q4>-Znnqx{`in=rqKdS~K)1^@y^nvXt|vMmtlkviGYH z!IlBR!?l;E1<i1RcL8Sfubv{g3boDCIocUWw|Yz!8=wkWfZ2i~yU=`7lSvgw)dEyC z;}zhl5e~3B1yHU-SpU3s!;lyU#d^W9zPa|YG7j@}ud*pPou^)r79MZ9Yv`QUVedbF zqXi239h7ndgw7Bt<1-&JXaF+kPhs{Ds6k?aoll_E=)Z({CIAA(Pz>J*Tt5?56WW6I z81i9*wRZbEB1Z&9cf(vrIuiQARDRYmM3oB+HiYDds3L|Y6^0Li(-KCEq1%CY6g8$s z4GUJzp)Ur|epZdaWEEkUfGvhP3J%I4n6N*%pHMmgcSURg_k)lZM*4pKJ((GF+KA8= zs<peL8awK<$_BnCO!*g^4az1oeJG^vybHkxW?xW2B!fVz&EN}qP-NVnlK3bjq!Q#( zyviX7B;NTLhZ2ynN~S{2@I2wV!j!R+zu*!=oR#1)ayKne)53=)aqBQn#e*mH4vcQt zy&*D1EGFi`A-j_{#G%QgV8WqkMFeufnWYw_A3_|193z?KJ!QyIAH_gezNU!L6RRVq zhqm`t^n&$n_saIR_G+Pqnu=eMipNYzv&Ma%5nmEnl6;DAO=Xw&Rw#@m`1Q5}?o6U3 zdq#{DcON4}VL-V~)|p#PU75fv!z(Uqm4BnuPdz}5MKhhCp~|WDA@cj)N4nzE8ImLa zBb+0<BQ)H|Ju$cfv#FW7-8zz`bIa`MLQAFt{1z%b5vE+c!{Y;@sp#p>Tp=T7Z2JgH z36h>J&4Gnc?!Je#%asV5Jrha}m6ZC_$P@z3R@=K2X`>calN8xh`4Qngl0ES~GR$wg z{c+W%<?3_L$JED=Hc6vq;R}bh2(8KO!G2MG*`ixbPFp;%3RL@GV>DxWj6dD>WSX4M zV9&7c;XTqMcVIk~^&zT)?{(I|9r`m&2raWL)(i!W-$EbBi8fm=3tnjbiGajFUSPqu zO!0_sQ^t{f{^7%z<Mej)c?^k3t4UDwRazZ&Kg+i&Xe&51BXw45Bq{_d*eZ@I8nv=( z{7rjmVH#tXU6=9enyuiLy_d`vIhXwneY)M}#?}}o6)YP2&JtG1d^-H<{Jy<@dM&y` z!f!&1MYIkE2_DCP3rjU&(Zt~Vn%QrTZ$}W19~s4mPla#51C!yL$(w=5z3NoR8=g6w z0hOW1JKn_Il;2#}bmG>0DC30Dn$&9VE^z!bBEOMz-Y{gha@x_QOpL(}a|dhR%C?fX zVzVN-TIG!_&`X#|s3XvE^Hm^_e^@}0pUJyhzgyqBszN`x4Q4H8P5WHw98aW$SwfR) zg)ZZ2^Ev;?^&#S!|IY1_;bwKiXYRh<AjTkZJ!PE%MKXc@2$=xQZ~y#|>7MAa6SoUm z3fdBm8}-8OyT*4`N1cuAz$RfU;j%9ZkgsIi<eEfnLJz_rvQpB&d`P%@`JIBAG+w-@ zN!U5-7;d&Fu#CEmf~o_J0+6tRG=k1yIy*m9e`q$e?Oug0g4XJd3K^85|3QBB`fKoA z2eoD_XSgQECc_RgC-05<jcJYV`uL-6!)FKl(Vyi|DYYnh2!nem`rQWPdtJ6%wu84j z6&z(gQk=-ANW;t6D)48CNy$hr#kh)EQzgoJ$XdjS#wAl^E94ZJ7Qq+!#vi4YDheb) z7Eu)0$+fmmxaGKSdh$vo7dpsw#*_Ri9()j`WMLz7A-`tQ$}LYSuU%X@**XzeRCF<R zd2+VjZP~|lQQTv>4!d&S7Z{EnJ|EB4-D$O4%)N$~gE;y^_AUQs+t18zwc*=X4%+;B z=&Cbnu7070(mio5vK7hd46peqTJ&^+sv^{bOp7X4f%JNuwCV3E*rCBomi&urn20tP z11t<`2->XkN?%J%D`3%2?W=X(k+7HQqv0c2y_i7xTLWo{u}@#^(9IAMT|oIz74LTv z8yR-VdVITrJJ)(YH_=;)%;ZX0E-f_m+7fe#?A&!NxLL*$0ppELAr-8Mv;M^Xl9pdB zu`T<zUbn%x3kXX=aFJP&Hn`^;6zs2sICe=~*|tu$Rt~A!n6<W&bTisZ-akg0D{Ae$ zy|%9OPG|0h?u6VP+-^L4J4pljY7|<>b~8ORC@WW%&E^WuTWV;1XYXbYtZ$D(IY4Y0 z%Dq-A4&HJHsw}GagY~`6v24U`g6H0vDs5NA3>?=RZhLQ+Zs)ng>=tIn&m4|ATUqS3 zH$iN_`)oYR_oa^^KqA_&%dOh3@be@*DFX6Ably^$Qpr<eQeV<6(`M5}(yKD?GeR@& zGfgw6vOZ*0W)ozGXFq+n`o54OnbVL<nVXacndhFjm#>~bP{3VKQixj^UieyMU$jxI zT-;m2RZ>!lUm9HoTIN=ESgu<>Q6W~*RLM}8Q-xU-UJa;rt3IkRteN{E`{QRVcWq@I zMO{WcT76gpNP}0yMWbcoMw3?4WV1~3&lbLx`c}r)qBfGY^mdH)=nmM9uN}Zn@6OvU z$F7rZi|*~820xd3G<#-xm3qheWcz;gOZ4{)2oH4r68P0N$UE3P#68qF%sJdJ!ZA`m z$}w6$#yQq7&NbdN!86e^$v@dKB{bDNEjHahBRw-bt1vq`r#81RuRFi7V7hRyXuEj1 z<i7O0{AmSZC2SRWHE9ihEq9$}y=sGVqkU6sb7V_pYkAvb`*_D?=VkZn9>QMIKGA;3 z0qa5Ap~T_Dk<QWXvE%XcNzf_EX~r4#S>5@E^N|b9i``4-OW;++HO_U>4g1Z{+mE+v zcXoHr_hAn>55<q%j{{E{Py5dvFHkQjuQabMZ?bPIKsz816drK@_4*n1_bL7#mLc)K z4)iQ+|1?>3n50ETMT}g`Oo>^3o1)5k|EBrdsbAE=*1=iD(a6M%_>acQfAMKZ+r7IO z{%yF5iK_l-w*J#V<z``LhGo*QGJQ8zxmn+T{=@&Esc3Bd?t6ImmRPyEd^B?wb+B`E zus5@JCFX`@k}|WhuyiHn;{J<e3tRx60T6}Xof2jM(@y{Z00g)U_zHlBhK7cLhKGTH zM}dQdL%~3TheyJ|Mnl6uL&L^GMSp)#v9R#)3Gwl;$jPaw$jLd_*w{Gu{`G($z`-FP zA|N9oBBLWBA)=!rqo5$86QHAGpraE|;9|W$xD>=>goK1-#MIOj6x7tLtjw&e|7y<% z>;j-b0~Em6!9Yj>peP_<C?LRI5TSSeAVB_j-dX+qfB`^2!66`_pkZL)K>o){5CACH zpOwe}5O5GMP;gKPC`f1sFnG52N)#|~5>yrlVI?CpQm4<Xkm#{FH6mo>7|PwoZ0yco zCPkqr;#4j+FHKxHFmrzdQi{c^nu`C-bLGUEQrj}Ss{PtC?Y4a_L6wmItuC>5X6L4$ zzHfH-R^8k^D5<cae{S!NOH#waBRILJabSM`9t8jj`pzx*A3Q-pLU8=S0tpK$_&Wzq zq-YSVpJUN~bI`3kdBH|*yy^TUPE-ZM#D)Em0`fNlP?Q`$r{0;!!!i|D<8(FK`WFlT zIR{k#W&yYafCu|yOca0+U`3iDZ~BR?wr2F_#4jgXyDUK?9dZz}au8l99*TM(fOGtH z(BZN3F|LmnTZR>Np}044CT{xYK@k&1GhuZ(PK*u<Bu^q;3q(c!s{CjZI~qXj^M+~8 z0A86Qc<JGgw3XqFN@@hFfRQG72tyYC4rz8-l?z<SFSD=ik%AX85>Aj6x~OXzUy}-n zME&JKrya|O(=>I+F*7@;fq-kPo{Z0L4sWKhgD3@MU-UA>lCrqO-NhF%-~#=}PU9Oa z#uJooJzRs#SjO-}UaEH@ws?P&xyG`xob)%(ERmcrn?#3*Dro1&NmI~}$4SdblgBB? zNs}l1*DG)_(7L)R9x18QGn3~Sgm<!}a=M@+x=U690-7}fmZRQojqz&|#F~Q2S;xpo zsSoZ;7~%d>d(&y<M)aKM@!E8LuJD}S=}C4@b}ze@-^8?}C;7Ik2L#yjJx>WpmH=Re zI*_1d{5!)8T$9fs{C4~geP)s&o@I-HfR(b&(}4hQgSwXJ*P+(86_5LZdujg*jWx-$ z<aTMtzn1@v{@)!XY0S1(wiYF?nv{DKZ#M5V^>Q_9m$q&9kP(hd@_!&a4p_>5g#-d7 z2PuF6>wp_PAV8A53<x;tcX;lbm%jBEGVk=jTNQ$aOV{?}E03UekuIA_#!@)g-%+^& zH4$xDKt?Rjj=lA)S*G!HB`8`QcNkadJJ2xQ{&8ueO^*hSqb{_MA#{%%kRfzMhcQOc z8%Z8dg#IZx!gm}sH2U+^<e|5UrZ<mW5Dq3(?1F6mcQ&Ddn-?yXP^0>OS&1TLG8F)= z=BlC6C&yQY-e831yhBCHYpr5<cQX$SE4J@+wNp2S;|n`lH$u`)(FkUWsO7r!SUZLs z^Yz=V3Wb6!KL>k{wu!*Eymj)q9af2ChtRz(G~5yFOGa)r<?7^w&ld_)WM}sqT|);H z8J;<ceac5G2b`a^w=WqJt8UXD+X9aHt{|e0%bpFYbU4{OJuA&QQW9-S-GsB2x~isc z2Kh(Ee^JHA<=U8ikHUdlZNIfRFb&Y^j49LPUVv{nDc76RX&&{Y3YLq7#N?7a3Iu5Y zGblMi_6muPmH#$N6q;z)-J0q|F*{-{7wH>s27^Y*J3s4c3W&@Z?-+>~wySBym$T)M zY!OzQ&B{sC*B5S2d=l?+Kx38k2QAOXL!gN`^DZHe6Nmk;yOhY|REA#d-qICcG+rOd zIht=9pX`J#4eqz9=aP{2ws$6nUv?6b2fii+wnUK}oOWTC<A6Mm$yr-SMPo-Q5AzHJ z2ICC&k>DtS1yS8Z-av;yp6RmFdu9zCl^-IQ3!TijpD*Izwes6M=X&iA(!W)@FxS)w zz#cs_M=MoWy6k68Os*+}3sB(m=o%VtvyRfUCgku3?K*eNPE2hl>6aFVeu55}mK3>j z{XA9d#^iN`7hV3-FQ+JabdK|lPJmC28ygb7dxmb+pxAg{OQohJJ#1+Kd#^FsZUX1Q zT?W*%2;WeVo>i87H%=L9`G{<-1p<{EzAiK*1O;sG(vmz*rU!4{PaOz|M|!d8Bvb!6 z@KgZ=P!c`N`%6@OOHY3s^lGemiM`T!n*jn2xZa|qnVwaEfbZ*1JQNy0fI6qpIhJbq zF}B){U*=CBKtt2AzrE{}n7e+o_zQTXMA%W88E0#Wk|$BF85he~Y+lxpteDGVf&2_h z*ikYbNh(J=HZ+3IF<*1q+R^ydj9Rp!G{>U8ERCI)fpz<xKIi#Oxm(|l!x^76_wP3f z$^L|ze)YP`0<6ntPW?A0uF<X&{VP`r-xnW0g0Ct|;Z7bu>&ZnGg-3eiSd<=%iVj;B zE*n*ngUZV}qSciV{{r{8oHVuAS~@Fy9^+KI<xN|dH(sL-=9o){LaI|%oI)A0xx8+- zDXqdeuYAey>04f`uc(?pLR@)$4leYB+KQpPJ%3gc?fh_(UsG`i*j~3-OnV7ua>;_R zf2O|m6IH)n9DSKG=~L@OR$l^Z@xq?lllSBHHV`veAUIv4Mcr)S@~6={sYI!4HQ2bZ zVsxR(+gTJxaKpqmZ`ooleP$aqIzy~lP9V^zle){c?V1!VzR5TVqcw3l@k=xb+FlSG zI>s@~WNevi5)Qe^>nL#|TuuDR*UpNlu7@3AbGC})-XFwr$KCYBkh7tAwGGp*oxI~E zBTrO8da!WBCu(&w(vHD@Uas+=Mbgcs#Ka)}&VA}$Rm;(*!hCepejpUIzqx(;Yx(?) zKXuM}(TQSoyn?)c1$ojSI5%b!$}bmvur%Kgk<MZj@ye1@Cl>)6V(3?apd^nIiP9(o zSsNmi?69*kDNH;%ui~bqtA)CTX+2-|Mg=|y$eWdmx!^{hK;|@s4Y$I3EyHACK5ehF z7%JI|&ZQX079fDmeGq`^35%gUTxwRIIlC~iM@WuBmKejrMqsBoXPhn3Rvw=1lBrcO z(%g6&bjAb(K!YTeRvlsrfuW(_009auY2xK2DNzAs2o5D&XXXPo<Onb1rat;Jc_=>Z zq~AO=Ov-KG(=uze5TiaFJZE6yd2`I0prw9jBxypQRmsxc=OX#C+E<y04;S35f4C4} zF*aPYP+@ZJ=s9F*(S%uziHi8R0<*<Gc_gWDK#bUsHdYd>FFfFrML0Fryp|c%kSuKJ zmXi+tYj<er*U;f(<*&hCCK*qW^^12rhGwHCT7nO2^Y->x8}PytrHce?lxxM#>+<I= z<rOyzJ>eMaki+=Txc0Wlrs`5ctZ6VuyoJI3QEP&ttaO~r$hh}&9xfd1v#rsbxs^U# z`DWTfef3&)42=|HxmC4oqYcUP<}1}l<BiWE{?vMs`TM3yDtOT*CuR6tgxz-<n#16S zZgY`8;Ux<QZY9Reu*26&Ue;16Le8>dya$%>BFS6y5^b#g=Ys4ABO`t69ny8@k-h|r z9eQl;1~03P)=o2Y?BhEq3`pI}!`qFMA0cEX5=$S1VYIfzbv<=YJ73$mlzv~>o5h}+ zS@9dKT?vpj7h9NQX`H^EC0vU$@K;dEDI(vQHWwJMzvypB>a;1;xzQtQ`H(f3vpU^M zlCUUm51tX6wuldkea!L50>@UewaLvzHj9^wKV(<)W4k`j&DJr_hknW1t>TDX_XL@# zigcNAobrc(&a$N;;_8vVix^7o)Dio#KcCjel}$G!(hqk(<^%=|04Ot`r`0}I3nd*M zHYckuYvSXcfh`=ZUfX@HBw{WsUj8)P5<A$Cq=};<0Hp~QXls*o2v&p2p*eHoyBCo= zVxf8xA$wF`6RBq3{Os;+LbRfD^L@dT#G)~v$Dv+?IP=+`N#V1<X5mtaokPQLeo(D_ zq%D-d^2#dYW`Bn)6H`7%<gY3`><FJ!g{R9`%4C5mC#OY^gB4>g`yI-khlsZ|_MM1n z^7NhdOAC`a8BeQP@JMy+2ivDkMBeHS4m;?8^OuqpX%|gBBHASVY55+~5g)pSd3+_k zDpHVDSsj}}-aw0Swf4|Z2?o*@T05QgaRh+97jAQ}vtp<e+Hx**WkU~E$>wewYqF3N z#OS(SVlc>{QB5S`+fZFiwIkc50$n-NFVH;oDH|w)u~3HPYJnDsADhc@#alP!ZQ51t z!zI7C?>JY|2_lBF9th8sgBtb~N#?8tshtRO_(^Y=WbQFT5tvxPIjL08&d8!v^z2+! zpDXxm%CUHsQoypXcs}cMnp_c0UE+x2Rqv=<E$OM9{pe{4ZvPr2>7^VX`K$gh5?lUN zz(c+<Om^iS$419aab?d5v|sA-B$TSr%`xg#bc(#W?Jd^qyyCQ{IRRGGvOnJ_P`2C2 z<9c({{E+Zy^$~Qo@9+et(QI8s&wcNSvju-cfU{;<ac?ljGvV{aQP{M9<V~D3`u{h^ zJ%82Q`e7Xq&=;^C)}xcH6Hqt}1YiIG{&WadhHv(Q^7x%&((W<%Fd$t7!YM-TrT3bH zhs{`XXPLse4b>c*y@M`(wS)M8Vg@L_yte@x?fJLqbw;+6CD+$=n&^Qa(ab2r!BmJ^ zgk(`Q*-~h)^sy~DIyZ705qVCyPTq~v-x<B7h4JsDHBFQ!pAF<*qwgGQ{X087swl}0 z2B!7|W%)l4H_2M)Pj8pD&yGuRsy)*S85&|HXX&ro2U_l@zJzr+QpLl=JHHGNXR#?k z60jn>b2X|<X-13=*yAQW-Tl8}I}mg(>M^}-P67dTY|n|l5elg)cU(~ThvaFC>(6xZ z7UrNQA(rGZ#0WCcJS-t(@@m7MX<`|~bqppC*YBb5-j*HiscyVIUc38&fQIGQp;bLP z&Q)1XB&?Jd9ti=8L?A#QZs<+ultj2Mxz#2R%W&UYfbVT%J7C%FZP$XR=%%%7&9SCC zMJUhWo~o9psP^rKCYx0#FpLwc=#X2(gu&A5zWUc@i(__}GFu6r$vAX`2eL$7tH=?r z3%v_oU^D_th3)Cp$D3-QGf1x2y7e>RUn8sOc5+58uaW>t@-MEiYP)z(esdh3v_dwd zsM6L9fq=A~h+PZy_Nc@4$WL?crdRd0T{WtZ*qZPc?vFQ50sjD#{yMlGYtjPY#V4_+ zzt>-bD0k9u^J2YjgZpyaf2ZzQ5gg1-G)Chj+hXgFxzd05%6JFm7xu{<=^(`o2;k5P z5IH=ZZPGk<Fs5_3r+vjE-kGo^g{cl)b9IPoStcCM-CF;Wg=vfwh!b=9#DOGxX!15` z0t76}zwIb4v~xH5FxRZ{^~uLxeKXw=SmNgZ0#IIu7-~k{_k57z+&saZJkwr2&QI~u zE!Qu&Y8akb+Y)udo+VDx*bG-+j&<o8H`ss%xaqqalHF98w2gfOz2_Y9L2v|1*&y%Q zh|-OTc8n*s1T*Vp>;?jcUJRZsDObM%0f`UUcM~dZ*rQKc50M^cPu*;nj5Z#%cJ*(s z3P?mZDi0=$>OM99qLdPX>QwBDy8H<BkV75~?n4)H?;b33XbN`6`_Awei+hS^x`Ow! zDzDIKVSsbpdafYMj4O&otIC~*ED+G-8n7()w$o|m+W8zT`C2!AramR}rTpI7f9Npa z9x?jaDtjgQq%s)@a6EWduhiF}CFzf9S*MDB(b>4y#3>-)jDYBFTBun^nijTpwdAj3 z8vJN6c<sr1Te7&Pv!W1I_o)~9_i(a*VPn^hyqI>}z38vw4gQ6Ziu^<ft+ScG4#)Ht zJ`(HVzLyCsW&eA)zd>(^Q^oji*#9l<|5oz9EUEusZJ%fQ`3(#V9HpG(7Ap6ApeVPr z%sijNZB9He(C#R6L#)kImtaV!Nt~f0o#)0`+4r|0*s2M*Y&$kMtmz!91p=fQ=y7TW z@P%W*AuFZiMq*&RT$^SX^MC-nvHG`kyq9C4n>d4qHMgpph4p<cARs>>+q3U^)1pym zl~G9bC~?1*AaR?G=;UNU7EIXJQ4R!mEQ2CjQOX>V?&8!hRe`9yW)gvFMUcNg{C&6n z&pJ)?97z7uhwr7pZJbchjiZ^3aY*+=qT!ccPp=sw88Jj`y}Q#DZ-IwvsUsvC_LE-I z4rHTCQ5Iwdm;OcV{JR@5a$g$=kdJ(G%YKq(dU4>Lf3kJBE(ZdpWaN~0Yd}ee!3$_I zrEjGcS8el>#Be|@L2i&3<Bo*>Iuq|+yT5iLeIH@af%&ZRI(6(P{`2RrgK@uyTir{a zE|-OQ?4?nPZr8u;m9z$GPlAe)hhu$%E<`33ZlC6)kwqo<IXd8s%=bvO$R)lft<Z}h z6mzee_PsXVIY@Mho~)0Y;t6J*=b?<1kOBe6lE1wa_d5wh_oLxL56hFUNztn>iuaW+ z6>hq1_|GhtoT?MJ9zkEON&Vvs++c$OP8_s58=nL#(*>kbvIVENbHj;aF-)yT!!&y< zKTqe4sIE27q!}D(TH`O=pOpLFz#<;1B|q5>n>|8>9on2wJ%126V(T>S@n5dBMM&Q_ z4WPRA-+2hAX@?W+JA4Y*^rKlN^zj=)1DO*&(g7oH5qkw2@&DOT!=o`77T`Yc+_5*b z`qUaM%6wv3p9(fB_;e3)qg7a)d;VC|baun#k(#Q#kZpP+x$r6^A?K~sV4=C3>(2d| zNt#I1%v>uTHiqNXNO+U?Q&dhIcu4c*_D$FXC$o_d_jR)D<vCdUC<Dp*5^lxF`eW-| zuwU)OEf65+pM00s*~j%7^&0+ywJ4w{(2?)2saSDq1#RsoJVe>T3yGl4-nQ;hau}rx zLXMR6+4L6Dw2@|Pn0QC)yyp9$?Zq@M0w3du|KM$n$@DF!f61%nOP|8{W5tos?2VuN zfd66UboqD5Ff%x?8D2NZTlFNqB7t$+Sev|^xk4U`TK}ttnvT%}u9H>)1b1(x+DsGs zjGWFp_j`vMkMS1wk;jrt*{{~{Nv8eKArafS>wWz^>V(A9yvqc`VM{>3yNY!MOv=8z zzoJ2a0AV@Ss2F-lkmDpFo1jWFHLa({L?gm;kyDGoH6zP_xXuls13IFdn@+B&<=sYG zzln7II1Z(lT-X!MJT$rt`QAt%0BeZFg($>BO;2KMyynYKL6EEUZiBL8EcKcVy&3x4 zoV5n_m$bQ{Z_`3=#t!c1Kjc!E2ZrslogJRRfPj|Huq3UVC(T`dmN$CG-7AeedYh*o z91lDUPs7taoICL4*UxiLa?cH*=Y0^X9{GG>tTcjrDVO&?>ie&sfq+$mJ~W~?lc$lJ zj=>|vMprQ)z(H&>o=8eQetEPegP_!@guS}?E9!(4EBt_C(Ve!C$EDC(SX{-?rR`hQ zQ|A|^ACu3Wp>H=G!;)ujRL_&jva?%Nv+r)X3-NyvCofvQuJ-}~-n7!*ZE8=QM{m&o zB%XR^AZmTcJ{Ol}(g^F+cs1z8`jghcEbl%5?e(C9;^<Q22nZ;9ApIw?>`9&f`Bp$X z26?+a?W@6U0Qf&?5B>hJ+;a1i%lZt;-l{gj|4I5MkE(IEo@KWIvo9$BBrW(C9{nl| z|Nn;b-_rSSW&6Lqb^^zr<bZ(8jtn2c(Sb2#WguX#=Y5Ba6Ez?@)jPacz6by={>Nbn ze~W?s4+CNU8BxIY9@hV7{3{DF2RA44U%~{WLL=0DT+w$ifUD=}i+WeYFvwS`<^-rR zg#73%@3|L?7tr7MiBCYMR{4oP8Gb^SLg4(Se0YUSAFOEwLSsy#ObN}N(Lh2SvX8DE zMaUx%rVbJ-+P6n7h4N#XFC7^TcyPGV&UXK}I^fZ{2m%5vPDU;v&xH4QFy7hGfge(8 zY~{5(#LKXI(|#)l#QB)`SPz%9Lg?m=GkqF?0{0#t6X5uoqhdSmjbBo7r<TdX-iV9+ z!h3CEqlp~L7y$WI*U;Z`G_GZ~ER1TOH2@CzKHI-=3M(Bg9Spu0!txY#{YX3Kd*~}y zO?$&kV?OUw(=sA_-HAb|p1$6*ZZD0#P#ajx@Jtc2c4in!M5r?#D&TtBz76*xg=@BN z0xWz)wujWSK5uow?J?%77j_hlCmMle9y)s7YTGB7ras#I+82lyl+y=nh~h6F_&GkX z1q8}V_4A$x7os{4_NeN5h#4{mk%nh|Mwrz6q~{6~uheT}YC`HCy6eki^hhvdh(H3a z=yY?s5X$+202ITo;s(VURPY!PA|W!%=&9xY96bwjBmv*c*%k!G{cYS5{4@{mO7=R% zYwQKf2VuZ6q7Pq{xh+Iv;f&)&?cmD8yzR04(dU|g-1%-)_m(DOQ*%8=Vpo;sZuqII zaZkB6^%=RJ$K?)wyE)COxTnn43+29;s7E^|)G3!0*{r`Siew^Y=y9|cl|L9^VMMJh zVwF4Mm_5q+fZvbir+6NB3FdOqV$m(%;$sKT<V%pr*Y3f^v7g4}=HpnGbSH-68r*YM zL5QwXnnQ)UX>tWOps!!_3xFZ2mpM&T&%@BElmb(9cspiZ^EvJcNbC@dDSdm^XB1B3 zqMlUO&0{zz;o9Y0wCoq`4qcc*6)WxLkb%!$ahnXjU+LiI9SIC#h!bt#bI^m7;1?4l zRV*oALog>`t#h|$Wb9mn>?aslBqA_Uja#TO;KVA5(R99%X!lAxsrim-nM)_@k_6DE z@1|c7JXXGZ_{=JPNduQiM7N9~>Sb{F(VhkbqjZc}<hI0z_Z~6~)Mdn!ZMmG8D_sYt z(j}T`&pM7J;+SSnucFsCD?>ZT4E}bd^;UNh@nmaFYn3lbT`={OzsKT&lW_yA@|;Y! zpI~KoPxDqhK7PuI{r+A$?H6<Uy%hz=M>4yd<mPb(*NB`<MyEuO%4%KDUj7<o_K?wB z=@WD+vDT(vd`2>O=VIj<)ZshyJd$JaZqC8J(r?;lajj-$w#g23B7@3diSKL1tn7yd zPkDygnhIk{-i+<+xMA_YOlR@DEtoA#14{#|PTA^Sv4`&&^aetPI9$}G50>3`DeG>M zCtcbfN*QSOD71`I+PmlM5gx?B-|f4Vefw|(#m>X1ER)Lf7gsv#*|YaH8d;Uj5>*@( z^kmF_q?CRvWZ&#~!t^aQz>gKV9ad<eOH&GDFm~DpnHGIjl6da*&r0F=)$K?@h$UY5 z9^%sL=rr{PWi4L0O37xHME0fG?+Ug^YcJ$G-o@asufO1Kts3p!HQy$#-Veq76W(2; z2CYEdC9II2AhTp`3DkF!Gh=R<z!<*KRdgywuuP@QZE-=x6}jv0mOY)5=uKt5UpOyG z%&&CvSUsIPwrU&0*4cNPn0`L;CtmvGb2_Xvtx&oAsAXl#^7I`siH;)Va2E5{wjR15 zf|pgmEfw0D3F3eGXe{EZYdf-WAPfd2CN6P<gan6hxL>`jDh?}>Cd+hVE%9UiW8AHD z{1k)!eNETA>;?xrCdOSo3%kRF+_6pSK+<)mMe4j+>b!|`tTv+a)HHi?CC)yaYek}Y za#QM>?sp&dZS$BnqZswLKsjzU1u7rQ%7rx0QR1pSR#=U>2fZ~={VED}a)a?IL}^7) zSt;uH961~oZ#%fY9pxBgmdbKW5J_$ev)*F}MluDaYGr2AxM2-5^yDTz2pEMTDuL1O zFEZ^vKM8h+NFk&V3t{(H@8aDVV%z1YA#z$XSe?GfSHu=y6b7jss&zEmi$3wIM-;0@ z=@|CD>{y?8&@%}VufO^_*nPQaHO8{WJiZsI!ts$=X|Go}KD)fYJ)w((ZGRI%Y!7YK zR4sI3uUXsXv=j)QDVHfPZ8<8N(@W}Ym1283M7zNSk}{pkx2=D$YQL89^<9IxM(b^5 z2f{a=vG-FP5E=*@4--Gg9<PTCPj5Y0Pfcr+=C$B1PYLCwL&KfRtKJ`cQJq-H4U_TV zl~tE`GR=Y(Et}y+YaHV+(`-C~f1qPqh+smhNmlCP`!R8le10owuH`(u{xP1^Z8QLn zALMD2=}E?S6;(%h$4)b!gG!*b{|TxtHqAOeUz5yud|zM}-zD&6`_PI`r_ZyUQ+XSz z5vgxQC!5NKyz%`H08UR68$#L?7F^jCy|$pRuVlaQQjb`^&QH||O^A)xu}31Y3!Ld^ zQaw_aJ5*Wr#I9V2hv4&1sKuL2YyfjA&!(^v55*~JSJQ^iPtWWoBLsDm1@k25<#E8S zS6PK!RY-*)>-iI0d?Fb73CyXbUuq$OTS{loHcH@Sr0Cd5+v8i#vXG0efv(mZeDqZt z+kEfS7Fj;i;p<M+q3CVoKL|rNm}}W@+bNXah^Rh|p9AvN@0tLWb{_3G1F^jDY^^eO z-C649H*Apjf(+%PCIFiaN=~CjT|>|-rb9_N==RHa6~5nf&dx*+b(!hZ-ySWsaPW#I zf*`^w!Yl&U1R^ikiPi+nB=lGBaRlBX*T7X{mNVN5W<^#XaXo(Yh0CWaoUKGUY;)Nz z@?C)gdfB~)=n|upe%H7b$iNdIMdy)mUUBY4Gg~d&G^S%~LQCCTX3K<j{~LppPHRr< zX_8EGWqzwQSBPt?t<~U9&R5wqNHK&rF-uMo5OP6e3h0y+JCIt6;>hu*uO-6z#Ly34 zkONbwtNlIPA5fIg{8*eQ*Ln7{T*k*&n%VBxP|xBY2~TIQjBg%6IN+61UBiwxmn|2C zzZJJ5G<eHGa11k%tV7U>WgzVi+oE~4_CXPtRmzb2hcA%Kut1ihSE<nXYDn|ehn{+N zOsmqgDKczSR5Jmr3+^xEcm-fm=MA8E;MbxBK^~5(8;$tei`^n0$SNSJBv$ITxLngt zivl-l4?5*qgs&HwB=os@mcNWUM_>%H$`7-afdFxuaymX>42fTny|wV^>dLhHo>c$5 zPdDL(1nTcxh7nCux<v3`Uh2|LQXJMzd{;Kd%F>I!-^g?*kck)_9umL9?WZ*~L=x}f z<3_F`EC$1s=QrUQbN<Y~5NO#roQb~s5r5~41BUarhm}tikZ!ol#|CzD4vsTgkxsX3 zm&wakFutyn65Y=qJe&xgBagZFu`>w1jzhm_K-Y7J+F>A?2{Ys})aum19RJA3h%w;c z_FWYA9%=n#E5dWeniGLqj%+aC9VloqK^ds-LaVB%#)of}V|@g$P?S$p{lsv!lgZ>f zleCO~=Dg-VGmTy4n8DXF;Y9dQyt3;#eCphWs0x;6dF{U|NrxYL)w|b=7s^$@OH1FY z)395X#ol@`cn!e=!Q(c_{=72%om@yQZXjVtf}&Yix}%s}gLZIN=7)^*k&JUqoNhjP zG3RXQn96=}b7|>NX+A7%3B=(DPxP438|dOtD!mge&JVSXgn)wwG-x>t#}?U9tk*9> z3mK*JuT~V(sio7eG-jhqmD2r`KPL*mHcmx|=vgIgmJ;;&&Sp{ev+}uPr1RsOzs<-c zya;F?Nlc`gH$Nz8WsK6BUB?T}*cSbqr1Xim6Zh|tSr?URl)m*z*6RdbRpIc7BXa&k zGO&F}*;=S@v_oFX&yjbwo0?dXRrY!5uAG^Sa~$gXCMTUjaGiA#aqah^v6chUwP+3J zw6>_>-f}5_|7)`__3>HgofBbbfdaQlN5a72G-dlX5=*p8?fhYS{^i`q`JE=Jz4R;Z zwu-S3CVqL4IkslgEsw*sj(GdDr0T49?@}!xp)<@lc8aG|Ka4%%MFblEHuyO91$ab6 z#cQ|up20LniLwiJcyxC(j90U#+G=0H=AOuL7wmRBN77OwMs`97K_P#6pG$RBI-H|Q zLF3f0d<ydld$hx&T)dx-iPfdF`vSXG&X@;hykM@7Bf@3)NyV!mqAF`<DoTUmB0i&l zExuWPm1Z3MgYvyjQf@Q%a39zV-NIf7Cft~H7`<i!)c6O2PzIPQ-RVj*dOn9LQ`uSm zyS_Ug^Uq9nn1q_Wrj|GSjC4u|^vbIFx{Ih7;yKikvAGeLr5LyF0*38pXHC0^nhs5T z<&7&dI~}`w#T7K8Orr+yUmSw$t+z(xqrdt0$#9yKXBB;`eJI*fI%E$$X|m9d5)pUV z{pPZ#&#-HB+vPDQJ2T@{&=7?2Ym90rOTWRlr0gR%U#rLZ$w^0h?H8_Li^x(vU@hMZ zQR`!(?DE-Jxc=Z9PUv`Uv!$6C%$0&l5=kc<oivZ-YN2T1NHMgHj5?+z;qyp>_(3Q8 zI{}4CQi;^}iG>g4T&R&&l64;q<hE6Ov1uzPSh!=bBjE3;8Z5ysEfe>`;U(V+RXC6e zA<!;SDsiUN?LHgWiHgcogp|o#_it-k0Q);ONh;Vl(edyj?JUnNuN&(Qx@$%SLLaiZ zsKrd!Kd>AXK#O;C@Pd9V9*^Op7S{P>RGTcpV2L2I#}4Y(IVl=Qi3p^X5)>tROpYx* zS1(jTPU4PSIb{Q1rM<B2(6;W<6HP4KVXf2m?Cs{;tVQ8~$J5UY;R1)BqCS8uk0F?m z@Sm9y;k}BOG(TebahZh|%NDf+3Cvv=s<~L?3_M6WS&uCD)JDpuOO43cbT{H75K{3s ze<eH1o<W`c7;te?A~9B^9^fY!-`p<JUcQ%;>R}NC<OJwf`rG}-C)vLxDoZ%nyDAvj zy(e$V$V<wKiqffg*%>?7zK77en%SunzX!zsrg%T^n!RV3{yqc0N7oCxnEZ}}XJKZ3 zKj^;af?Bu`v;Tum;~$an?96|*GqSU?^&+PJN2l+3y1zS9dCvuPHL)aSQgCp#GkPCN z%<LZ>GO)7nz%og@8rfQz2-{oOn*BMvh=_yd?`sJd-h<oU^IYGf{C|)Aes{q?g7trQ z`nwNjD@Ruc=l3fNj2!<NqW{Oy`_HQa-t%Msn(6zVS@65z{~7q7_aTT%i~T0}JqGK0 zr_y37UM}yGB5iN(@Q;fTRNNdLZO!a{&&4}l#oyN|SlL?;Q%jq^7g@P_z2j9jv#@&4 zaCo1BKT-o|e!Bvg#58{M|IaD<H{--Cza{fNzZ%3kEbl;d*g1Ig{s!`Q>p#)F6Z{3u z|I8u5%F6XO4}@w=w?Ypye9*ZUED_gf3_WOf&QU@9M`18AxO;jh*?f-Bm79r1s*)uT zsDU2=z$>ilQo-3ViH-9ew?Gu$WBAPjswrr!lZwDRa1%G(vfbH0*sl2I)PWGuQ(h$T z?gvTK0V%eVtZ_vqH8;!U=fDN90J1<+J*3t<1%Lkn1-5UFeIR<^AZ9R-eEns$uvOL{ zHUeHdwfUgHn`Z_Rd-3oCr=*5%p}!#`Meim}qynGzT3nxWP?ie|Vt$xfd7oMsylo;# zDvE`=^7Melp_KP57kokT(m}i<>jE&)*o*zgTL1S;75+2f|3ei`jowxMUGy&Rg7~d8 z@2MyMz9!?hKD^6L)j`eP>URb-@gI_S7u&y;5td2Z{=MqAGBEwA3=C3#lir_ME`MCf z@jsIu3oAP(=fC|)?@Q!=a44N<_&BQmb|}r;gMx^BK^X*iw0;N&ucr~^BdCeB{bF~3 zO`MM><B0W9gdCN)fQ2=6LBWeYq-}R|<^v77=&gCG`;QMegh%<-3`Y~Zg61=&1llwN z7?u_y_5u6LJ|?K&-JTjf_D;_I_kC8bGq3sfpD}Si30r>teC1=Ys^8>0M}`^G7`e3Y zrNz^4y47L~WcMDhbO}L+cXIfAHn(`sxy(mYY@pf*Ol-mVwDy6g#d5F*37*zqZsai= z<eSIL3X#FX(!f0eq`AXZ#vq3(JP-eYc~?u|DGP@iLgf0w>J9NSM%Tu#xB|GXP9I<& zOqa(1pqs_*vj6Erl)0n7z`!z6-|-o6Et>6-H~v*Z>vakPp-yM1@MfN=vr{nd%@`%7 zHU7>OLINU43s|Smp^-Y<&-V**rL*PSjmw?sq>{D4h<*R0Yg%d(q74+t6EzL1K^hwG z3Dce}S%Uy|!MfJlN4To+f^X;e6tSFN_v{2cz5yMz1@C54le%QE<yhq;TLT$wf~0-f z!Z|gaIT>ta1vf?}8=$6>W7i9fPW+`=^Rsz*p<d>a>rvrH4TV!@c4N4D8rQ8Q3s_pE zNNxVF?or$Yu8qn%pTEt7F}~7^qRkmGA#P!xTIZUj*F5tc3zP=mAmd(;SacNI%^Pe) zA#PcGHp1@W?x5A@C`)wM?0|A*a%EyszrG#6@q04oFPk^^o|Lbp>)>g=xLc;gjKJgb z*0dj+5Y|d6!CHu7X&~1{jhZJviNobXiRvXqgBvoJkR^T0`8eAPntl{=YRS^tOQWa7 z#;-&QmXQGyCD<UAKS!fYv@Oex3j1v#{@F`0*vDD4iqWS=1>K{1&7O%nbzvel^05X+ z3$6!l+a)gwWxsJr*u<JP*En9Pyx(1JOkL=4#*<RUKt7^#6WT&gL($FL$yNTqv?E@W z-YH1IvCyfvt`O-eQf%;|uYnycl%rvqmu#t(i@Z}!O|9p-DTiGbSC(9+$K8rFDa50R z;kw7&oqJMWJ*k)}bP;hVFG}Kd^7c{TFo>3lPd(2sT_?#eVJyO*N{Bt7?5oI+^;-h* zl<}ZxR<bnv<&^AfvB^`kQhWhQ0jk%mbaPq7@bzgn0ZjD{#wgrJUM$9R490X8D(hr& z<E-wiJ)J}K)~J$GBfHaxJ5syTa-K<av}t`E6#1hxef)IoS-~K`$pi#+nhHj3C7GTi z$RYL?vJ#8p%SRe>-|!=jM3H(dSIdtr@CHRj!ogW{X3xBtylfS8%Di4pIPFcOOUu?_ z3ku4$`sVN_Sumy;t_ZGyV2xpg_a?Su7phN-b=cc=)lS%*u(n+|bVxp|S-tMZ^y&xO z)$0qv>mM7G17cpQ4)<#kPxX4s`UUU(j^?$zv<aQ6B657Qo#`tE>}Al*Qfm~`d%TF8 zBJGC?x*!?Cy%y3yxYfxdG|MKePP@XoDtbu{*gbOCm_zNrGW+?<+5A6)+6cTEa()c> zV$u>aUGvjdAaaiK2)AbG1*-Oif!u6#!)v6TP`LL68^oBcGay!3#wrg%vVy-cyg?(S z>U;l*$WX?ozFvwwm*N5g(kpv2vI49hj2X^~=>tw-=L26dr9#(y;p(jk2XN}G-$eU0 zoUopkc-&z-m89EL)<&g^8-h0HZ$!~9v7e(#{K6gAe^@vu-WqrQIP@mvoa$H;N!Pv= zxj#>k^=<UFJ*G0&ATr7rk-^9-YV#y8ms7s7*Ov&Z@sc6;#8lMrdXjpwj_PH6V1x`G zUu;Z16)G|*HFH`+J*MLpUKx|2^I0(^_{rLUrh5-F)H5T1y)y3C_RYE>8Rq`0M#5~w zz;dr5PvgWBF5<^5Bfi6DX}@bHcF6S|{j2Km^>s6Rccz$Dz8j}Ah^zWpo#W~&p6$wj zjiI-$6rzHRy<HOHL-aE}?h&u#=KkGs&?WkHoLtX@eiFj_y_~cu`g7wzrZ9^vXr_bg zLEU>$CvJ*lK{ymD<*fu6*w=E5v{CHJWaFkSIv%nWbbn~BUyt2;!4X(?8a|$Z%^#wi z6mKFiIqAeNBvH;fN8=G*&NAbe!ournE$p0SxwrGS*u9v1lKo(dd{vu0@RC9hXrTrf zh$zmR1~|W6|H8|i>=%fy{mDh7rT{yi5*>#|Xn%Pc`;vfvsr0DWbda|XM>JZ0_=3p4 zx4XwzX9j*${<)ar?HkDF)Vl0v7QLqe+*Y>p-3Pjzb1#0I_U@-Ukg-&upFAa2Pl)g= z<3{0cR4=Mu&rf@w2!(F0)dGkf4xS%z+4+gEt3#V3L_u7x(6;<~8I9rmilu!+@Cpj0 z^_1Kq>&2y)#^)kmmc~0DUthf=g&?5(qm<Fq^sXKXn6|juUS;)V?Y-BL1;;K2?gz3S zNBEwA<)uVa6KB`4aWOA5_`TCF^DZG9jbA|4Ccu)tkhVwpQ~K_SYf@@xT@8+S;I!NE z$&p^zKD_jHp!mNwc0}!l;hiMAbcLA|Jam`cqg-u3+a>y1s^7r|eC0b#TeC(m&NR=A z_rngJs$V-%ui@G2GYR9MU3Gv|9W6C`l}cUQ2^u-=BKTb42rAUgiorHHh@E@T`__e| ztHEu1YSeC6-sU|;Uo-vy&V%FZU*VzG2eMSmz4QDjcTc|?phfrKv#<v-0&STgs`TcG z#j}N!IzNE*(`Xzkj&F#FOC`IvKcaOal_hzb?8b+~ZJ0)pG)qZ_{}ZU&*b4%}o>hLt ze)D<&naZWVAZ6;ere$3Du?Tg49y9%JDI2)u#{6;oZ_boSh1V*Xfs!?<W5RRS=*Nrw z-=kC=!S@$N)`^rGQ&D@$yBtE9@-%nTcE+z|4Cr3eEtvi2A5rNCq{1?~n+2TW&Zjh3 zgWG&)*!tT;DwSKm)nAeBk`19C2^H|K1FJ;8cMsPHk0e^XoPRF6nNWKpI?)y%xjAZ$ zm9V2Xvt}~Vx$nQ3R8(v6uWw*#33xnzH2^q#Ec4#<yyjW+u%9@e&F4hQMu7j$lz&#$ zXZXaf(L{B5Z<81uR<&gTY`gln+Dg8+lir(=Qfac*BH%`^(}&`mHLNwJ&2WxX%!eYr zTHnG<<HGsKRKJhI-|e7?YR@*dJ%|wQ+bMZQL2E$kw8aaA?ey*NeLF7#E-quOA~`Iq zU-VT2Q%~o!f!|(T&Tx0U29`KJo~@?}uR$!YT&t(AWncnV1sp(4b`iH%YaLXY=g;-K z)2g#Q`uAs=dzP|AzWMyHRCV^<+&Rh5q?bveI*w-;oXd}YL0X<AT%K|3A0Oswa5+8R zM{ugHa=?cQ5zTx^^d?`Cm6F2ea{499Zw^W?)x}9+2MQ}+AZ9ZiBtxE4%upp<rglV> zSWMCl4*TtrT1TnAQf|7TQkyriIG*_e%9=~&By(HIYRUWbI`jO&cYkI5{<-6N^;mIy zBzw|;=XwJ^Esqk+B0NbfACme;N`QKDbvg2>bz`vdzIEbhjC5*}CT<Lo&nna{#;pc- z3LP%Se93r|x<m~$U!jDo3?sE%EhAt5kl4mj#Y*KT2rBeltRc79&m2Zn*w$Y~Igb;8 z$XUay>8ZDzl6%hAY_*g~r{^U>TA9eHZtU^xY)s=HHIdBCuP~!$Rw63tSxsyxpQ*}C zH!Q$<q`2Q2(WG7rH1U<9!zoa1M@dWDl~hr^yjEyom|(Vl<}*{s$uhnF0gw>jZ0l=l ze6(@%a6>nZ_T>pKlZKR^>fIS`;pQv$z~c&pY;h3T&3h&yxktE#1YIUsuwROrv2(B! z{b}AbWlovM$#l7xoBz|Cg3i{RbWvB#y^6wKhvBgzFP2*-O)4}miQ5*$xX4`{cHY<p znavK2vv_H)JQBJe7RGVHk%942V>|9mMt5qmuo*k~S3%R)@c{}~62@*<UC{llsroXE zsl;K7DT@02TzWljCfWU|AFj7cZauqnot(ODHzWaXKcRe|R`4BKxZP1o_6NquNGkHt z4ky`Bru<9qRBoN0JPviW%jRd1*FLd>8+|4IaEi+f<-%+h8tAFvHw0_7t`(q-b<wBd zM{v6hg~+Yq(96e}-=BfEUpHee3R}3b)1Sb(_9zA+=>DD|j`-xiA+y<!KHub`6L!rG z|KoEkLPQp1X+C%%Toxpe+#;?=p{?q3r#xh5r*EJlCD0|W4dCN{kOe=Coajo3!j=Jv z`+XLF798Ze;+t>uI%v;<9>a%k_}#c7$X<AkJQz(})1=16iTU-@r;s<nE1>&B_TWS| zaGNw-pQsk`rKf=HfC-ZC_)}bLU6Tw}7JLIFpqkeKSAA4oN_rIOX0^$%QWb=eR$U(D zP^vkOU5mCvnA~{PfrM@qh)1wtMo!(%ZMyNFMw_(asSm!jL%ddZ8lofS8QIr$DFaA= z{GXR-TX3QI5zHIAj$Kt{OhwoQMjm{H1AcJApck~jQsxxlr^+X#eb3xJIBr`tr$Ox_ zrC;Ikdwn|OJ4T|Zzg#$4%BxuH&~YTJ%+G$*d0N<=j6KO-^J03QUHCOB5p?~r0m<x& z+oMbg(M6rFL1);Ahp{nfIsJQ>noWE=L8DINR>hp3Aro#F*jzhjD+H=P(kIMsA4FjH zosleX3=4}v!Stv>^pQVuc~<<oi98ReN&onn3O{s4jD8bYTV20{9v_{7TJ&=xp-w&T zoN^JqwjzOW{o=CLJ%iu&Xe>+9O}od&RqW(2%WFkBU$ieO&4lzen))z7HR#hvY%wJ= z%fkPIuy+8GrCHmx*Vr>_Y}>YN+qP}nwr$(S8hedx+xX|*``dB$7jfeJk)4&<RauFr zvOB7?>w0e2&9Jw=-);WxL3VJ|HNfn@1-^`C;iTYzd_V=jxK)abP<1##vLj~-$VW!t zm7<-^{I5<`tm}06enVsci<x=w*M@+iyq073<xbAR9MnZQM=qHCQqDEDL@jU29Nm{G z#3=)z+||0WRGv}05d<MPk$q#}%eOR+We30B>XogAU&(z>kTSmt6xac%Eh~mcqlUh% zq^e8_(u$^2S?L?4z9ojC*}_@CXAgM<JY<`?XQDOYn>ew`F&^<0*T-K<sUx==pmZaq zrPy696FMr(w=ho03KtIDPp5Wmiy9dbEll*9S9F5Lg^If{IVJ9-rbB=T9XF5TDrTBv z>y$1EM}Za^TXbFZHNo|H2S3C6wtmvh+1q27>Di(tG%g-CNbjn(oq#&QUH!W7c>(l7 z#_=Ouof|d%>+ADy<E3e-sabHib#ObwvCgqZW-V6)U-9#h6}X-NjODUDR%~<|x&=t1 zkq8D6gxqg8!!7he|KoO>LQL{DTsXZb_e2Qi1<~m)jWMhtf}bx7fvidK3xi5J)4bDg zX;2_h;&p)c-dc)3c+xXweYp={<^eRINqM=$VIzO3LV=jX37PpDr7Up2K|~sGDHpa% z2M*AxIHFKVE?V}MnPY3_PkN=f*~;AcEJst%-dxNpgP777BUfx_7;ERbtidsnOZOrq z?4=v?=hEy*wCKxGNzI2>tIp}4QEbV@X)c|gI$ZX7Y(m{TySJJBLM97i=86kxXdjQx z)j=`pqZHCWffMT#&UH12%K%BsUcKbp&$985TGZeZH1bpgG}I?^1?Y9fga*Lj(V2&> z{K*=!h332ql*p1L;EC_TtlmNx`h{{bqlhy~i4cqSjhvB@>BP(RO;d|OCLmlA0x#;v z<^#Qr9d7E)|6We1lEhwYe}rPcl}LSKi+LeWxD5NgEy>n>yh}G?(t7c5JQQerryq;z zYMt>Y^?I<ioQt3y-U4}GjF=<W{IacZ_NW#-UsyIDJmR3Qv8J<~cd1^f_STu;%#J~m zv|Eboc8E-M#OJ3yjn}cY+I_fEa2<fic5LrFQ=HT<9m{gYPzPnZuXkND+HpDK^8w7N zLZT>^DB?U~q$ZMv;&EiM7(dvQHTm#s&rzb9i{fn`gA_$L`g}+_ibUZAXa4r)n{3c# zVdPmM$KRh>WZm|<eeG>U=ltsEH0M%kXSjgt{@RD>)d(I%`TN51=Y4*r$=1(z>vcE7 z`BrcBQqIb~k2T9|^O7bt<%aCp$0aNdIX^>D_H3v#1DXtkdB^$Dd>Rj<U`x-rj^wDi zR!4II$`ktX%Edjjt90$8kJ3P<vbk(H90aLEcu@Yd)y?R}wxtq;C1M|XX7cLd{7UV; z%hkue1&+6m2lsf*wIvm;s~?TGZy4YLEwhnc78QCW?{|cJd<L0HMGH~cZ^|SVE~G^< z>vB6pF~X%_@Zh3oA<BdjS!O;ZvbdmmmVt}*@5xOJv&Op|)9a@vlb-MQ-fPTSND2FQ zf1|cGqnjQ9XxeQ3pdQ;ZlOAyb{$z2OPIc@`V~Xinf5PnDeP-zr6dgpl@L$qw3}O8W z0tWK7($4UMeX3)6{0)I&AH@<54EC>u*89=fw=r(0Pr2Sz-AbA4A1k4XW36{DDLtsE zF4Z2jB|cZPA2C4IL0mS|n+o51l`rGTL)$3KRef8!+I`8gaMuf5y^D^B-(4!mlu=W@ z_nf!fyMTr=>Ag(fV75c!lD?oD9_Stqcy^Kk6IBw(mPO#K`@}_&UfOXWQFZ`nAZA^& z5+DU95<Lt8zyR<BXOjuSC7(KoDAQJxGOX#5Q6-xg*}nt_gD2sJ0$r}b4p^UMArkkp z|DZc9%jVk*Dyp{iG<I-Xrbnbvr$jgpJLj)#6c0-Z!Z>yg^-Q`g-)-P?DO8n$QD{cZ zqX<G4tzb#2=G$4o)~e#yYH29!B`Q<E9odP`z^`_)hpOc(B=0#hxC4o@1!gnAt0Un4 zUeQb}augS*if~KsS}@M2fsfPL=&k4^wSTvAa*jt?`FP1CjeZILS^`UTsvbD(#pPO9 zsejY1WMNSI9{6KH$<=mKSxYq%P<P|Cw=o;jO~Z-=R{7VGGNRyq+vA90+R{udK8?%% zrP|Ex>E*U(bJt6E6-$@=pvH!bKN9V@y>jJlUq+x<recf?8WXog;Red2*x5yUyoPSY zoFl8~P>~H73XCK+uo>`)9ehrwtSyS^;mKTb_+fGpRZE=M6pvBMwO+n*kql9J35fx+ zSFbNsNg6HK%TSKuGTCKZD9i%I5p&AGTOnV4Esn&jf&{`1Zdhq<Q!PQ+ze66LVNPH% znne&Y9)9g8B$f%IV5uqEWfsOMXxynA>H)L}MJ@Rc8**(WtF)S07&SRhhRG-?2}!~? z(hi#B@U33{ZzMlUbhL%7ZS1;doHnt$x&~JI^tMpnw3E`*t7{V9rbvKpSG`MowN5w^ z#bEA~)DG{f)hpQf)Adwp_l1c<cPmFTI*G%>KmKI|L4^%_&+ZeC=GL=NZH-y!9fngz zmdlc9feo(d3uD7PLzk#yui=FhljScv(`>4VjEbtGtEnrZD6UvcSPO<KQ3uJGchFDH zW&NM_PmXjig7YKB!O0pX7Q;ATftVlmbT3)FF)%vEAXrg%4+C(W3|CseRugeRCy#~n z|F%jcg3T6%1ok^B8!R8*zuPA~2H()^dy3pKq;(YKCreY(;<WG%n#ST<YHx9@e9p@v zltvy2<!HJh7QVEI89nu!l|&r>C804m&8!`<D16zxtX-n2Do$;X5Gd@<gbPXjcn(wB z%t${Mr<xg?IB(Rw+S1CZUdKH^nbMhIIG8TCbfWKuxN6-zD_SX(2*EwB2&r9By|kqB zNTR8>6p_`K$zm4hr5AEJ6%+~C&P9AoXlO_%D|(%yOG7vDK_RcD-PVzT^0EoUKhODd z;{+F!aQBOB$==eCRIo@*qC;aYb?lkYKwD85^>39$PH*qM+8(dvUuf8>%fTGPPG0W3 z*p1iao0&|Ebq)-aZv(yW?rKKR#&HHl2hX>6yW4g~x8=95=zEQ?`tsId>p#tcqC7Sh z)*78C%g|iNvA<-sPl8z&(Guo~2<Qz^ZrdpIF$e=t1|!0dz?V3=JHR9>{Mqg61hKfJ zu_nU<vG?>s+34D)!v{gL*xY76d56j)|4K6&r<wBE9Dg9iZoomLUi;FVTok3s5QECB z;tqV|g5|eQ9{2WBC=E-9&jv|UxwTkCRBAE;OLQorCx_aMIr#2Y6V#1}87~3nQbODG zPCK40pQdkCOEvC(1eMZtCR}m`(93nI&5j~nH*ouWajAQJeWbZLSy?sEhLeI!<0FSs zO;8_C$hXV;2wbIx8oQ?kIZG9JT_hIF6PO1#fz-x7?c)DA94Qd$un!dqi$ea*Bp*)~ zpDs~?aa*$x_MBjy+K*+<we;!gj;wc6zcl9TrwZWSj+A7FZRZF-+xF|i<&<VZ7`!*~ z=7XOI$BuD`=}Bykyoya*Eq<GdBN#b*AVR!2{zcTwVlVG0udY=s#<OgitJjZodFiiv zJK6<W%xHR@L0uf4Ol?8)cLQqHu=2rbBa_GV--r1kVi~_wx*Qxptpz!rw?t+7nKWEA z11gB78MSiT*~1I6gaspl2@4e^L?vOkBQWuf+F#0eP%!wn`cXY~T3FbmuHHxBhuhfs zUGWIxm;BsnxJ0WB`3^mmuvBMER1~4Y&U*6LYD&lUbZsO0@?cOnhwqz<U&km=Oi>SB zy60hr_=Eeqv!Gs*M}C6QP-{N#<H1lbo-^I68}Pop9VG@u&zFgvaQ?s%ZymVQW|ocl zgagP&mW9@w5qFN4xS<5dnTcrg^++L!%a9;oc4#w$CVBF<EEzzXZHl_aR-?LvmQ64< zwmYR-r@*-(oeKw9WV<C3I>oS&dIDxDNCudPa+>`3n_%NR`X``h1Uc!@uyCf?q><Iz z`{I;-UpprbS&bmc*T67B>>Z2zsWOF<|4KPd6h^&I1oKqZ@K;!dr5PDuEVU6-uSB?L zuId{}R$w8)N_UN~5^u?doT%uN)8gBF0C2IfZG8%kS6tr*=Voi3&aWSQ-YIu~o43l& zNe1s$49enumVx57E>A%ktzCl+8f&IU_Tg_Gm7A6(M-QhFfk)IFRZb`YJn9WR&iUEA zwie%lo~etd({<>#vq4Hg24L=&H*df8Hp||g2oaJ4ZU(n>j4RTklC>~^54K)rqF!Xu zCGK?h&hL=3GzybjJLEeqOUmwYGt3E99ER~!Yf&v)EV9_=FB3n6Yd?rLYkST!<5?kt zosc+3xBOuyxLRb;2IUfFMpB1|-ucDc*HT7iI7JK}T<k&zwns`_l)(gFL9r@PZa^^w zNgPoXF161!7AqmgY$0%22fuP6uSBj%kt|iNewIjNPDp0km5_a!ZbF0*+i1+sBWdh? z3^#1A^+~(Ggu6cEJ$&uy>N4%x(|C>WJ;omHd)T@^#Flo`*h3T&M9k!m0%;8tGafsj zH?^!_rZ+*sC)r$6OxJw2GTX#ZzR62B4o6?ba!xcBCzo%a*{V`jG2di$z6>LQ9zE4_ zoqY+)QS|BPVe0F<;dy<}bA7);cj7ZI(Y$hD6+?`B$Z|M;-9XGf0qpMoG2}pxImWT# zyb5$ddXaoVdGV{-PxKT?^^a|7?yLk0M&$(oE)A--K2HI^Q05K#@G*t4`O=^?X0p?v z;T-m!-KyMS3`sol8S0Rdps_MD-h*;n1f(rP-;5cm5CPlCY~?aB^np@dW`X|)WCpBy z@KU-&_>;Px4YjOzVn{^OrLe*Ly%&>moC7W&vs(O2_0hpe!)n=KABTtkIJ+-F`c6i^ zcHfqX8o^+S2|x?eJJA|z6ib`p01FvJos57o!RlPgfN=sQY)T!v7y%aTWqQf=ofD6` zS13Md<S?o&KL!v#V;({JZj<15!6S;y!6Qtzy_vsqD{(}i8Jj$sjCPjAlqGhW1f^^$ zZLKZ3&eJ@n=5jTua%e&Hqaaj|y@8xnXWiAGL6L?ZM2oV#*fMwJX)NFR3~*{6mzVCY z(ToXC3FFz5d%7co6?@5<dg`6!m_j_WxN4EHjf=Z2$Y2L)xKGCtU2z;Z%!Fyu2r<UJ zMh$P6#_6Fgst;%l)LLjoUSHKpMK*MNT|8MInK*w^s@RzwjTBGKnCAk=efLv6zgO-u z214m>_5Gb*N=2WOxdsC!t-TxHmkiwiId>{X8<-Qp35jy;p?sacmT&7~b|W6?vTX&n z2>f})Uz}*gymdf^(+kq7WQ13?kNatE=Mp>3q;kavuQ-h@Pr{LBs<fg87?g7?W@Kte ztDKDgwc9&eyH}(4s6F_JFukWRHv9FYdh<QCdu{XkWiR(D!`kyHaI2?>EP2p)Rr2!r zkmi77bo(kJ`_spH*nKB2x|Qds=59R=&vE0#Ip}@BDkvyP#I@O#Mx4xX6XM!np0p&1 z$F<WHS-r-J=sZdPu1(?0DTVmbi>{26J(DmhP;aI0H!8;Kp+Fuj<`mjCZZLP4MeIz7 zTPp;bUe&KmOu-oZLhd(Ek?|H%r7T?ncgqerT)tx1`toXNwsdohO<?LvZ0qWcrKQ?x z_cN{MJs*Tm0&k1(w!H%&US8*q*(_2`j1KL+0<*5l&iBtxA;Lz}CGCrguZ3Vo-<rD) z4W)>wDWuG7cdG`GDKBmndd}HWS0yJf6%E{GSaxd+edWkj?cH#>C4Km%hEBU^D+lSZ zvBCQec#-k`rvbKGRbYgLuwBIaqI)>P2(5j-stoAHnW1txpi9RAUY^{Y;6&hxoM`YY zk{$B^#N0N8on0w_cq(waggR2UOs1?7XlbL7eJOwlaSR)x7`rfKf95!nMHQKB6uTbA z#)q+x+g@tzG!C9NhbGd*)6FgCo3WF~&(Ic{j?_t_Gso2H%gfqwLPS>EdfCv2=b3Ep z&d3<($r8;(uZ-<KZsjN#tY;RlVOaaef9@+eUUAw>&-mN7yTxTZTP6mTA=9s)IVn)u z+$0Kf(GJC86vXHGBbmayaE0(v1e1)LoJiup4w(sq>AiTs1<274K7a!S<suMsL-ZHv ztT6Tz2kXpVdPfFKtYzg7XYtaTp`<p}0#>5(#$S<k&_66>oaV+r(2(US+V6}eN=tow zXR6X?YG0;D8HbonTMoW5FiGPraeaIeE_`o`|6W$v4o)x{)Q4tF|D7qApB1KGi|@2= zz8&pl>sVie%QC4hpy#x-nuS;A7#GO5l37)O5)GD}9~K+u4zP#^88KN=%N#6`n_Qg| z38V&<lo3!81J;bXl0zSk%eIcdq^khlF#>re6LwL|*d+n^9kD4lw}bWeRUGb2{L7Sl zCP{Wwi3?{YLnIQ^BY;7hQpSNAui4yoT(uS|M2eyvvAT&LV46+(p|;xF0;-+%*~7%> zVDj{!LuA52^OQBWnwxEDy4$_(=mPfwP|3;$d0X5b$;6;&IE;p@yOe&5i8ZC+G3klL zh3Rn!hX5!AY%_DrWP;>bW<rh+M!hMoa;t3bl#Vq72{vRGs8m9NC@?W|U7Hw|vL3W@ zAI+#*ZcihBsGm+M=p<?>^0E8jyUh0Uyczp>G|`?VU^Y@ug@C6PQRY^_`fs1O4v{47 z(TrtqVpki2)!PQ6p$99voy>+XP98^?P{g73zEMDuBx3aympjQ7)pHBy1b6C$w&vp+ zL%q9~^_%Ce-O?S<y7?om#rM=;U?<9g&*FWP{m#i%YU-y05o>&>&K3Fs!W4}2wnVk8 za{?aus%L#`Wo-=)%cH0Zos#>m)KN9j@FO|o26K}0X3-Map_a{G71ZOT;1~rRd?A!C z(!s4hlfev)U&`3vPEr8MHX#w_JEg?{4_aGehsXkDdDk)e8hnLQLntABCPIo^?w?yt zsm@y}f&)z$^M?T<arX-FptM*6kQn|Le{Tnkvlw?+&|~wizXj@lfwu6uFEUp9O&)BD zER1o%r&TAE3_*n~6=WzvCG=nJ{WW5;eLc$|uBG{7q+)`@{sr3U_y^7c6Vu~Zx|@Hv z|8;oc%K<0tg)SMSlj@otD?3exUP3&gc-P#Jy>gf!Y9-7X7qkh&F!^YX9;dL6F!2Tr zM2D1$O=g~uO*$Q*9tp^x9+mXZ$8YCuIu&Vs68$O$(Q*0wldHZUbpP-%A~{h80r3=A z>~NKsH*pv34x<?(s;%wKs$DR?^^cUj6T0E}p$#U5$BWJbZcI}`*i~5y^<FGuY6mHX zqW9wH3%dG7Dp98nr8;aw;&?=mWdaR}6FNdMD-A3Jb@`zu`Pdb`13#-Vr!fIuwujvP z-9h$;+=Wuk&s}U8RBvhzUh{&!fn)FQ0W9)zf3i9*!*32qC5<kn&6)JTKcdZIr49A< zyEEM%Xw3}+Xg^-+=%l33X8%O-dJ&pJn=({=FI@PJC6hYvHl;|PT1mP@h@3OQtg>KG z$Gq0ak0KE89X%aZ4lg=|{WzsDdVm)Y>lam!qUwJv+QA@yf(rOqQ*behy3;L2#vVvg zKn;kSuug~mL`sB`;7XbyG}<`Xt!NLKKbsFx!c+Eas6P;bDZf=!0ndqf`>kt^KMmO* zhTzMD!d&n<mtS^xg^LRk@!SU{Do^>Fw!_~_+K+t}b*wewYsgMsc&pf#kY8?}kuYF7 zj|wkv8l!x`-<9J=25jrJj}yBC(GcL7?H1?lD~8>S!&1K)(K7(C4LZgZMJyUamjJ8G zegb?zS*ADU?}=OVzcw}Bsh<`wHhUcd&sW`^&-p(-Jswl6_Q6`Bve1km_EttB>EQ_b zxC}74l<9_<V2b>I<&cjo0t-kn@SPfFl%yx5_n1HmCX_S@VmPlM_z*Ffr_I6woksGQ z-P?(!4J8)2op!r651=ueHZAxr$E&JtWmj%qdmVFbhp&o<ADRoRe(a@5cco=#tL1x+ zDzyktVmMR>?;7+XX=+wrzR_TxI<G#H(#iGSc9!Pz_xgzIq%|;AN+;z=8;Wy;uo#ho zBVjXgBR-7VHe*OO7wJu(!nsgivu*g-HEZ1gBi5DvwRUgtkhIwkFAeSwh8sm}#-8dw zN~mw5YcsRuiq|U$X$-ui^x*X5{C*&)>x48`__Zu`L9yq%svNN25iwbA()#<}ZL9Y? z-Plb4D*qkBetP75DyS8{ve?Ukakr`~KT@q(*^KtvlJT`kXYU)%10poL1>xwFb`Okf z5B7HvB$l~T@k3HAeVl#ssdbMOpGzy*%65R#6rje*qSeXiUW74Wv)Rg+p~oXzXr?5L zEI*m=UchGUc0K8|{MSZTAgkjTT=3@Zyq%<*BAO?Cs|}Vtt>`KyF+dA5L_K4WChdSW zul^C<qzfs>dWN10H>YT<rywh|?0#t}JS^d(>g2Kjwo;PjG;ZD^+w+6p>rAW2B&(j? z0W}(j`$7GvuiHW9HXFr;jSh=!Kgmh{oK=sW4v+8C#p~bc+`=#()2CfQBzczD6imfL zfk0k%<C@tk+wA$n?$LK*ce;`eBm}ERJ!vZXu)SF?mJ%oWarKKx1--52elEj>S8Yj3 zKi1eY<jeuqLJo^XwK9#sGRg`&Kng6y77{P%Soo=<S{1XvjFg$jvTV!984dnurYj;Q zm((mE(i-ifkEN&N%^_XO4~jk+Pca`o<NMTDvB`cFWz@0rW(GX>w$b6x6dJQnd);UM zCoP)$kB|jaS)RAU!|e&G^|h}{)1VS>)KnS`9b{I^;n+7#jw#itR@q**UVW=JhC5A3 z*;y0|rY=0w7eB?)7{3kxR^{~e3QZ-Why`MuI;?dzFvJ(=ZIj$QaV)^_ed62DWHucI zq54||NNQ%dn&{L4y}P>0Bni-zfnrU4<{^Au`%6ODl!n@q<s5%~p~JT6K}jxNhv-zD zCgJR5`o?$8bS<0vxg|ym%Q<{y^?9#HT(<YmoG3DS=I-yc?K$=jVL3Qej7=fA0v~*= zYK0PIlH`(c6=VCuO#_y?hN{Y|ngf#C9=_|Oo{mTHZ9leyRTg7UFU>vqwwZQT2CQ$6 z50ym#t$Ylv0GDJ%1xAgORMn}LBYHu@?Nc-sOmbryl^F<75g5^$5JMG2X~$sjQ!g@w z*9<5;>@*SIfn$PHZD6Z@rbL+%c{!AXwwz<ie7uVXNSdP_FJdIf9l_3DJWLht09aC; z3&p1?4;c?~8cA(oVgNbQ;IhYq<evztA(F35bvWYc8me;P9DSj5K4TtT{5c3B8xiM; zY8=W_LTNn<!Qzv;@wtNm{(5+QuqPWU=Yh4;s;astm>asv&blA{4}V>lHrz++qrGdJ z%AtH4uW|yz&0R}zq%PF!ezoxURMhWXY{VFAS05hY6CYIgTzt6$Eu5Q7mAR2`Zq+;K z2n+K;l@nh^n$?)<dMctJ!UdAKEt@S3g{A$D7fdzb#I8V#2Gxw4m*-NVYU}$lSS@#K zIpgBzc=qCuO~jEm{$qF6Xr}RP=7zR7k0f&KMZNNn0{M_)5VeJ{9r*RkU5PKoAJWxC z%hF>@oQTqU{xC<xjFq%O{rLEEPpd)m%N#Bnp$vlNKGod|nx1868Orc9n=yumlPM_> zE<MnJtgdMPsmHIPql?ssx{3|@IIWk>szCW1;kH~pxc*DDsnB^L1Fo-Z*D<b+2@jwD zfwG`fSt<Xbvp5{BJ2>2Jl<deT(e@5jK*xEzDRZq%p4SHaumsv`=?*5BD{x%Nnmzk4 z03zvixFNdHin41m@34XLp4w=gWp)@$OZ;H>wxz$pnN=JLb*rSyQtX(&wimXg6vy>= znalZ_vDCqXXsx;6Q;mc*P2J@x>ZY?kl%ti@Vm}g7o^rS?o#N#8aGD>`NEU^bpvfL6 z18ys#0wGISNSoFLvM@rv>K-_P9F|fcXCGA)I2GTg9bcS3*#lvi5I+hYpp+m5vUduo zfgTXrFEp&UO$MPd@M_Ud@Io|7jV8?k;*8>Iq6vv49?9j5(gvrf0^2?d1eIBM$UXeV z(_@o~i8?R^l8^IFHcw|X>r&SRkYC+PRI`1_$9_`%hl%)%46vS%_>*#7w$P4>ArbP_ zStN^j<!usGNr*8>R!Hl5A{QZ*Hx~8lw;;QXZL(W=59OHH>$@`gP+KI|axE1=F$mGz zJxKTj01-9d5&fJ&`DIQ@N!Vg@VSHr2Y!oEUzyaZF?3A<kz_dFmhnxQd|I&|1c*j{T zZ+p}SJU<X29lCajT^M&vphUx*Efbgn$z?*Pqmx`02P8pbgU%&@l!s3@|Jo(LFL?cH zcoeKy+qZ*QcR&;fX<W<DmLp`jid$Jql5wt$#0b`AcA<(Te<H|3UZGTIQ4Ya0fF!Yt zBrYB;(b$1zq=#uSXk8>dPA0jCvl8wxBvUSW>SPtreWFvZx>n>hEx(K6w?QD4$_XsE zMQ&A66jKJ990)@AkSYfpi3UkPjTQiA#~TQ_z)Tc?s4$C#R++_en8XsabpTMh4zW3{ zP`azNKP1%m+p&1GlQ}|$MckY*Lz;-YKg3EqMKQFT4aBVI#^i&1ADkX?NwCW@2%}TC z8#Xp`LPU?y>aBYfHa49ycs=OU1n#4M1tdg<(b=;Kfv9dfEl@n7Dc-m!)rv%jKLJgI z-=4=xw33f?BL<1zAnP|F+O7*7F4QjwH6*6p``-cp_*!m6DiD!w)L}b*dn$MX!frS^ zvG3$GoyMQG%l9{<`VhFfV7)==eUSNM6+!jZwAu8>J<y%{aMM0i{eSHqmlVTDkp1R0 zM^x-WMn~y@6`6b}fP;_K^MX%cw8RNRzZrOjbR0<8Wnv_Iyu3zW)4OBFe#BRz302C% z!*|=JZd+Og**D$SoKrnZ-hsM9M?7;#uGb30=Ae2+y<%UCd(l@ArNDX-B!YZUV5LWl zr2h5#;0=5d^JlSox&s(eS<L<=)>qmi_qC7@u(QmaH+Bq$hZN(dR_|QkV1XRAFr)Eg z3|a<eP8V78wdwiT#R4z1n@VCGk$j>JcfHfEATAge(A!5+T^}CKOkckg0KF>CK(tBT zxDWu={Q?$-#0B9|{T)EsU+MOAvArW5z71_yu7L5dL&bUDjeCFO$!VLiY1s={Rkc;I zp;NhhCAWX|)@>5GmJNoP35RPft@Ga71l=v{>yJhj=#z7&Bm}8iE>+ni0;5q%vRMkF z&?G23V(7-m&?5v=<Pn_meS^S6#E28yE7LLHBV}_Mj#GNNq~Am!-)BC~FA+@T=)|~# zz9Z77?jfOBL%{HISQOR+oK<jNjtL+uPt%NUhw<jqhrV+RF!QSyf-RD^<{3<j3Mr+n zrbC3Z!?e9>Du!i%0{htAooTP+49;pmI$f-hw9u<rr#x8_3fr6jJ~FY0u1-RYf-^1z z1ann2JXu7<Dmv_-g-&pJ+Ck_7dnQzQ5`6GdO?}c<UEg?sAO;1V?;WJWgSAEE50Na` z$`=7kFP|izbCPf>w7Hj!#aVoC4YZLQRSoJaSst44Fj)#bjYv7!*}|`uAxVV7pl6&n z@Eql#YB`vAi6*~XZqvx$lp~j2?@f_xfNP0d-%C%OLpx_`5>Y5w+0orwZY0wd-OKi- zda2I9=Ie}D0;O<2u4^wpmlB<#3~(pv^(_PyasoAbL5RANUYHHdP=SEFi2`YgwAHW) z54-Qb4vKUM%BjMn3-4wOEWl`I6xn`m$WLXa9u_0#hi6i8bKAe7r>EWwAFfu%yD)jX zz8-GhH)22AKh=*<r(<k+0XhMk{Qj?W#*cj6f5R+4=*53ANdlBA#tuJ7hW>w`B=#R1 z=N~-d-&7LAKPbfye(`^yk{JJ|;D2LDYX5@<F*E&875}Q`e;B3zY2^POQlk5RK}yt2 zKcb%0j6XV&^h`{w|Kfc97sB&@aG(F?lvo*={udoW|KH~LM+4=bj{Xm+^k3%qai{4d zXr}LgN6$e2--L>Tv++ML%D>0=e_F)*mzjS|^)I=TAGn2)`5(LgcVJ>>!uu!wvyYMG z-}!%1x*uu5A2rYaiT@n^PyV0u-|zl&-hb+2VPpCc$^1tH^dGK?h4DXaF){ozynhE) zrhgs#G1pItAItr0>Hn$kpTP2?Bl@3m|4CWs=<!&7t~m2g9sf$%S@8ZD#Xor#Mn*ga z)_?lS%+Bzy96Rg3_p;FaNF)Aho-i@6{_9*u_Mg7}l>etB+Yfj3Px=%8*QNSzibvJl zhTq1~{6ARFk7dmr{>Sz|lkC5-tpCLs{lCE;R@NVm=-*yr=zl08R)&8~f`1F*{75BM zsleDNsUq_{U-q<S!o<Pg$JGIBJrU2=iOWMW(J3>oTKS3h|03Unk#PMJLRC+0j`@Y5 zSzrulg+{q%5eYH~P0qZ(*a~bN@FA2vuO<&gVJ)A<02Bp_1mDkZr`GHEe2Fq&u`rkF zb-m$v^+S969Cs1(3lWwlS}8rh5-*hS+P{_a7u>p-4MtbTx44ftgoaxb3hlj)G+(|c zT#q|MeFA(^Qm$uc=0a;xkSJ`CD8w@HHs~MfZY#j*b04MVL@)~%To0*M3OR$-o2WoX zY#6iW;yd_Yf9|PSzI&{_%sM?hBSu&F7&#jb<xIBAC~1oykl936*)E8HlP>p0y}lL3 zFP9{;l2GGo$nhDyTdbdvuIaPpfYsc)t?x@)`(ChlXPHsGwNP0@x9k12&q(sw{#wEO zqnFM_xLvw%sX2GR{Km^I1C@IKKl<a3^vA7UmYa%Ggh#zhQv|a?av4%ILC`bfNp;EJ zgCd-_##!NfG~ysSp&GkcX?X&YFfM&k)HhW6<BF_y#h%&-_6JsBk;o1|!Us(bN!%ar zClRpwwdlUxO_>2mNQlEo^qP}}k}KWdj;?Y6pme6-mOiq_rww)yp!F(4!}sH2#x?VP z?jQrSEfi2mnNJ1|Vb0@bb5@*qHIUK=kp>)mqbEsIQeWau3-2us#j(o^Ko6Mof=X*1 zR*Ke%N&6CT@`qV(5$`5bS@zctvc48;+=s?J<;AYD=~@a_8;47z(pho`=(=ijHdbG0 zU32YMS@MLUMx6~=u+q)zNc_eFR5xIV!LYfb=Z_)d)n6~1?<Lhg|BhMCpKy}6hfq^s zEa%cy@zO8on$4p%j@+o;k3dwg+baC5rKAm&hIN6k{n0=QYeDl@Hp%D3fHf3u-d&!` zm{_1vJlG1iO5MI;Z6BW?+Z`8S#H60ImJG|7fn1fd0CYrvWEV|IO)vB=Q9#gZN=w$( zXNt182#W856MahzT(`j`pptC8Q=rzj@L9m0SyD|;hEMEx5L(sq((KJt3GyZ^5MjCl z5uyRaL^Ps=7&>E*0T5v;$#k)z(Zw-);FF5Q#ERjgifWaQR>rLi<(R<TxWIl$E5rvf zshP=&M0d>PI<FrF0;zPm8rjcuZuFGDd6K8?70j0dfQ|u!whEg9q*k^o*3)oTA19^! zHD?EvyNaOC2}JGDRVlvKA-#rI3Tn`nHs+g&91B(;f`<{VQta}anf$0A=PfA55jLwF zN|nqEK<L7bPy_3(*k7ZK_0Xs<QnZ%X@f1a3Wu6CK1m@hL$HPMRD6&~J%nzhU`Ld+M ze>(yxj$<pPr@R?jDb+k=eR6AQ6j68EKg`JtFIkw}RtwK+8I@tjrDJ%)d~3%wpL?OD ze_}(JQ&t__3NpafOcSR3`3FcFr_n@n%qi5+0M=jzx=bDGBWpAhQ^NIpUK_BOugdBa zw!rWEHH9y!oabZ-z6>l7A8rrnz=eB_{ErZzJ*C~K$_kr^G;;dQ%g;w(;^N}0wg~wo zEju!xyhnzT>%Gh&1MV6AD8Bze>?@FKJ{{)|6&xm;G4CxH^R|=B^v7}A=n;s4qZIuU zFEnfM$P}0F?2)`cdCFeB{luYQ$+m6GeZF@qZH%PbI^8CZA`Gm@72N5SmeQ}DZeo$k zf{;9%FjKX-d9@q{I6h}Wgh<Wup(0W8G6^(;i2|YGm4+gGww;kwCrNL+TmmHDHi{&& z5j8tLQS)RC;=;q6Y3>SRz$?WphcQMu@kMgcO9lM*=+A(n>GWj?iwji+j@HH9ep#rY z(R6Q1dLKBYjSF!P(H;symvzJSt0txKek3d82PjY6%O#oxs0#6!j`36WOD=0Uo#O7N z-=Dy4Aa90fyv13>gHN#Bkgpvb;T(Ckiqg9QSif2T9{X^%z}(QyfDPF8q3eEm-tc_? zqFMVE*mn8|Yq4xB<D<+QJkwmPy7boqo6H+rQ#^T|(|>n3tK$_>WX636c3Qaw9Ekz! zEi!p@lCV%ldk1K)xKygJF1j`sR*E~!qewYpA6IBq0QzWy&4lB}8Gf}bGr421(yatF zFMlI=T=Ny<9|LWU31|U-HR_D@fG@OJzc`S0Znt{o_$yC)zEpbnp_60U>HjEtqOS?J z{5WUxsFQKRCVprE6s>AHFZ3>to{D;je~UeqyZ@0rZ|nDIlDoiD|2bb0>)%v=CDT`u z=0N8T)CQ_GzQTfK_4ktQ6!OZO_kdpy?8bVBsZM=Uw!1Bpl-di%o&#(Htkaea><=tw zX4W}*f0h`~xk;Jrj#Xk$l}}{BnlgoQuPo2IzzZ+%cBok)ix^`z@eRn+&ewXHr`kJB zpI3RvMlrXHtNG{&_hnkzMb!NHDVx1j!(I96Du}L>2ShKlZoua%A6Q!j5OxZ+?q1q` z_c{ylJ6)>X@iqQa;u2z8t}DRHG}cH)x*D&e5(wLjYyY#Jc*wz)gU4h7_MOkXUXHq+ z4j#|GKSZCFUbzKDA|AYIJ)9aiG$77W9^l6wtzHj<DG_Tw+!5%DZc$B1D3R%EC}SNE z*1p%=P2~&^(FlIbvHx@9S~;8>qF7%p$u#N_!Fu{C=0-x6xa1L&+f0unZ-04|&=tGZ zBA&J0ce<{5cvoO7rrO%Ie=XIzJ}TRU*M!)#_y+Yc;c*bLCc_gRuW?Kr7GCZFM_XFT zshW|IJ_RXO=Sk$e%h8l&lhh~j0ha?i3O#Dv=w_L|VAF(i5G=dn&Nsx_vJ24+IjLFn zCqgy9^tw%DA=dl#3Ff%CM~J@*U<38#x2OR#7ifyq=qq8vOawpfR0!2Dca<>DSN1hf zJ!Pv=JaK1xiM@8bGfcBE0rD~WH%pD{NUOV>R_Lf>+d&4|dBQO5Vd={HUG9B`W1?f` z%uYsj4zHHUGU2s?Gt%RJ?gMgUB7`1o9q#@n`4?qy+I<O%xXx@y0&14TT|ft^d%r#K z3h)P&J3#$f`WK2FOx*2OMnvT#w=M<i!lUkGX*B~J`_&6<**W=$z217;6{nB1wwA3C zPhEEw=47^#^mcWRz_zk)RGOo=jl}5^c9YD}okKY;<vF@VdpD_a<AJLIAyMrV=~8{L z3wy~dgSxBwvy(-XvsJGl&qaUsSLCM!T*Mj2{x<t_J=~MM>#8=2`-ZkVnV!~`lJSNn zprP7)gUC9cAAGz?MTs$I0tfWjd<4IN7@`$nyc340l~Ni>r5O+meVKSRR~D0X6tna> z-4Yq(h)AW(Qs?J%L!45mc!}@vh0pW*Rr_<+4}Wv}K8?&1`9={zIQiVMuea14acwn} zF>J7db+W2Mz!De7NqV0Bf??Fq-n6fW{M6?<%vv7{-#AKNCvrjmyBb@uMUUBd9>pVt za}p=4JaiaLzp(}Z_I!D-gE%hVdXR^_K)3n|J;|~k;ZN{oos3$fr?TC*CVupAx5zLW z5BN4pjQ)0gS+oz>Z5XQez?|6>J4Sh^_s4qT>mRGiSD+*6%0SXvbtooFkgEJfxi2hd zD1w>cXW7BQ3`;WB!tKv^jpY92qwk1&J+DSDeHN3~g6L4U%MCG@>999)_6C|kBCJ5? zUG7i9Hqr}0DL05yrzBWB9}NAFlJcmdWz96o+<qFr^?k9>UpD1qzD(iQyK+d!rqTu< zO!!v|uVa>RXIBMLo^>u6p|z08yUdKd-yU8hRc`@>TVHn9QZt2Vi>$%Ydp|4PU*KOP z1=5Ua7xdq<`b$i>X-)S|-4S<V?NHv+d(p30uS7Az$Jqn*V$(3c>O5kc)plt^SJUmC ziG1Q$)S(=`YkYnissNRTJ#*S$I-#s{{K7oT8+B_Ho=!@mJ=y<C|9J%cz2uYkGH2}_ z<QXz^fYZc-Czw=s__};a{^OOx$8~yhuzm~Q<rC%JJl6+&2K;lEK0ntcqllcHYXqoB zxThJC&{RQ@==q966_4a$mj1-RIaI&5K+|W}tHSF`F_H@nM;?QhK`eOO+@WHfAeYYr z?o_+THEv6UBcQ34RoMr?+_YBGr^oe24mBI^yK<D~`ZPS%^}%R-=CSBcuYmd~KKDz< ztK;3om7BY#Ys!aJt7eGJ07}JWOg!Y&;9Z?*+I^RZo)|{K5kFvP*ty9KNwQ6w4H;|O zPH(u=lyh_k%{#5)Oq*+$L)HWjI-58c>|`<=w3^Y3pKBfu5;zDC(sZ_T@^|<F`veLa zTLh*)R4`n@&GC-cD^L+Hi1yt(e<WAH1L!<0Y$%-K{W;MG$Le>4Fl?7VQFl$Zlcs6Q zhf%5M74*>`&Gl>tuzAGG!@Crh$gXzp{Ee6erpN9T)U(cjU}w~L3+-kJYFA{_5BFK} zZ>p2T^*GxBWLARLz{h^RxWq}DRsESH<^s(aSCetB;}pZ{^t-P2$#nIScJ!kM_L4=W z`wOJR32T`YX^3!f5z`N<!cBP7rjfNMlRpM@XwsnBJR^7(O`tKT(0pb546W(q1TEo8 zyK8I~ptMQ?zug$!<dhOJZjNBwa0l|F)ryj*pqd$4`6vfln;LjygQ}3_>br=LB;#l) zAr6g|p)Gdv<!n@h#g1#s*JQ^QmR2weH0QV{1V5JfBPKn*37M4xiE_BN68u^(hWY1P ziYqDgH7aVRB_Nqqs45{r5-J4SrQAr$F&gTsEp=LLw3nf+dt<7%UgZ9K(f3dlUxZQ$ z8j=^k)5g%Z^ElBLCkci6xWgjZ&!2Yw`A)?!;>J0(?J;d1!vp=Dv>oz_@29d7ZYqpt zo|xqc|1|)ikw>qQgXorX*U1M>xl_?kkAV4-ZA^o@L;F#lR;4n?xrTPv05xX2!swHH z1HjM)A|W^H7p6$i*iE5;gXS*t@hNnWka*eBMs4nf|FZ162HZBxmgjfY2j3iz`P*7A z)WBGWxI7Qppx6XNJ>Q%_x=Gw>9#}jMbU1IM2q3{PSGFz3J&5-pu15O6{p_yqeCiL0 z^H8=7bwcx)H7k6y*d>dl7DM<x=weKKem?vU2G`lzR^VlS3){QKCc%Rb@$zoORPJRq z=<$f#`Mwm1>EmGpv2aYhSZXSfs{qYQ+_HFd{89hdlv5d7-3nsn2Q>t0MM8N4Ar^FO zD6;cxKmYhWCt85v+HatsWJWz!>UfmM4q_9Ue2$JG&-uX}iIS>{<k?C~Oid`*q{dxZ z5<^0buHNg<_U4+ndd>=+Od4HLUN$uvb_1)AGt}oT!No1;#{DYhDFOt4n&&36&j1?L zH0|hO&<}V@CB#nAXNB__9>bZsIN?eo&snd)nS7;{k3dg1{ghe2BmFl{=0PVW!Ggr( z3^;v^35aV*5Y;c$%^dm@(={U$&L5P#IHv=GZ@p3aOiY4GyYMN?J<@3l{)C!kJGOkR z`e@$1M@X2Uv<&!>j~<#^px7^>AiR%}z8tSL1EzpL-+r+I_I`Bk3JZzC1QwiKgdMXO z3zHMACoj!O0iJCYgcIB%{<#0wvxh?iXV9xUHs<ysJnL@kHcxHN4pzbCk&ZNS4JLRj zB;;}-o0vZXacNY_sW(Y(VLHpXrgSx|wZOGN<%~o%hO4c=8i@k^*6JS@0P{cDdU&84 zRm8EEE1InS-&q$N289@i;_2Buq}eD3nvp1pU0R}9gA`(?<c-3S2V)b|NJ;ZxUZ`(R zQK|0|65c(xo1ds&hs0+i%+=MFIF{z_Q86%;r7BfgKWb%s3L&ux2g)!oyk=H%qM_lZ z8|;q}9{YR3ivcI}3nRA^L(m`*R2NVYLP#(L;hz3!&l%7qr0f8^`s}oShwQUTuQY0y z;2{Dy+jUADWn4h@zi7v(g{ULt_C{?<6NAriam{j5b7vAE6GYu;o+^fMu4#J?_1|#@ z7HVYMpvCK?|FBroNeC|*G`2;Xr+69m7>6Ier+|7kYQrsB#kljR2qL;Bc$CRP^WnwI z8WsP6zm+3ca%o`?8@x@T5?8!Rs3}xf203Z%2#ZOhd8VyZK$v72AzuJ44K-m$W%r)O z$RBA}Q9FkoK|ilZA8uq2<JYIS%E!xHRZBD=mzr|Cb;%6NnQa5vt}Y=Bvp1h8%+$2p z_FicYT4{wR6~FdqF|mXFe0)sCRpt4*d&aT$u!Ap;L)GP2Dt4i|mN4q{hAbo$ZUcnm zDh2+Ua7peHvCrOhcbvI^^{l<EPksJ%y<6{UU0eGYa~YuCxAM}5W_s&SFDgxsCPz>2 zLI6mNZ?SA6&bCHDRAfy+><?^35FwR`L$08iP!hc;lHagMR$V1D;9ur8`f17{7uHef z-pI&V`26+#{&oGGgHc@ujR6Bcy@WOhAf3<CJBwYjsB|U)DLFt~A7o<eCTzuhwpcY| zQ+_4N$^5z6eF!!iW}@qEBR%QU0%lQKM&>r&pG+5^$2M&Io>ODx;KXSsYQKDIAZi@7 zh#;gLqR~fXs4|>IMk0)dRzzr1<p<xOB%eQm&F7L7KA(N9ZW6ZQZ)u#)<yQb4x4Sz5 zmJYySqSLqn&#tYt+L3Slw1NC;bKU@xYg_)2a{{A+ips0KG~aX8`K4Dk+ig{9d%xZ4 zt|=#Tw#r*;X3pcy)`03e%A2%d-_D|=d&&<(Y$-i`5%RVMnw`VPWy>^dm|wQ_^AkJY zL$#_c?`7y{)Hk^^=vHvJnVLp!a~9i8s_#i68U(rkq!N#zGCf&<bb$n#0cZfcQFqoX zGZ^PLtw5f6$WfeK3B(p=A$SA<T87zPr+bzu8}%!6Y}_<|q+FK8I6+)?T>(KHHM6q5 zT%zGyY~lg51WCLmGIv}mc$CsJ40|%lE#=8d^n>jI$s&HPs5V*Xi8@>OanUyfmK!?T zPUs_O9lm+CNO10yv7nV$*l%U4WEO)mVg##dedlG7mZjc6?y<AWr!2;vj|HAJ{`-#v zMW+_VZ}iY2dLitieHk!YP!I@xuDzv0z<q$dzO0vD`a3;k1Cb1~0=6$CP3Tnkza>R< zF2e3yn|PjH%UiE%?p?ckxp~mXTqCy{P%T)pYatS@9wbCtk7pl~wSi75y?d?IGWq7b zlE^fT@7asq5}4I#A_YaO5=?FPgHB>~06~Uh1U4$m*e4^0>$2xF2xtaSlO?sdbv3uL z{+igjw0gc>dJhkN^bo&447PjThZKrnQs+2pRI{6AHN}^6NSdxoRI6C`+Yi(G$&5<k z&Pn-=g2NcW8ia%;3zgl8^AhBu_h4+Kr(KIi;>ZN0x1=R!M%2|<Xm)-LUgLhYl~<ZB z@ZnaxkllL3&_0K!r1z>}>aNe<3YItP*4Vjy>d5Mf*O{{GXvMX%oLGoFhxqmyJa!Ca zz=(ZEy2{Ue<&RD!eX#aBKbi(3PXs1kkCWadPK6uk<P2BsLv4oo?76O7{9aHv>2=Wq z1M>3Gw#c`S*;mKn24H2&!OJZo<B&*Yv^=q;*LK2ejJz_0UGPNvErvo$66yzxHiMs# zUk?A<Hb8)lKVat2EoP5CVxazXSeA&eHdcWKjV>ESUn3Mn9@05sb|(Svcc0T?k%Kn8 zR1Lgkkx0FKgp^Cc0ll|}iHQjID*DU&CEMSzUA2aVLxP=1Eex!W^_!Ll(e(rQjnFL! zDe3&U4*+LJeN-FiZ^>KhzkH9S_!3Y@{RAr{<rdNIgj*)7{?D2rO@aeDtd<O*zJ9BZ zE?uKvA=s_!=<7t1K_uws`i63)hFyg#PUYNeBR?)Rx5Y#IamsdP_$y;6c@sT*QktOO z_%1?)A-O3`OGIgZBr=*YdD&8V{Qh=-E~GpycdwJ`Bu?qf@pg<VZZcm{+uOt}2m5s8 zQ(td{SDgKR+uW%<bUs|1DhBaaa=7urx9p7iI?=XXM7*mt7gQnm^bsr+?>jWTy|_>* zhO*MkD((T|rVYUE4gf)k?E=BjOP|hyS%~aST4T3Xs`B@5rfzPBz*X;y$TD;6<h+$> z2)v$L-&vK7vg4RTV!dREP8UlgCoLw<&M%OYH6s%W5$HiOVF)gfte-ELzfu@rmyk{7 zOGtR*)K^AHIQs<w_y-P{?Du<U<zNL;7#xMjrB9Ovtje&d(8PEFd|*K?)7)po$LVr) zr>o7SaEvM|FYVGb1^7{Os_-b{IjKbjg7i{-VtDc>W#qa;YARe3>4>#=cQ@Jh8py#$ zi}9=hUAW2XYq7J8DV>6{{o_e^rLp|nb^Q``@{9@CBMRjrU|moQfMkjvXIaWe_Q=yl z^^45zFCV$QYDB7d=Kd2AcPn6=Y!^1Hgu+#RBLL!yov6gOSECRZT}`qKC3QZE=~MZx z)0O5$8uwj;H<lb%^!iL0-MG-+?%K22{qFA3QA5tR8`*CnS?emwe4CMR)``EoSzYSJ zjZ|DImlZy;{D>;zTOrC8b)e*-lE`xin}TrtGI!D#JYf>Mz_|idd&*88l1aqs(1>O> z(Ax79i^>?#Y)N24!H*p{ck>6Ao~&c@PJq4?-r%_fHsh@a!fZL6y!WPbjG;^7CKSp+ zl5TJ>s#*NxDSF_5ibqU#fG>p6W#;ocY#^(<$?C}>@L^$cr_ZFHx&EZQlC688^Rj<r z69Dc4K5SoA-oPgdZk})#scx17v%3OL8N8OHXtxr$_>@_Z^#IkcHlmkXx&SP|8bdLR zMWQ?%nqLtMJX<kcDI?2&H6FP*29XT~Zw{pKK?b-75Lzb&i950O3))1X0RPp8z8B|7 zVw9LeS(8M3NK*ABWiNGU4KtHdsT%7N&_5)|do#joK;?5W0Oc;(`G5f(>~1JSz+qme z)%8*5A!yBuJQ*X}(}g^M389>Eg6>j4mBqu*UskC6Q2mI0GLAkGHT%8xJR_NTkV+NZ zK0R=jaYhMtJ^tNqf55+r4qZm8{BU0ME1trDxFr<ZV04vLJ;Rth2miP6!43UIgC9f$ z0KS?*)Hq+R5-1f;xab$$&0^}E+Z(q#R`k9+TaF~0JnKU44vq0KAOc6_v^xHw;{lQM z%@C0RGwq$B-mLBrxX|x_Wqc$Egcodusd$6sg?s%au+qT_#aOhYm}?fHu+?DHqvP<) z`};8B8bXyX3w)nkUEMw|q@$F{Sy6>L>C^rYzFzte;IDZ((~!w83ZL!wMBR;dAgbp* z-}aif6_RO2pX*WYSA_Rzw&_sBX_>5*qsgq5gUGlQ2nZ$!O*KGbh&DP<9kfqyeZIWt ztRSS{PEN$w`Lhu<Wd*nyO)>>>17<De?}eBMVw)V%<%rnAh}2S=l-3$I#xtI<$|`?& zvT0LQe;AzJ<LCbEw95=#!58r5v0WM|M_{~Jz`T5ji3blPLHw*NLr1a-EvtoONQ4r2 zO(F9-W-@Ju&PzWAd|YLnq*5zWB-QBZnUoRpoHewBh|6gWA11Ipz|NZ$kpuAP1!|`_ zg>fA+zfq-KGI(PG3WgaCy&u<cSB{LR03jc3ZuqE>bO}`v9wZ)8_f!B?6-;~({Tt|U zz<q1#$^09LJPe~#GgH2U&qN&&9kZ>o%6H}_ygVO$K%0LCbEj0P)D_){=eB|o34Cy3 zBh>nAqS*P7y9O*4X`mwkLsJQe^yCH75d7m9@)3yT;MM{_=Xicm4;TQlP{cS~Q_z_( zUJvBkLk`@7?8uLsphMFA5(Q0)n_TWA>YVNKsRy7%<fKOffffO<MP6`|o2*BG?Yd*E zZtP^u4+Gz~H*p8PtxL9e=i6&PsLB7`P!SS|mp;lUI;lE$T^G~x`yK$4+&8ldYz;)@ zw?dpL66K``ke3)Vd7#;EH<Icwchdilwzq(eo6FWVo!E|<F=ofi%*@Qp%*@Qp%*@P8 zF*8GA=9rn;o=U#kefyi~d#BgGX4bN*<WlLJqbk+bmh?XRwX~1x6eOm<%i0pdAq!b* z9r-k;g+n8PGeg*2B&I_x*+JX6jBs!NVKR1lNTEPnIPvKw@hP6oS32(^51HL6zCFFz z^mJKdW!OCbYRL3fR%96e)#b*BA#5to>T*tIV)Coa$?ZrU#kk4>rH0%?#+3;T3**8M z2eqv27`|aeX)sutQ#plbu0-T)c%il}5FiOmwJ9gsxVl3|u)~Ft7esDN(&9ZR?YnU| zb|ZI#*`uzS$Pm3Oy`M5ZlZIFtKh|7s)_5l5I6Yq%v)(c$*Q+E{pslRk>@8MEAL5p+ zE?6G!Gk_|}%Y!9Eb}_R+JD26R9_}YER1cdH<qqyi+8q|YF;y~`Zz{5soj@(3a#a_H z&z0ww8NjY1%&Q!CIOQLRCD>I-NLUEYYwj50*tLFfVm#zs6Rp!ewP*FzR!;mS*^yi7 z7UfHc(3S$CJGdXzr<T(=C-bF#deIVjLM*L$#!T)us=bGIwXQ28TMbZ{2~)EF^_^p% zH9NI+rQy7T2D`+5eZ^CWF~{EhZVHU4mGOqo$H0!!-TCqE#EMo-X2NT%<Z-%}L@RAy z{6HAVITgqZnfg_KS-V0f{e`d&(mL_83?Gd8xtRcVw#{(!xW^t>MDWdn(jGWEphg<K zDMVNR-CdGQvTThi87$z84Fw*%hEBdP$~B_$=U6TBuo6RwW5`{?VCfTbQs!d%({_IF zxN5Z$CboLUY)uu5v-xUNNfRzQt>y%L04&B*I>WdY{tOPSs``W!nlSaFB8^9>;(HLh ziK$rH@I;!~&BkTC7pZNb<p^%|g<8kC{X^2?ERG^~*e!ORx5K^Y-o9;awNIgkdH?Fc zOo$KN1(V3^{bU(dM?-PD`Q0~_w34f#v=h-vmI)TzC+X_>S#;A-G_#GJb{%?W*iY9S zRgDPJ$oS8nYvEfEik_m(!by{el%z+@(hO04Iqx$AWrht4)bzNdBRDKXUKZVEEmK4O zg#Bp<5!{#l*@YHR#>N9H24eIJ92g)K1%-2sOArkl4bh|B7!!se8f&>+e?|3<Jy?dQ zH<1upq0r5~?qhvLlNkml{)9Y0F^j<&C|oN$mK2X#9dx?yjD(*iMS7V8l<(EE0$!?- zo2FsxvX7cB2yE)xeYFV=zMyriU8@uHLQ7YwOYT-Hkq<i|#;#nXzKYQ)zY^$Cm<aMp zws$JxF@ocr<I2mO3l2h1X&WAe<|>w0dYUL<gcYAMu8PxyfZ5lXEBXyAnv(EmME_2= zAbE5ESt4GIoF6UO3&oan6^f^AT{J1KHR+2Hl520VETnZ@fVE(y9Y~OWtZ^240vU7Y zs8)!H@q#5`fCq$p+8FgKG{JS@^zavk7lhJf^Y1p&8C|NA6Zzw%Uj}D0(mrxKiZM5b zeHbWa?<CQu3UO^`P0dv4XW<Fx>ac4FDTJ~^;~9g)pbU0;*rRo6kO>icV20+PLdO6> zu9sHxGzOh#&5;sOZz>=!q$s+-lZc7btc(H=;^}c{8PStuUa*87CqCkO)a)0N4bRf( z()nzg%%PBbtb=WmCVAb8Tu%rT)XLNS4)^yOSCa1jx&~L9Cs|WlGwSD(YRq%qZ;Nu6 z;E7o*3u`ylE=AGgB;PKC2;_c&AM;^Bha&~0RPeHY@=OMcpx-g}U}y}v)v^+SA|ly* zR#R4&>w@Nw?M4+&m9k9x;x{UB!-#ui?L(odkfRWrV_M~AE;d?#@<X7i=KW~&Y*iLe zsu}Ht8~o)M0YiYUFwI2vNeh3vnn@)kNUK_CyWpJIgzw!(hUbkQghfrXI@)4n8~kl2 z06OS+GreP+1%g_(1hJZW+eW~O2osA;BwYp#vO2+8X`izsRAUa-Ud0h#CoQ%ZO@oO} z&B$o*zzBsI7k*ifg^y1<k&3sWd^ckvg9Vo(+cx!JmUmjl<!w9qsehq&X;;ZlNokrV zB%}B0l~YxYq?O*s`iQwEEho!Uie~h6!`cJ5b`ENNS!lQo$c0c&S!~B%Kre}eZ{-lf zEUL4(_Q!mg&4SDEwCfp)_N8I9goBS^Z|{`901l^Y+Gv?#T=~AtUa1|=H3R+6?YvDL z`oT7(sha0`@12;ySe{0rz;IZ|lz<6V(>g=KuMOZUBlL}NPS&1gL4(zimTK8Q#wgo^ z{PXhEV1v;uL>?nXX`Wx2&s{Bd-)7gl)zQW+X!$iVd>r>r!+w&y7LBjf-OWd+G`0A= z?y6Y3R9lCAnY5T1^K=;<RMGYjn05%dnXu>ucP#ZeN9}^cRv&O{D;BKn|0&lfL9{_6 zOLX-aIZLxQQA;R7=GuBPh@|yF{A1Yd2bhpu_LbM>de>@9$Ljja&MxeEa>wpT53n>y zc^?@bq`#+VMZ5xZ$u~sMM2ym*P!>}XsyB4L<4EE<V#RmS1j*qAb<DZ6sU=4C`6YD{ zv-qP0Ns_EsW`+Tu&k{K<XbZ(}1@mng*sAAC2W}giH!qi&JJl64{-3^=tJzBzI;w`s ze`cuUEXSr?GALrmF-y3T;A+8Xw5oi%JLe*?fLF~st@5j@^D_yDgSFlW{fRgJz`Zm# zgQ`3mqGP;`wS2!4lrkhBOTt#>W}Ij*r+utjNfp}9k6eRLZbz_6s<_HL&aniWx8AVn z=zHPlT#VH#@QSdCP=e%jey2WHd`dq!s)@Nch`p~GZ9q(Ao$W$WreiEjABRR%bn;cW zJT0I=HF<K+OWrB>NCA%Wg@edYmBZNko!I5f1lWbbA><-!PJ5PEj$=%~`PahKJeSvc z0oJn!Ao?Pl73_Rg44jL_{J~F|Wpjah$OQ=wEr5eTXY@H?Sf6i%GJ%@VzL`v@9`dtL zk;Kt=#|g1aeKGM4JZF81Tw~P8s3~a_gnA+r6vH<^SzvduucA(IN)U;{pM-v)A0o(@ z8}P{h+grdjj@}$>e$=`Z1~#RhW#sG}?ECQIrr)f8r@97#S#jQHSPHScq&lm+CU#NK zQ(!!g7J2CK$RBRg8`srkC2#jH+jc0=q(YG?g!Dhl938*O){Tamk!#;cJa`t*CE1~r z062A(QSmc>RYVN_I@d+CY(iun*@Rdn^b7V5oW)3Ia56eMIi{QEak1I|uG?G`OT}x$ z3C+c)U6koZm5+tY@S{3KxJANu?B2S%yn>2$8ST!N=K6^fL9?T6*<_x&UL|!1lgzud zRT#|lzGb4D2-9Bjx5to_?8$HP+%WEXApiqu0npg-ZS{iNHgrObHoFkZf=cieaVqgM z$-(;6Mr{c?+iPF=1$%|^a$l-3X4S9>Z<Mr-h$agp=D}rEjZPD$t^AIgrj3dgTfIp$ zo<x+?Zn#WGK%qZ4mr&_WAT%09zJMIE=(1BG0_)k>M#2Hj-1t*a$WibD<L?O<z#0vw zX19lY*A`zn(N{+uK&Zl^>9lC@jM}=DAE&=LF*Kt94Xg^{US9=~_NnCAYYwvHec_IF zz5<(7c!hAx#6_vuLK|ecI@wu$grbAFMa&Aek;shc*W`+M{6<t1$rBJE2yPEP&!f|c zcGEN6hzDsLUO+nuiHM0(G01#XYbv-Qi6`+X;<J<=sxG;{P$HA@S%ZGGbp0ZwL^5{z zoIzzAsGm~l{Iz6i21EnYwzn~HbaE!bFAjsLEvO}Bs_M6Wu{jp;{DUU#+fg~IiC>wU zLGNX5QSWtHIhMi`(c8lIX0h!G1ObF1W)m~1xSmUo*-zA`{M1bh&Cz!A+9@n7PGfKN z2QUvxa4B5b)D&dg6HAYFC6r>#6NNVHphR{0QY*z|lmP)fPi~6da^4Q!I@|_JvZ|17 z&HC0595eY-G~uwg0WzW*)CN$u6(@B(^bRW^CM`DVI_V=sPD3RR*y+^AUI(KUER7Hi zT4goVuk2@8-7ygwa9{!EtWNLT^G=ou7Ahtp4<0S3{H?84K<6(w`8e$_7o{ohxC<Tc zEDHi+?1vB~K0U_3B@v3g@&?X}gftbpV1%~7_;BDxvIlW$xd!wkhL_deLKVtM+@8&0 zGlJn6w4fPP>of1rHJGKSMtPQv(iJcu{@x$aNfa9V=F-W>o7!@n*l56@-JtKYLGrRi z%ng^?u7|vfrq<fYEIbDj-$|qo_CS>Cljha^s{+XooGQbEENr6`cWsi?u{{a{4MI7# z_Y{bcm%kZhGqefCAVE&oyFC`}rJFCIr>guAz}zJ)I-Y3ZtPGJJBK=<9<-Ggjd}<cP z&{EfE<4y3zdJ|qXP1l0Dj(&Q*#>RXK$_(^$2hTOlrBQ|^2bAW91Cyo5!G-_8TK?o% z6;|$A*eB$}w_671J|irAmFU~<$c+vEcflVqKg!5;;;ue&4Xo@}W$riM`BSd6NLY?< zUu1`yKv0jFIpgi|);MAkHAQf|VwOdg%dS~si>Ceg?!bi#c^mbIh{O_Iqw_NnNa|A) zUYUjwS$yLam8b&4W(HFd&##Q~E}j(bjhVa~f+x-oEAFDdaae7$njHI2ICeF*U>Nv6 zYi&6P>j?`jh)Pn2S&COhK<DGzjWD3ozJ!)3SlNF)2^lvBU1H!$8O_bW4@Af)DbFi| zUX~Ph>ZvhqunKpsXe*gZQZR2%`}P33@#Uztum&W}$qTkWe?r(8MgW1iGyAgz?&2Gw zJ$-p~z&X(^Mr^Rl(O7P|1`qbum-g*RXbYC6X_Eb8C7Jd1>$>FZE{o=U?~YdylioC_ z=~t_)nx~)dHdtCoq#OvC907<muXUVId+d<&3JW9Qow0o#g=-Ykn$HT10v$Ta8j7(Q z7l>N+hi*Iks2|ciliJ0cie8nxx?GgWf+Gp}mP|5X{=cpYd25={A-;c=|FTnQFTVww zCYCWvIw~|nY1cqVd~{DJUAO^`JrH^bRzyA?R!Aw%^VSrXb+N(jfvVae^g@<w>BO`F z>rsqHHs4a`1dIAG$!4W{*UXDYqvU(=z4Z|-*`(dKhTOo-pKQHxd3O8IDU%kCV`6FW zI-VYO=BJ<43@QXtk9vc<=v;qbuet;^%CUPTj*yX?75&xqw$3Z+U8uB(!CCnCq|%qA zeJU59m`$D5`3+o1Bcn3L*#t>lX*5Ose0PIh6*XE%v)1VKA^J=DtYEm>3DT$|5O&{~ zTpPVFu~%}Edqwo=nxx4C(|YEyUwG{itf4~4)Ptu5G6@6-=R}SLf#793(uf8Nb~sJ5 z#t42ES<SXLDLZ1(fzxT}XzRA`AD9K_cEBT_Kq?yNX~*2%EDaRuu_S~vq(LeMMWp`l zR9p&d-a?=6QGvp4&2sa%L2|eAKiYGF8n(lA$i#*3RK;q-gE9-;MsJvpJlM(<k*X{M zJu`gB#c9UP(v=EW(?bZ%-W7^~$RM0JvMTP?PgQJ{O^iZEU*2ySw9H~o7+ngXrL6Pw z5p0imn2!Cj9WLtg&1_a+W1nKh$m-XwPo0ieogA{>Yew37R@-W`%b5}%UOS9zPlZcS zp^)HQfIMWEyt#w~l%oBS)oRfm?0vJTxhS68cz|#e1ZSv>zaM&JI6gd6cGv*VY%W|+ z09<je4_rJvaImk-0gDG_r5K9jLdjH0*4yO6<dpY^@%`%PEB9*!=Cn3Z)TS4nRnejY z`-(?`*>ybgbg${aj4^igz-Kx0P@EMK<dtytP==uSI&+Ylh1mB{^_rFh;-3SnUS5SE zcja82(=9?Y^DvotV_iA{zIuEG0p}`AO162AJ1yhbJU2+?=IvYXA|aV%o+uO|pLj~m z1Rwp#)q7y>$TSCapxX|hgq*z{b9Gc`rHkqx<@_O)!tq6X1ZNozGfa0Wzq;Lux=sr+ zV@|Yh@KM%(!%WOdPjdToKAlWG&(|JNTK9Ti(Y>GWtIHFU?fC}f+MHXO{n}=>sFB(y zy>XvfOlCc1vtI3x40d`LBWR`kzMk7DwrqsgB8g>0jWV2=bbn9IJ6(b07yIt9WpixM zkC0{QJ2Lj6AtjOdvPK(j;|5}dbbs|4rg2Kyq_)K8{O?lS<OMQK`9U-2R;M}XKi<?@ z5>Vsx^+QBaxP*O|+R$!xFLIxEf4R9|5#x&_S(UyRzX}`5y5OEhIWbW0hu5l-r3cAV zB}bSH;9sS)5>16c?Rab>fss`V<U8S56-arKFg>wi?`Agi(@Vh>T=<cFp8UmUZ9CM4 zGnk7OLmN3*Px`L7cYI0O->=Uf75MTXdHT{?UY<R|5UEI(rjgJ{@IdxL<&y0DE;<a} zo#_E<=%K5Q;m;EHohWlc#GHg=6s|GHt~tP~ps=_|tWvVZjA~_m<;4?wb9&S1=vy|* zfoFslSZwuE%+vJC^hpFjbJI?jB%V$BROsk+6h|-DTz1;PE2axNx7-u7wX)UFE3pTc zgiHES`%T-fP@f(d0=q_2=}CMq>1%r4Uw<FbmzZLEJRr^Dx`+sso1tG^EGIfnh-6X0 z`Grcavpq}z6@X$Opiq@*)H{Fm+QRTQI-9N2m)fYpQs@#@l8-Gi*I1}^=P_-3Im=+j zIXPWfTW=_8u{mizdvo!A`j|y_MSNj{7gTuD?Ln;E$J7OO)IFOA?y5zhM5jTvjs6tz z$wEjK#%kth6p>b*6q#B^CVTj!yskTGO!;$&b&VX@mfjb9ECiVsct#IZfwdDo*cV+v zVfCwRQV1TmR9+^t8`>axeUDUppY9Npff~SjUV}o|)(E>ZX^+h<Fe9UW`escPf?*es zSzSo4LJW(Sd>cBRl`ELXz;LD)E=i+1>oTUFYp)~vUQDoNurex0F4sWJmEOsb90EgO zkebY3xgy+GJBd%Ro7$Ylh{X}d=t6XgKtM&L5F9XHVWLU~oI=;GkQ^}W!Ui!sfgYwv z9}ZQxjs{&gEFW!^mIj{L<GRR4B@dh*l?n>1D}X`o3gaKpNi4{G>ihy?Pbo+inrp}u z`R!{e5vp=tKk0ZY0n!EW)JB9CqDk{kI~9BgQ>vBT1Sy%sjyhSVd2GgzQvA7t0Bta< z-*tz_{$}Q#$#*|yF$>8~4%3Xpuhg5)cZu@Dd*YZd19Kc=SZDTkxP02r_YCtAU>|YA zMZ#n*RAo`0)>-Q{v46dEz!R;>4SAVBw#mr^+#*ALv1JUFFN@ooibi(9@_@h@Uc>!@ z;vxyJ-^wkEA_L+UJ+=-o!bPoo%RB?;bjRL@ba~0bI%~(jz{P&LKG7Jqr%Ay<yRjvG z$XxZt1+u+6PyhEI)Nka)M&JA|gQ4Hl&wqnZ{IdT5p(y!nEDirbL(#GP17!MR(nS9o zK>3ewD3;$AME`%|P)v-B|Aj-b03aMXS~|wRF*g4VC;e|Y)NiKepC8KKaj5^thw?8> z6oCFx_=lO3;{V7*{r<%Nm5E~f2dDH8=7@<AzzG2+T0B-}7627RhsVhJ7oWt2$3jQ< zKTiKx|6iY3SeWrxnCbBVtPdV5!*9k3KqxT*S_ZVk$^c-K82_T5m;v)_05*yhz>u*5 zma_m@CBP#qU`@K;YyBQ0%O5W5uSaG8zx1#DFf+3Pcq%r4{q$eY%)ePK0RHv+H5NL0 zJcd78$;?9cXO51RiS^IR%&Y)V3h<WS6C)d7IBX0691JjIz$(9o$;JX$#tfL40M6EI zEPwkd|Icu#|G|um&))2}GuS^($o>oNWBYH%v46+*{VUG*r-lD_n2!zs&i#*$V^qw{ zzhORlCIFp93veEzrDguxCja}r_iv_S0N(81?z{iBf&biFe{arzu(STdA<WFs(G&pi z(X+7rZ9xA=-vMCP`<EdKxjKp{IO;hX{S|^l6zBkSA>a-2zbQ8w0UJvjdj(rPga2AA z;J}Oruw(nj;y;+;A4@AmMJBvIBp=|XrKre=_lNoWGY>%Xe*d2T%(DP6y??H(Dx+_1 zWZ?K$zhYK^e*gMu{_a=S-o`+|$PrJ2MpjT5k4DkR6@WvES?QS=326Ndzx$7E+UXbp z^c|q-f8utHDiSuU!tmcuR3_}uNLkiMM)OIxiiP8F!h2?`j{I^-BS57v2(d{%zB9b! zlCo993F)b-Q@h}B+)T0*xO98P+wxp^FJEk&JwOtk#&DDgDe*`9=n+LBwfXg-J|<Z# z@tkgS<zO>1{Oa3lmw$}<pk?W&Mste}d5F>xQ+(_+XjvZac*Ew5_WEdC=%)wX{E$MG zYD|U7pa>(s@BY%3+Y7bWkDo359yXV}@A+scO%eZ?&}(nr0l`z^%pLXSXCh3>eA9j3 zZBwbtPf?Pe-N1FB5y!uUCPXa`20Fx74)2R~zAJXirGl^SjWTUcAjz2#XHhOLje(Zg z=|}Lir#i_%MrhIgUBgtQ-iEV(gn@O|APsf`UMB`iDH*!>-Y#<o$95Rj$usoxB_jxE zh;#l!3ugm=1EuM3;!IyXM>ZKWoZ~F+Av62#hJx(E@Nl(GRv35l5i>zcY2`&g+*O)0 z#ZMubb9^Fv;u|MIGb%$Otr3}KF-LG6hP;ea_@B~RUP)?W(R{UI=D=mhADx$G?+A$C zR`6quD8gunw9ew{E)WqpvemC_;K;M#w9>*e^%jCqRRE!FwaQB|G}`6WJTgy4KAJ++ zwCYSCXikK|dWh0A(HF;H5=5-SU0#R*WJy^O{KHju=@UC}V%iJ?#(*M(2hS~eSnvVv zdvdg(@w1^1{k!hScaChiN2|y1$8va?uWba&BH2lYhuB>#&~?h8-?;cN#q0C5Kp5Ra zRo!x>#@kz=oO&!gHAQobwPxOfvJKtYy9=ZgqnlD8-<`YX5ow}~Bp3TY)Sz>(LkUEs z&dx<aT(w4q*!bvGG50lZ)%<0_gX!_nKi{uyy-ELyk?<oDRT3|wDy)a;7SfFMZ&z!l zS)XfSSU_6O5HCMJpi^{PQp>*w=A2x&g<}%((mpQANSV@5l>b#k%@USiv6Co+WpRv? z{yqEPe2Iya9hMe{%d_I7P^V*b#TaWSq}ng4gD}mEkkFv9+f-ZY2kgs)r&FizSLN6y zdyQ>tC*_dVSG${cRDny-0t?O&Me*`PL^4#(AYXdlNilh8D<yor?qr1{aXpwZyegac zXD;u@2_1M4NVPkjD_|m$>j17B0o}!KR{?jgwe<prNJghfLauCJii~##>zdi--N8ch z%^^L*px1RJkqmku?cmJArir&&?l%}#iK6X7O;V-Z%DTKAS)lnoo1U7bf)bI`t%z#9 zd}Km$-?Jvx<SLwk0A;(?&xA5W8GuSQ996&SB?;a0UXo&gZZw-v;EzLjP!QnV&Jw@C z8rmbb2~YTZiO}lJSFSze<xwO-pbm%kOH2V0Ao(7Y9g|WRY4F4yEjY^VY>CO<B?f7y z3Kfx73br>ymhZ<*a(c&V1<hy3{d_nk-ugMhboX35g?P@0W3q7HZO}@Z$_j!(;B&U# z6(9k|Wp+8@BC!uSP{<TYZw&>a;q1}#w#=X==k0^^%=Gl&QxF6iMA5IUo1!hqpjxR} z`n)^e=k;*fdeONMqC){1IN^l6AYPnQ6PRIkl>QwQQ-XK9HLBL7n3o!qAjk_dVN5?n z5Fyj3=8;loa#WO5@CM4=``dI~xSNpFJ@XAPCJIrsf@@yK<<5KA1C4n#Iy0JjG;nhp zQb}HZ)+(d6!*S;}<<N;J5JcUOteE7Z(}f3wcHcK9R;(b1I0FzG{DBX{RzXAGTiZG@ zsfP7J%{o|o-yY+u*_er7shOWrl!&gp7?t=DpmHwraM>T(Nb_^22zN-oMA`Y?fZp1F zWoss+Q(JcjLq*NjtK00$cj#4MLKo}x&)AE7>E_>|P}t^;@eBeEa>zB>s_?<pMI0vU zc()!$oQbR8_6%*+m^M3(I_z0ZFCqeXEorA+a#|x}E@i2Q4#bFcSs-Y7U&A9%u>LSr z@a%5*!b~`#d`^_!rwQ``P78GHpuG6o74xq^j`vT4Oh9(^FNd}Noy2Bj_}ihNQbo#U zml?k6MCFnc1-_p~Zmw>!eH|65&<W&r<_te9X+UdmLmgu7?ZK-bZnWN#gx8}^(u_6J z?ZS@LP|{XUa4>!GSIq5!R5BWwhj*dK_<6(HCHcB>n`8$p-1x~Y%4VzNV%k8@GLK=K z|5p!UhpderteDm6Gd1IRn_Fdf5}GM4ZDA5EG_>WLma%K|X&vz5WXm`Yoo?z3%EeB} zl*W3o<wdJSw#|8XW{(<o=iRLNYozIub;Ba9)sBN^jk{ClmbaZnOBTwPh9@Ef=<1|W zDhqBxwjLz;`hDs=AM&2omG9O|XDnMw_3>P%%``eM6L?(eFGV&gmR))*BiOtNGHTc2 zYLf$p3L-fx9hPdF8>i<uen<BkY{3;lBKXsXXt|Wj79OiLIU45%RBl?mWxD4wWUfYP za>|+-da5^pnx+WqKygeyL2HIWdXFq<QU&PO9WsRea#Lv^C6Rk$wfVK3<)mXJzjo?d zPOC%mD`%J@*`y9Hn7K*h9u>V;n$Gs=C_3wrYrT#bpfEJ<ff6a}N@8VYp7R({rSvU8 zq6QfkgTGdqZLcRX&8;+zdR+?$<YC|*&o)Z+HeK35-q+?Z^%P{1-POw*oU7hn2-Y)6 zBr0{{;igOUi?*TehptsshsS3;UXi&;N@_V2`t+?W1gAJRe!Ousf6Yx0t-t^xJoI;= zVkInr{`?7lcG2s4wXF>Qlvag;8(>w>!B>-<j1wfBNZ!L-z+?qpLbj$(7V==24=?O~ z%|ng0Shzy|=6XjqZZY%j$BaJK7o$C?lGbB7T8}8NO3h7Gim`O-QI5{s<*W{OQT=w# z2ELAdl-P#bJHo6Q<b&@Q3U1~b8%I4--uDhAvzwQ$iY~Xcx}Yom#D<$4X<8=|;$Kek zMS9)ii(OO#&Xnb6vcQDTBXq`VpbU9%`U3tkcHpYp`4WxQbxQjNHqr)2cg$ZU!U(?X zmJavZHTrByUskAuTZp#o557G3AvRvY*FkxF2P=*(gH#fh!p}GZQmK$2FQ^{Svtdu- zv0=y*XJJ*-|HOfvB(h6e-meOTuovC#YR}S+K`|WvzPG)DITZ;U-jw$hcu)_WjH%oa zg9A$>9D0WI%oB_3hoW&zBnk3#U8<m56375|(=f4Xx1T)X(AURCAsBDC1asXvK>1v$ z!6Ov7pae153tpv)L8y#u0@v_C5B<sy-0-OKdlx%&*u-FZdsZA$n1lIkR(W$-l?Z}e z7Se1%IdPZBtRmLuJ7kT8TZhZ93X_m)a*vHztj&Wxc6?ffA=H8GF*Bj+j)4@ll=BRP zGPAB6;u6LH2nwkrCB%m_V`zgE-St<9fk6*<Hb=jU&pGsFiINYuk2?MSO^JKPG+cfZ z3Gzhv5`z%o$v4pw+qWOu0}KjEqBY7kp8Sy;_{x+BWls87Y8=Ev=vt}roxL}@uuLlo zoEJ>b=QWU2b9<BL$<G<=>(;8RP*n_!Hf0@tC`UYw9b}i(B+J#WT3~4Yodc`WM?~+1 zD&g(q8QxQ!uERsVQ(mWtd5L#u(XGzl1RJcez#e0-KO)`lsvTmSOxJ|k^H7-7?E^4H zx0}VZDUREN?UtB#2e(Qcu~keGPxxZSzUsxWqkr#6yW)6J@%;ppt%LXaZ{y_O(*OT8 zPSVo-sde$slzAXU!e)&bIq2aTMN=Mh;F>6Wjk!t>*)${nbG>N0?a=N@(5{(@KI7X1 zR|@jowd`bJL|??$sjdU<@K;As24|J8NP;D56h()e34QKlC=uP*4?nbwoJjgOH-fef zuHcielHRAq>%f%B1~ky-p)J(&uq^E5O@pw%;g-LMw@|Bi#+7Rjv9l0&KZSGUeMeqc z76V>3dq{*_@r}+VD-o<PL3)0Q)J@e&&K=dflG5>7e@G|$xv8WIV|s<w9m!9{I9B&A zkaQxx5L@R?0Qys~!CPgv1=Y+)$>!;9)5$<++$JWZcnIl>_rs$m3nb`co-+TH!Fn## zmnfv<3+w2MlOd>SI2n@rXdRt6?n@P9*w@=?<GY%-2S$~Qeez|SB<8-=E!w2_)uj#9 zbW!;78+<7cxZd=1_1bnbrj-MPq!(D3kK<oU=Yb;TMd3|z0iYaKiAk!r5WcOpWZad< z5{$u7**g@CUl=2vKTYO)^EQ;?&S06z;gQ2|W{Wlq+XbK4NqSE(R)-{>(M{P_4huXJ zcb4sf)O@(ODrC_3E?VG6Tq4q|GT|Z7F;mms^uO=EP0r(V+UADB=>g?`4p|f|+HJOu zoaqEA8hTCC5lGWUW|1Q^TF*-cNzqnU?G-gfYNTtpGKou9_Ws6nO;5s9^&E-ZP3RP9 zGWQz45YL^UK4&5?@mv(fALa5~83CJ@7L_eCRl0vGh00Nx{(V9eY;=#(S$u1`C6e*^ zP~+Wzz8j3z<VoZ+?K2U^b^i_7O6mUTWmsm@E?mp)@--J*`8PwbF6qPfK-(}Sg2E$h zbka|d+yv=EVu{Z-ro~xHx(rli05OVduaLy^gt1u8GUX8(^|8OLbK@-P^e^M)Hk^{v zQP#y>X6Nik2d=57jN9U3G&Zy28_?VQu+`#}HLKPyj-v<K7e}w>k91k@;keO`W0>Ac z>w=LmxZbWgQ(kxzI*aK#MvFvqA1(7fOMy|lJXXBRihwQ^C7z+J#d3P7j#P@n8L@Iq zq?m&Ci`|*sf~_Ps2j=vb*NlqUMX84oAq`q!U5$oz(K68M9*eBmTnon!cX1!4MVw{e z$yrN3RpwZah*DWsHVp>dKG}MP>R5*vNijoRju@9sBvJI3`Q^bvOgVOl+WIpj8PCdd z^&7Q+Y6$+ZGTW5@q{%+7Z8;Mrq@)li9mSxxcPpRChW;b2ORQ?uddV=*uA831%eZ3! zdzEOY_JY(UZ~8RlS>vqaUCYq240j_+{^RCK2*S3daMHtlx^mEm`YW?#eWbTY2ZYFx zPflI9wY-SivoHrHt(lZYGNfdN$Gtj6SL7SOTRX`F;^^N7kl&XF{&{)se-9vRtba?i z3IPEGkZ28GJR@_)gAQC0$#YsakooJ+=7#y=q~okrvSctfBXIDp*ld2~zGcS51ILO; z$7jp@h!IGSRky{jBw5$E<LGjG`_2_VJQ=38F-HYlkWbS0e&}6Y-9F}CO+{C}T}@P6 zw}#job@QEQ*y^t8xvqk`eF|Qz-6m=GQoTpV)z%-N!(~5@oGDyY^5^U;@#yifK_RMq zshI0@+K5xP-%}TApFZN6c#{{q79`)}OrdSFbe)ga+C)vqyw?1>X>Wlxc^G9inUTcY zu3=j7)7C2a?DS(5&QM27Gn>+lwU=B^>avdCLr5C##f(<m-CF${%6G`4r=|rb`Ub>o z3Y6z^J98z;Dv(LV5i8HRv7ssWVyR^*g8<*HV1^l31~w%j3v6H<7v#Q?(Ehu#AJ`Rq z$nIV@K|gA7p3{B=vn0L-USN0WeO7M?WV1dx`;kJSEHHFLqRevXG(1Dn)d7VJ<C*(D zcd6X(4l2&I19Ur8=6@A*%RCS{e(7A;-2=VLgKs!7KVTWSvmuRLENbYvI0Q}}o)+9g z<U)YyjFCq2y4Ne>9BWtZiyRMpv?4&fqGv!^JMG%zkL>qL9I68ZgpSpETC4WF{36L( z5ZvL17N%;)*R@TNCTphsKCum;V7E#axhlO3GJQQ2r0I2xSq4gcgoH>+U19N}{$qq= zG(luvFD_bv(fy;Pl*${r!9B`=^x%A;FAF7uSjqXHV4v0RA9e@3S10X?^Q<lT1lC8= zf`!L#Fff*`p<q%XqzZ}$oAvY7>&L|_JC+@%+pvSYrq0{i;ziDog)d>1lxoP4gDcC2 z(r>=#gj=WP*3_X}Z3h(*)sgs}?du&FPfXQDu4!w9>_7K&qMt#MqF3#c{;HpQiCMqF zR~E*PN^fh)t4)#<)f;l;vsSdLK;K0xvW!c|tGw+RqPKu1`NGv>_W9+(t-+)zQFFfL za*~S*JX0XD!d)bC>z(dy3QLsk#eziIQ4}FuZDrSuo<11ZsCJm&djlszuoq|aE7ndJ zBWtIw=PdA&*fdaEA546H22(Fmj?)T9!58+Oq0y3+PVCI-OSUxi=5VM0@#Y|LrZl0x z(O=;^5AR<X$$>Ezh69M!1G&$f!2%JVJoO>VXM@+~bv_rvbp3Rtn7hr6OOlB8KWdyi ziU$t(T;uRc1VRR)*QDS|RKTR!qes#+Ioczw<i&9oaJ>gUuL(qF%nGR8>XPqx&XkH2 zd@vE#*RT(wX9EWhd!NE54%!W8&o?1&Oz#gft_SNp*(O5gmSWa`aWM^Sb^Ku_p`~of z;gWUafpIPv$3drQ=6lFHw736AqMeJvbvjCoSDGg)&iZ%{)@khVJUy>+J7bH(FDnYO zF?4aX2gA+QgScVhj%ulHmhmkkikGz?1{b0U$9v!i4iV!A881ddCcYAqD40ecW$LKU zXK0`eWvH8f8<u`wmi&*x5*y3k)0$3|QR`J&!08`_D<M?q9&vc*Pb>IaWnNx5DY$AR zmQPq^O$f+uFR&_f!KIW!5k0YoESkoT8{_DCUIZM%9)wMWl6b;NdXWSY9)8g*li%gy zYx;i5RzY>P{6v174rY*!@&)JP2wI7@k5acTlc(-Grd<zsn6x5r-+j=CMD23aIk=p* znOU-3`q?AGU<S&5Hq=-epGaU0Vg$KIbU#@V^z|q<%NB0KA*CmCup`fn0~#n-Z}S$J z-th83=UP&TV~7yrJfo4e;PPsmVGR9bw)VzGK5tv)2GqtHZvHBol>AauT@-UOR#`3x z;iznA^$No%vEP8*do$XFqcP*$M-p!LF07I34Db2mvhIntvJi#5F7}5#wfOADg1wm* zq-L>Ud+G$rM)u6!m*_w^6~asi^J4x<^ydw5p(^A#+?o7suj)bS>au=utf_u<|D?b` zhuvgTorIsHRe8{6SPkACY-Wudz4h3;$}g7RAX>!~<s@Z8gx;TT4SZ{&fW@N)QH>|0 z$hS(<FtC^uf(ncg&xsVaco_UZe|$E@2|*kwo59CEw?y}cQzJMq0B@m{bn#@CoIMGL z)DRZ?6gM#+e*MxZEQ<(Yq?^GGkv6dwmmj+z*u%o!$-^0o+B|@S@OT({r0opb=z2o< zV|%y7jFaumFS5Eez`YY6wry$Pq6Rz?M%%Elcy0uC3d9ojz?Q^+wTV40tuecb>Kngf z?(u!w01OK~(qcm)p>-?t^x6|Sj$J{m#Z71{6Dwj1q{xmjXId8<i$`c|yYc-e$_a0_ zcD!mhof6^@_As>eE?>k7l{lnM$#ABYgXJ?J#*ql%qs%ZKUqIm`*UnMq3s*4cj+;-q z_m<12cQ+H<$~()}(<qen=l9<_x+vG*``$Y0TlF7eBk5noIxgZ@U9uF$Hru?m#==ec z?Z};Z0>-^{1`Di@U6G|>OVzmrNtrab2|vf2UYYd6b85Ukq4#;nrM^C~4%SdmSU!K7 zhr{d=R)v1fxnm}G#PQ=uSp_yDG6l=TE@aGp?~n*VtZqchb04SXGKiqo3Lsc5w<XjV zs-T8JupKz;I7;8w0IQ@u8r<?X<91IH>*KEzCk`V?ODyip6zd}&`(ggk(@=3GTCzDc zL-^g9Dv%)uTW`yz1<Ygyi*lK1ZASQXU195y$?>c0i>o`Qna3;Ya|px=SeRCd-lst7 zUHay)8(iCN<&Ov5g;uRK?J`5{BrD<Z&7k*>ExNf@D6oxH{eI01+Al<8lr9gRLxQ&= zfb*RsFC^%H8yEg|H}QW>SLm4jZ-F6M!UmHCkgmLtx&A_h?|;}`VyBM$)+A>#i?f3T ztx=~nBCkJKc60WE8~+kZIT|X59E@*o5{xzVYAiYwkG8>(l_(mw&e9SED;N0QKLN3~ zOc|ED+>b?1kcr*&@f=2lIanC__%KH}`BP=QxcY0;Og7o8==d`cH<Qn4t*m@lDOf_T zs0ZUyk;R5%mT|aKPCBoL1mN(jYbIE~RMKJd98}cfqwP%`lsE=B978u5uk%cTd0%&B zsck>q^_OS4Z9htJDD+)EA$H4yOhxDBHOju)ySZ?Y5RET`mhf$nJZ_7Wpix=yICsZI zG`Es2OBG~+|5SIK>9R6aFfjWdEmSDCtSK^VV5WiKLDnCCA**tURKTs`>QLV$l`j8v zQ)dyVbb6y(gi46SOepCvP|hDlLTHCLnZ7L-yd*g=G%BEPvA|~+p785|JL;MS7+W<M zy<A}gt;=NIPEG^*&}tg^@H+JY%W~S|t-n-%IBx!tzQibH(H63$A1c#)Cs23_Sz0<w ze(*vahPiug;4%btEV+i4atB0z`8X^%Y*TAd#==)!YC3!4mO#sILx5_G&#gtu%5DP9 z1-Uk<9)=VP!q!Xv6D$m#$Vt}>kEp%ayr_M#xw!od@Q@wwE_o^P5KOSk7<o^JQPRe@ z)7dDlb^*jB+jg61a)?AWM)%};+c|L;3pgN^yMAAGAT$r*5GGiG<EtBT;jX-%)b+@F zLPb)|GozA<y;JY^yL)>!lTGD_pK{Zd5OP3Jq&FdhSf21K6@WVW7-Zim<10QTz8#d& zE!pL+58IM5`5dC#8?F}GM^^lJJJ?w|wsY$$)+-G5d49<;5ur(hz8C#)SVneb9viYw zlnPU;&V*n>nUE7zqK#X;UZ$uaYShslH<cYpVw6L-{!|i(OY<oMiRJda%IqgL$H&ja z#NEOxo9T+@ZF&lh_ZcPGdpD^xHCFG~>1+K_xMWpy_f7V9(X7pz!GqOdux4|ZA(=fr z`$hE`cPXUFgIJL7)?z8zl%-~=(~LM@TVW2vZQa<~;Sn9FS1PZMC3@c;91jwuwl?rE z?LshvEX8c14*16k=8@*(D$LW{u&bOzk#6KQ3!JLLyMBIokNzoVt-hGPY+)6ZtxBNI zEL~+G1hZe1PrT28G6b6KG>R@1_?@nA1QK7Kioq^X-mvV&qmTAJo;@Qa&N=tXySd<K zGU$-wx#!Zz)hFr~u*cWyRTpQtYee55((QIUSzAEW3{|+oM?&aMA>T@YGHV>mLZ_Qh zzI}o*5Z@t!9h6`4shJG8$T*@_wmeY0`3#4J^7%ku-x+TYp&GHO1Gkx{*$P(I0$#}r zDstxZ?r@AUp!cd4r>M#RX`!$}vhpZI3~1B@cDYWt`~0W*9ebM!zh4WE1tL}li@X`x z7@xFkbrN>4g-jR@g~9S#VC<-Vxb%%a(Y43&<*>7Ph-3|4x7^Y@Wxk1T0`mxr+rMV+ zY1@0ho~ODe9q_b(7l@31%K}jGLji9TJwRpZFkAmNnEhQZVrJ=RWRFK9Yzer3D`;e3 zV+g1L`&WTcJT^ep+<(2@S(yAM$qBx2hr}uSY%CgnLg(ahROS2oybF3<O`Rqz3-55% zrB%F<icN}|UW9>g_{IkVF|VQ-B{c88IsdR)-#=BDR8Z~(RJ*YTMcC=3<9S6{<tBiC z95`Q4-dGD<C}gj>KAaKKt~~5h4mh0n*Oi*`)jkSR68S`LJs1Q}+I6?7%AQKX@DLbu zk_iwJm2_I}OLL0|-`02Cb`4kf%eE>;KeqFW$4>?D*1Brqm<!gPYW4`(uhIC}#52Tc z7|Cd+T1@hfXVGWyu+6oB6$+R;OsSd<`7)EpZ@Wk<#@L!^nxn{8ln>&_ZRkRgtEnTG zJl!%<kuT^+@joE<RrB4b;c5GT35?2Gn*zh@)$P>8U4I22mbMC?)u*@R8Ag}qJi4hs zMWVuaD1MlCx2;4U?2X%{|FomeL`SO>GboP#^oE}X^pMk0(Ooh4J!KKX*u$+L?H95n z+BGo;1R+pmUgv1EJ2T`x4-=G|Ooi;P+$|(>BQ6H~YRq4Wv3-i`7`egSLIT-L$m|wN z@L{tQueP0~WZu;#$>5#-Xpk~u8i_mh;X@AhbY@wqz-9$$MUhcEOH=YI#fi)-auU56 zd5xo;cvf)%c_~FwCqpzz(u-2yxk6ohtHDNANfRr5IOtUtpz_q_!jv078BB*-ft|i) z!?%zQ?GYY@408Dp4<6@mIP3G0da^3oug|B&XKhQ4zSj>Q5D>kL6Wa~x#5y1Ql)oZ? zSQpPwaw@#x*$~9rD6GRJa7FDyDk^wAf(O~jfx34z<sBR2EwMp+`O|5uw+sjN;4c-i zm053U^L~xlx_hr(X%FtZ0>&%3LZoWe;sTbXRX=Br=}0*|H&=zmjlB``1a6&zhK4<S z1|slO08<_BfKBlYs=KP*y+@%21>O`#!o(^O&BquUuAAtT(FxSA1Din^2m;ip_Zp24 zT|Npsc?)axt|usdAlVUqi)*F(P!Q&2rD7RPoYPA9$<}Sa5m>`uGR!>Su=vLLDJbq+ zNf`9YonShYU@+Ab0-JLH#5{7{uI0Vz&=jkVV@60y6oE`Hj4@_(CoGl4<16yU53I*E z&M&B+W~b=DLGZWeVGgte@45`@U1Gn6bI-yGbRlJmsGOck+NL8;dD{n$C)!3o6J#2^ zFTv`eam~SWSu8<M4;m8-Tr#@yyRvSqK^nXQ%A#+XDkVtC-MdtSZ@zgS%o@xffqI7- z#N_!AwBJM_Y>EFeJIsjV<jK4cN!x{41CCAwK1-AhWAlwBK0ef_yFs}qyLp5zgBrv% z3tMKJLwp(SPY!;EpartLLXrKqvEn~3O-xJwcWp#w@@n)dGkoyH9dc*l=hSVrUsf!j z>G@O`yu7|x)XdEZ#agQ;OsGPe-#=m(M(I#E?qSM&f<w||81zpaj10%4^ykoVIFZ&| z@d=?il}^nx_~5+dc$DP<?w(0|b9G8IUxvFz=c=D8<g54Ubt^J|^}-C<=+t@<)nQv5 z1;U1B{e1BjvT{91vjkP~wohb1<J{S<?}BL@H2qLe={jXUtbEaIx^jOU=CgNL(<S-| zs8k?!3XY`iO4JJ9S8tsaOkR7wdF<uo;_7Ls{C$I)sUo=|ef@%k(d4B6{OYpP#&%D3 zH*Tw$n~uKzF622o8{nwyA&rWQ@ze9vy9g=Wk-zu5;+TCAQ}t-O3W`+)OcoHC<4_T> zD1ItRrJ0`kysz@pSCuXfwr1{aNM+FhzF!e|#qDsN2;mwGhHS!QCuG5elHZn09N|~t zi`$!t`xw}BdXu&(j=z47_s)dR^OMRZTNA1a*>*j+F@2+@bzvs5h;cL2WgZP4w=JF& zW_@$nlH^!}F0t1{*^aa<_mf&DX7pa&u<0@2w?bNWaN4(b2v77MU=-cSiQ51EzMy-A zOCs+l`U9z~nwTDNN=l#4L_`nTPBJ$ghBc5a%0puBI3@VR7AM4oq&62LARk1w&|fG8 zwIg%_5k2df#vZ0yhAFP!+?~XJ6k17i+{c?mNA}ijlk;<0=A(%J5a<U;=PlmuqJ0*S zVYeq)pK%o2L9H7#*iezL8^4scggWHaXLB<3pdFS_z{IXY?JdMrpghN5E{_D1A}6te z+!XPX%e?D}-n?%hsalw7bu&D@77LBpXhD6wfH+8I^rL`p<fJBQfbZBz!00?jIKS7L z&iVvQk>PkGYPWua1+^LTcpUxcxeoswme%pkVWD2g)BAI3tYlWEOxpHnFcqX>W9z3i zRHuSTTv%olv8c*b%yPrAW&U01ZJJt+P*7))gYi?;y9sMCT#S~PwX{43FwLQ;sAtRc zs9|X@<`6g8HWiuSfc0;SW(^fD&MRBW<pG2A5qI-ZcM8O`8(yG>TTbgLGw?jbvxB{r zsmU^n4;QSSBF#9+HH1Ll>pUa~?v}caG7t^N9_`$SQppbY^233SU-`mXMA~ea#)MAx zWn&BvuL2x)WZW+<?7Wy<rZr<c66oznxD1!e%p!91bsR~$Tpo5b9&VKr*J+L#P<$7$ z$OFL_+>vP$>iCsG_he<CWh2|W$<#z9G(bR-=05Z1no@m=RNCArf}b$ht*q>fYa}<j z9rA@jxiq-td`wdtBcrmb%=FC=;<x0)wZhvDc19X4rkQgD$xG5d(Ym|7lde|*>ol+- zA6ohVTv@5q2d@3Mf#hE=tO(fK*z()B;%WT8-OdCk;m*qVyNtT5p1l#EWc}}ZO8})8 zpS87(BS5eANBD*JN7jY+N2iMSNB)KPN7jb-Te~F>P}%){0yH6IWN4=MZ<>}ja<Fl- zH!yO*`=eX~G%o<q$pU)&Er|Q8jX(GP<?$H)bdUF^@;u<bKRPvl;PFp441bN{PxboW zz5Onjj|UjGiP7&g37{wVbLoiTZ+f!-&$My>RuA_-=-mFMZ)5zAMGeeM49tH^_&QUh z5>{Igf-jy?IQ_|>h<V`&W1`On#MqiJHIzIq&-v13T^r+y!i+PU9u`Y5qiDo|gewWj z7ghZ?zI<}#@x=6eI*7@Cer<i+HNUcNiccz4c7d9`6jp|LEwGV?s&@L(FH(@vtnQ`O z>G@V#kuhApec2D*i$%h@af&99I;T4`$!|Lwqt*0mbrts3>gKt!VQIq>)3ooer1Um^ zToch!5rpFgwRrBnF)HqqZqb^GkuIz>Jh?M<E;&2p&JAm9urINB`Kh=ZVRy1Q{g~mk zcMhg{tla(XB!AC<VAY(g^%P94YGY|r@63lvu&D~w9?P|6i=WHJsOhwI2A;YN-4@E; z(v5}nZhXf(4^MlPRC|n6o39gko98WW<;4-AwWXH?W`3`Fqp!R?K57Set)k=S`p(5* zTcb@nh45ff^Z{zbN^_N%C)A6j<^5FnsB&tV*-wq2XGa`G3E8YoFV~kk?PgEi3vX8) znmoo+sV>D%gC9AU57}?BEVR$EKe!f8`&S=qPWFH{wSUBlPICW5H}H8e#e>)zzJ?r; zg?d=lB`Tm7U;|FnA`rG}j&z|jcPLRr2!#4L8J;aR`&J=*EiDCHd`HYKbd8}1(IBV@ zR#_J+l!<e6{=5uC`m6EFXUHW9S7pPk_e^t%<i3!H2wbTC2kTkqu}XLLVpsHj2Pbai z;QK6-3&+zwnt^Q8#i%^*U+a4_5`!X2PB8R5*V@FfJlm*<D~Dz)Q@$dJLNT!Vp3sz| zvTu&1C>1|f3tFmPhe!#+qOvSYjvjr#&TPpPodH2^%)CE6?LACByULqYuAxI(g;ZBV zTC-L+V0MU;K}?lS4_DVyDVB&uk|c4<3>%_#@w&u7Wv1%ZNCX9G6GR*ZNZh9&D?ID- zb7|6P+!kzZ3Di5S*OJhe!@TCIl?}~na(0XC5`9D<8qDS+uIAB{F~!c;(U3J0dT~1w zDOF`AE=A>#T^XXKBra(^)FV<{uA1PgDyY#te}hsnG$jF8Eq}>gU3b@^3EHCS2$JEh z=MO3=bM=lm?yj~i){r|F;D99ghRJJIqP^Q-tsyldNpf|)a5H^ZrqT+Fc7!xDb)cax z<>4<}1?H3Pe=u%|%gjScvZFV4#`={i1V`*@`z!oWzI$N09_B1~OSdqFP=gSw72nR_ z%=Wh+dIehDQgNo_Ui0>BQe>K65%)pvvt2Xe$KDG4R<$-Kn2*S(-?i_zcskkxfTurA z37@*+?QzJHFTsdo5OQ)Yn|g#{cv^Im=|*xmKeZm-Ga6b^@)*q#yoS9^Z2XXb!Su1> z@u{vD(385i4K`zMXqv@Z+G4NjgX(QqUolbYNo)KJh2!T${Hn#}kV!x+f4~G;+j4;e z4+eaN8kVeEvMNG|tD6%Czm<)E%L^y=4tY;zw?Mv8P_nw6TT95%GM#`Ada)K@#~iF4 zAaz>$p5PZj$;026=@kf+miG#>Oe7z=7p=z#6HC_r6B@y)#@XU@2D_u|s<(A>8*^|3 zb{N_L+p?H#P>Sq<x;;=u))Z<)b1?=D$wWaFFS_0&6sj!)d2SZoa9BgyrPXb{65}Yj zdF^#1p03yd=Oi2ja#P4V6Z$Sz&N8h~dmHt$*!9<V%U8{K_mOC7T-av_u@=#I!(ohp zIID-RfeWR3Yyws+%|9EUxz%@@sK1O5Qg6;#!D8{)_sPsydhI3VWS&1>weN~ZKaEbt zJo%+E_s7J2^Onzx#>Ldw7oqUv>&(Q(<VnGeTBONd+i89{K5Bi(o)l@I>ZH7#VA{4& zixt!A^&^Ec^Y>3CCN1Ic@65(0W${@##}f~<29s!^Z){1(3xk<FGYstwu&_z+gP{Z# zw$R!*HVs<#b;8&n8LN^O3hXN055Ay&EVl0)bV-SWqXb*|7%Y|i43#Q(J{Lu8dL*1O zk8P|y&Dg&7x>VYZDv{|-KKHFc{ENqkrDGbAD-5!Iu-fut8)aEE-16eYjlsNykL>4U zcHJ^<*iNGiOwM`==OvpSFW&FOYKhaI)f3Yj13;<HblKMP_U<AB_V$NO%TTp}8BF6) z^OB&QP1_FX)vcf|j2LF>uN-F0x0M(olG%it6zXAzJ0{;ZHC%DAm)<{x#h<=kx~@Bm zl2OhGE{o11H!@1Gt<V0tgO+N0n)e2m6A${onmfy=xVCLU<L*Hc+#zTIRfQ9r;0{59 z6z=X8AOr}m!2$#cngE4EfZ)NM!Yx41;0|3m=l1QJoY&*MAN`|ikNUCaSTgq-YtI@r z<y)^=S7Y6`gIT1o@Hl_E<E*y28#(hjCs@o{HxBsGbMjDtVJ_O|Jano7<;Yt?=`(HX zonE#H&aV%TaDpP<uUP56j?>T&j;nrmkSL+()t_~+a^hR0xVhP@gT`al_u`37LJCtT z!-10fJ1#`b3BK~do{>Vuc53Z}%GqyAEqZi@*QG}B$J*i%j-vYp5{frEDqh>(qU#2Y zSDMR2M2`*v%4~exo!`f@0vjfOJpNG2SK8k$liJ8zX5^?3{c2xME+C&$MHDbNb#B*6 z-1Es+ZIz&|tx{C|0PF2LXyIbNqK(_PK0E-6RrymsVNd2+U^3IbQURPVjRW`7Z_`U$ z0$$oHGJ7+NsF_tptQ{#x@3ky0Q&YDcP#+w9$B)~aVTy1~WkKhQhcdG;20Z1|w=7Th zEI1u}ljay!Vd-6^zrsXi?{hR}VT^{^_EH5We8j?1&3gpZGNT=b-fiJkXL(?&3U*B- zR=l{!Bnpw+gpE>5G8z+uuvS)92os;*cq`yz$`?P6)<*1^&ngkbdpA?C$6t+lGyJ_= zEK;qjqUXsf02D>WE<R6{-p@7<bxx31$kk+$FYT^497gprWH!7)Jj-dSSt7CN-%OdJ zGQNXM)tskf)7%ht0fpA(3S<J4abjnA$1Gk08I^_rj1<qv=qtP#qK!Drwz=aXn!=Yn z3_1y|4c4*eYglrfh0v>;9H9Z_M8O1gyqDXc!__ow`TSE7!zkW_x{}<&0JdV`#FiYF z(uLb}M$XH4hDhj}0?22Eewj4+9mW=sA=>_)5vCHmD>X*>(nra}$#;3F;vETSsxSA` zc5})csIfPtpUN|kK!F;9i*(=5a5UgM@n8nC(qlUi>%bag{{Zs5BX2%Cmd;<R^1Ct` zSG5u)r<c#e2PVr$#rl;@BDYs)voc++=$&Ra^Jjr>Lto@D*>hzY6)swH3vY~7ysUjh zCoWu1%<Er0PT^r^kTFidiiy`45z@x)&v3)2KXPld$&2_w$zSvmWR5btT-WOh>uj{q z#NPH^u8fS6HwdBrCO((`DivImb+CQp+mr5kQrv^0K2~Cgl?6|Dg;<+~;%4LUcst7i zpUk%N8bYVP7fG`ZzE=F2y=_QQ&>0`Qa@}x;G=#PCs-`ix6ZwW}WIM{i?hT23oEiEC z=PD`8qdC9dHbhjxdam(Rs%K@^!R(1|Pa|CAtwdZME(fCmnWYq*^9|TGuL;sIlT|}n z%zNy|YP?{X_Ke4$r&i~$R|Ao0BVQZUo~ch`p?fHZ&$YnS{gicpaO~UD0v{(>y7Puo zyg&wlDU?c_s|iPqVa3QEO&lD^C9W3#1t%VWkp4;I#oHYw%89XL;Qal%U2E4xJIf{E z!VlrVQ+qk%+Qp<76}WExCiPReKsC>gh#2Rn+NfKBpBodX_EPq;3{C~RsqOl5zf>=( z$1pcUVOAy|04pUF*E1%lDvjai4`e>uuT>89^BU5Lah99&nn}vT#oIrFyj=~2QhD<J zOpwXgcO|<pXznu1n8G+?o8XwA#Y>nXl<%ooILG7Jn>}E^=YYB364*@yRxKa=nAvrG z4jZ~K%AAEP$3P*=Lp!ORxy3h*o`I0e#|#lr2Yt?2OCbTtQRy@Z3R=@^*Ln9NHwg;H zv{L&_6mtL2Z-R6B`4tj#)CVpcL(yThBT3siqvNRaPx!Tao{dHKiSw-0m4<*lL#<y~ zOutfsGCiUyk0du7l3vf}^lM?OuX};&NOu?$pi7wE=!R=I`#$~CcSa>yra`$zimb}a z2ZEQLtqv55kMG%=4D+k-E<zQLtLrp3@?~Cik)aEGQMU)_lszxmSOH$0ZDnUKGCg&% zCKR<Mq}{Z&Y}^>^^s;>OrsH1vi#u|4eZQd;@t||iHSh8LPvgbFYSn|dm$B~m4FS&B z@q!ywJ$CfgwR)+M&kO1XdC|L@ZYt#XR6gv41o>Miysc>nDNa7ZZQ6*fPbQ4bEZ)EW zj7XO<SCKO#gQ`VMc7q%^wliT{@AHD#n5r#ML=2rp3A7EKJP0w0vqo&!k;TGgvS>R7 z%D;YQ<+QXaOpZLmaR)&AamM*gzJd%WFvZHTk{#4rrCx#K92zK~`dk(>UzNwiPTzh| zM}uRgE}6b#8Bw~6^9FrLL0wcg-`#IF6}*AGu<4iJt?YmsN9DOw$Gqk%jLz$s;cbW8 zC>$;PMv6@f+mu{(UVnJfgyH&Xmg<Yo!!@#v+J~i<yLTD<#g2{qhPTpfGdm6vSeH4; zgJl<=f&!JtzfZ+Y^7)>@na!8;5rnXDN7#$44~`!4If;c)3fhCc+g_lb2+{+arxW`u zA_3Rt^%#wP%bsaCoo5=*;zx<uwHEP%6AA@SFVK3yRA)%DsbV2GY>8{vCN(MTb)zBU ziudZYNjKq!45p;M3t>52i#zm{F&_?yjbIkFFk6bQ4>XB<1yUZBCu*Upv?ez#+_|!4 zLv+}Qms+=CTX7!8fXjGx0lzM{9X)Z0>7C_gcf0ex9s=@tXWleCOgVmCT;X^$by;6u zd_a3D5;Wni8>r1?5jq*;fT{XKV>98he|GY0yh)MB!ZqJ8`;mpb(!GJ&`x-U{a#4AU z>C29?lG|>%w<2FI-{J=NJ;b3184O?+xMwp0o^~_@ad2@%0E^%+<;C6DPu|A$(@+U* z_y24t#aZ>?3D?;ozGf?q7Ygns4G59L0_nr3JfNg^Z2BUggwD(OBMSecr&M12K}?a7 zJgb*+$x+r@EQ6BE;bJ66V{|soo+}!of(6`$o^Mbh@d%YNlHL#!peC-BO5YEeWxhXg zr)sI{8g;LuUq%~vk*Sh8rc1rrwC=I{+^wm^)jw=EJD-C8PJ-nSYiO-t6B_n4C^;}Z zNO#2A1#>#$l|DJN1vNJB>F^kogw*cVY3gLcwpaISd11EANCao}`>V21i^ey|WI}nb z^*5pZHan$FT@YG7KHaGS^R<fTJ;2D;d6;=!gs|7S^atxWfWmp7nF7A_0@IixOvxIB zLLL&h$#d9ccAC>QRGs13@Gi|+QFz!TX)Y0pud1yQVpUo$q4eIzqB1A4L`RfH2fx8N zp&boHKesMU$4s?-7?G)m%lfm$UP}!pjaI@>A?@S$-6Sh~ISRW|g$pjZGE8p~cS9`D zRwB<ajIC5x(h<F@vpXK`BNH-mZ&hQ>^=Pd2m3q}#UNHI>D|*i`V=w>U6;w^?XBj5= z>OJVsdo<{ns3a{|Zz^<TQ%TLAt6CPpLOxAii(3|8D!yUbX32Wk$y)5_bP%mBK87f8 zsOrH(8Sa!3r@w+3T<BF8p|DoUwTl%tekSv#=gq(y*K!#L;(g39dzu+p7&T$a9<l=? zH*u4?!`r$XaIBw~KiZ)UlAZL>$aqhKIXL8bu_sXo;_GS(m{Jx>CH~DwcM4ZtXyzi8 zCW7`h$d=_^gw(lWYAs<DXp78BarWJ&_2xCzV=DyqyXx5HoHg*I2Wyeg*T}A)#tCe; zXIy{)WwDB;?yp2soc9Z4;loVVN`9Gh=_iTHI@8A#UDuUl;Z>6tJBmJ=w~@7Cn-Zyp z{W;#lo)WGyej9q#_&$}mbbw*L`D>9lU%8y53*d;>^yU$z*Wf1X16#BRx`Ul$1xp44 zHovKV0KH=^mW;8KOy~PD64TjNFs#12*d<MuPd4JEuRF_752wFtw|oxbPsl>lGZjqa zO-K|ae7BF3kS|K~6c(Wz%+J``i22d}(;7bMUi$7`e|7b#QsdHlj-(S{xqzd}{ubY6 zl76FwXhq8$vle@A{DR|iOrA%g4p(L`trLft4gHzbaVu;Iw)9;noCmP{POzU=Yd%;D zw?B#h{-`R=FGSt^YjTVv8Nl&va4dO*uilfdriR?G>6robLlo=N9NE!4>ET4hB|xUc z7Hf{?UGZ=d;fjG14Swcge#6ba;qs29^ioFYw>|MuD3c|(4mdUiN736xZx+Q3iTNF> zUeX{Td51_Bnu^ZX*A4;pvaJl@em`;wk?R(=0SZfV^D0yF%yPrJ)!Q~bhl}?gd-<%1 z>5#Aj)~^$SUbKk%>+gf{qbY3JOB!!9)SMb0n*2l(6{EqQWL;WypMAOW8m&`EtU8hu zj<0%QlV={|SOSsLD>E#VTE!h0Ao$+W*UgXeLRJgFBAuisuEi@Sje7F=;H!LQ<pFt1 z+^+Wg1gLq-)vr3Nez$ZVj`l0Y+&tM#UUvTG9~L9^R)HUMamf+T?sc~nHFo=E8ZfDu z8--AI*DJ7n8%1cv4zQ7(9OI^N0xf^>M`h;BOXI>aqVvdJm82H@Rt<6W|4zgXdz1j< zBGw<HewVx_wFo4yB6Lr32w0>%Q<0WEwAlgXZ(3qnE$~nX$8@Fl7kw(PHi6Oky`C47 z8#TSO{&>W2wSj7iVKYTAu;@iUSqhRRG4u{+j_1-pLmRi_`DyZzXD1`D%!1BC+ie<u zdf;1~`uNyc$N>9dvRjmpl{9lQ1G{1oL^aH^nKlEypV(j#)g@=+H!9$Sg`yw*8$U7@ zYgBSLRu|8ck0I}}iU~I#ZDFv`@X?q;hBM{APl}CjdeE3E%ZSEufj$=!hQB6bJ2O2> z0L<*zxmc*Lb^=1_Uc1++$7P=3)sIHWX7EcpBQ*BB-~D`*8aUD3n0sy_>lU0=R!((_ zmJ549kr9GjwaL*RgRmt>YTTdm8U8wX%lLKaSL28C-#)8s?wCwl<XOhEzDV58ASKKM zZ4^+sMS?1f=-(n$Nx01~RFZG57=`rGf1w<0ZoQ+c48kH1_P|DS;=ddUF~sJ)OIoob zet-(|raim8KKndJMfdr_)?<V8JT}EY#z^Om&YF7`EwDJN$-<68*!uC;;OVwlhUt~! z0R+4}qN|uHG7CMZT%hnx1IK{prY;o!l8M`N$xJhDS^ecE|CP$g{7RS4Z4Xt=l|!y{ zpzDIIUj^HTZ^8RHy7;OEpHt=%Kj!NRsCDj0todG0;l9dEVz^`LswrK=C?tDQtBAOs z`z_eT8Pd+^hr}_D+Ai&fqEwB)Bi7<N#t$;?bC<cd$6eN1L!a50*|-jD!pWJ~IFjJA z4G=LG^QqOVOWOFs_O$p2^O?T}VjFlhLYOGfR9|SX4dK}tbZNg{Mkf15GsV*J#d#jF zRTX9oA!%8ywJN~!#=ACkpHQ}6Oskn3pCNhIdUtp6qn9MFud3VDSv9bRnEN1oD4Tzv zDT_MEwZD?y%kSW!ZW*I-)YkFBRPva0pliwTObHi4Ab0)Xa^!NtbXDoKTqo!F3^{40 zB$GH7$bc1z*KQ!nI@&0dWUOPMo58)AtjX}<8(pf0W;KgejnSy`7`1xJo|Kl$3#Pnp zbz-F-mI$XdL}S<_7YC(42k9}k7V-7{F7N{(o^Y98c-99N+3xY&5%`q>9yQX&p>UL* zW~fi?M7qO)kQ>Y5GLe%YT(um5!f4V&#Sh=?iY@{|l5k=qI+#z>mah1ehiWK9>6daj z@=BLda)?PszGtAB>tw}q6{OWLO$?}~{Ycr1T(T$ICfQFg?iVY+{#jkgev8Nb-7oy! zedyqqRbAaz);&~B?e`&f5_3^OZ|@!KoqtzIk|Ka$?uI@JX~GW!+TL01h=%N)Okh=P zwl+0i$!KZqEBP63#++-jofvMFO%H{|o6pteAR`GTyOyNwMYh|MDYRP0p(vc^1%AaI zUcc_I`w|%I>vf5GXw%LixoP#|mk9u1C1rA)HHm7%|Gf@i9s9_TH)4{!)a;fsjfhHo zvjZvgDMsboHFCes`7|BBtUyJ!z_d_2zx4CEDc3iEk*Q)8$6q&3@272ZA+=|f;A>Fq zWfpm~)Yg4k22AbT%53;u3Jb?@e0go6XhP2ZtLdoo=S>hA+X|ZpIsdCYAWo;rt+v3| z5VrogTOa%`Yy={yM@a-}t)lPEi<M;!ISm`iJ@%XQjt8u)qFg5QBd#h6&gKR?%Am%X zufgqX6*EOBhfZO)9fWz`KfOIfwkrO3fNW-gr)O<}?7un)&8>?0c*fn1B3G&j9^Mmd z6CI_#3~c*p4t|FDleXtsJ(|daP^HhNhmp<#>hzk9mYy4SX;kwRB=cjzIp|$LEwk4L z)JdY&xO{m{@<d3WX}YvVb%R{~I|`yW>t~N>rHjS)ZS%QE@EdMf%;E+f_AQoG`_tR6 z$k=XBo`kuPSKNwJDSRK9_%M*S#uZFbPyaf@06}ws(*z>})IO_=JRL*Yu5ij_LTZl9 z5ZsU58N7Oti<DZ}Q#h`Lg*p;X@q9X*E7pQziAL2+>uSp%;ESVmp)97gdgy1(@Z?jY z^oQ#tr>mMx*m8!|*H?L?cMcrK<fnQC@1O?z-EXoFFCFytOOk76UV8PX7Rm(2o!ON3 z_zc8Hmh)0xVz3}~5K$_l(HW13BO0I`_FMD)ESl{f#kF<~Zb}6K)e>aGNk=j5Uf9*J zk*RrKw{!YRTQP8oKI@RYhZm|A=M6jlm(Ul!-yj(Oqk!6f9~=8Ca`m6FvA;q`zo-lU zWuWUXzRAC{rT;$&bp4OBG=T!3-_K1R)P*N22oMEa=x!(Iznxsuo;2qYrQkr5KkRD9 zi{A^K*tfYTpFeOAPCpI2(>Y>RVE#U^hZ0T8si`faQLb5x_v!vRf%KvowtKZ_D`tht zx!E>st;t_*z4HF9JH0%GO<!hL7SxjdY`Ai1Ps^p5J%ilXeZ|nc{lJs2jJCi=ny0Py z8Hv)>&EBJ{&o4RONAI89nVmQrB&X0fe|qV8qcN4tp`hahacf>kdB}Qjn4ZoC4`k99 zdW05cu2J82p5m$~MY-P3<B*P*hrhWpxY+a+tCE|VqWmIPR|^S`FV!(2Ca=G1$-3K7 zk(!-&d{E?Ii<_4(_3)Xdx#%Os(rA7@$PPrY_kEd9Uxb(sj|GN>;bb=)r7*1(B3xF| z6E0|HThe;wQkM}~Sy6M4Zw`9Ydh_gF!~UzB<|RFq7}c}6kzrHvOQ_D&=UxEpQ%##{ z&s;$Bi$dEc5i0o+*xGJ}WOc9x2H`%8ck2={MBF|@v#bQJ8wh6yTf1=<-FC`L0hcA^ zv~(K|dXf5xYjXa}iTF(ZJXejZ2L6T*?nXLIIvd)UEG2!tL;(xUacJHF<2)r=ljLA0 zb&?PV1Wwqf$~)0@EAb}p=d_Yo4^X46VH*=tTxtCMP<KB?TeM8{8u4VbK|o9lZZ1!C z^N-L)C1Is?05Wtny(3j3{4>v-xt1^lvmmk;P=xk*_sI-yr7*d^)8o4Fc5e|hufBUB z6DN`LdzP<Dm0ZH1*LW)TkWoH$uY7{X6H}au;(=iXl?l3N#hwV>(!37Him%!b&jJmw zG&rbpu#uk&tlxgw3!yx)YoX9_JJs<><9yZ$gR)EoExv5!ARAd1ky>-lvf@9FBrzdC zmwM0A(Ik;`C$OPi)+GNH-A5&>$K;Xt6W8=*t+4Fb?<CJHk8goLL&cafIUtXocUmPe z(A?qS;~7=b`6+5Cdgt{)648rY{FAan79ME((`yC#11YO*<sm>Ec@I$=k#l*H{3ndC zxHv|i2F&hQ7MQAi5(n(`3KNr&E|w8ikm^Y{pw+}7^^DG78>H5_+g()78g9Sb@kn|| zIx51-#XVY*g6p2)=`fVt?=p|(OsEgGUo!9b2yYRTxn!B4>=Y~b>@>rKS}pt~A8#+M z@LNx{vwci*kk-c}xqA##G-BK7*ZSX_1ilZJgvkFKZXX?j1=k>H+vhG8bD&DK$&exS zZ8O(U-Xgv+;~l`Nxs7a@+tVG1HVC7Q$AWQ0hb6A!ZX0oCC5Bi#o;H?)3MNfnywXQj z=L+5f<{?7m++#&6!{S{$Z>G=$?)aNUPQfTDkh*qEOFkPX&-Ta<>y!d38~F?!X7vcd zWrp<Zai7EnzS8yPOL<UzM!uZqK>OK{C3r&kQ=A+y;y7f3ck&3i(Mx`BgZTL@A?go4 zc`GH9>Pu;3ZcZPYV;U1am9GFyHtS`LQe-tfTOck$Fp4;61KIgN{9uJk==4GQ@e8p> z4fmLCcS#W4e71V_j-3oew<IJNIOb!z!*Xk;E!VSn!_6i%_%Z5sK0(sL#QUnBN0L=9 zzL7>D%OsjI7(|)+xh0ySk44~Q#!&NeH1z{J^fgC5$?LupANFbz*H^E<=I}hDBhz}0 z4NyxW8OKnB76}MxQot)_mNOL@5hA~G9^Xey;ah~y3`UGF=>vWA#oEf#^u!!NH7Vw# zs|~M9-?_RQl0eG|^4j~M7P$<ikMb->1m*`8)tTTYc^0xZ^0PekAqaTh58EajZ;!Ex zw^Sk=*Qlhn8TKN#QCJ!J$x+Z4j~!nGXw{^ma&sz)0v~`c>!jP^XDW!uqrR93MzK5~ zPNAz{5&#h7QUV&-Rww$St16>YKmo`(n)giNk62i9Rti1uUG~gk*gIspzd-pAbmtrl z+d?Dg`2$m<-O*a5t!C{vVH3<uF+Y}4yW9cAJXCTlfb3}MKAh86D=#cD2s`F7%(Ll) z*V8ASO#9<3AVKvQ`N9P798I|r(t-^n+Xjsl?6C{;2tRC!a~O7Z%OiZ%;l3nBnB$gp z5zh#L<b4(<>aiOA*m|WU>-pTotJ@cLw8+@;@QGBSWh~N+w?Upg!9)Rz&`EhWo)t_d z#as1Z3BfqoA|td`W|8j49{Zr5YFa$(N^ieI)=*8{a7tOHBokVXD3W+}t=C$P$W5G1 z{ssLoaNY@jvpai=mTc!-)wn?y>61659tCq3cI;gM`RE?qR^c1La-bc$W+Jo|Gz3$( ze^t#_#Zchln9d&ww2O{Ubw7W8vNZAByghV_Zb;TJ7>L%ME=wMbvDpg}FJpRHKN39@ z817ykQk7=WeL}RNnpAhraT3gHm58PVU_$&}VIwY6g(+@fCKqhhqQi#FY8Q==;(i`{ zyfhJ<PnkU;nT(AYKh7c*B{`B2MjY)oebYT*+on(J=JC1_i1?xwMvCuWj7P#`=f1_n zWc{s(EQRIdQ5Ix?Stw7GAdW?}UDL@ecpO<AwMH(SnwBk=v<fE4Mke}|xFHfDsj3v( zgP~>P6vuWb7Rd&~G`tj1$Ba9#&Y2DNHD>{kAPt(X-g%ywJ=rZOVC58jDKQp5G@TLE zy;;d>pk}abtm9oKNDW>1RJTm;gc?=xRsMXyNZj!J@$DE>EZx;nb&`j0#mCHsk77v| z{JMuRFAn-}_p%apvuDC=UW-k3^^F@NYRM*N4CrG|d0rb0(LXiNKA>%BKgFn~jd`nL zhSpqCwsa7lUqAnqybGVemYtpEP+?#_YyF)1P7OIsyXriX61U=HE^WNy@xp4u3?k-w z)x~=*(dKQzumjSj+~b1Oc!MroYX7FT;aXB28pjJA()3+QrrqL{4h9{fZUr`Wv~Xzn zf$Fqy7s+(IpPz2}Mg5Ard?2;kQ=CWYAHRTvJYM90tw}15hekdlFi?g%3&R4eR$o0$ zHOptVcTLJ>t}YZ(c2e=okT^9h?w7?E#%<!s*5MwW=Ffn-Y2ozvk?(P4&Ac=5q9Y5Y z`k}nhPEE)u`Bm!fDzFN6K*d3$fU;E7uqY<x(B^pHiQ%C9xto=CGo0qI-cL=GGAIyA z22Mwy6;YlrU!A}#K_}*v_zB1}74s=Qb+PF~9?!AVP>`!jPCXcs=MjA)rZoY#2ecg( z{fOgp^y19fqO`3SKw6SdSMP$|>BtZ?`kM%oD&8NpfB6_ffWyzeA+*vLlP6@-em1<K znYhL$fl|l%V#Pb;o}lr$$$Yg$HEQ;E2fN+@r+ZpfJp?A<C&^$EAGNth%AwWuOcGB9 zZPa?kFx58mBl0D9?CT|f&+%@s=uHKxD_f->$Bh}t5ShY%o+olab~)jC&BILTDb6vO zJsb|A(Y_xSOp?%uEAx~UUF{y|Zw1^v!hRx4YIX`kU~5x+JFXISaTwa-C)9+W`a`3u z{F@~|&|6ju)0oawop9u|<-W1P2rN?=4T`9iFezDB$&YHOPWr<4it$rH<M`TVN~rUD zjJF(}Ls~id80kwN32<1~B>|DjW#J|YQK(MJcHwYqLBd;AiFSKju2cSza=Rx@!wNjP z2lw(2)24(Pnpokp+lloIQ`QxYG^Z+5hCsMq3CSJWd>q)e1TR;%HTl6@YebBE@|&_A z&`gLFn4Ii9pL!XYD~AF<;Uw`fUJ<F=ry-}?a?w$NLfwwbMlbI#eFN{XNwINlQP^am zpA~6N7D>g7X-H+SWKECa8_mvpPD%J?`E&;b#5}qOd+?i4Bb*2V>T_xPuI~YqdNW^y z-}Y`(c|{z}-l&o#=$*8XBUxQ`DcZh?)Gsl_eG=Y@G!EIt#+0sM{Fuiy+V+S|HfmjU za`p1Q&KsH7DIgmWku7*|i%E&K51oMD(lA#5UGHGCn=6FuoAo*WfsIj)q6(7iNw3++ zGY^g*odBCGM&fvxfbN#7pXX7+3FtJEc5NlVGD;$Jpz>T_sBqY5F6NIQ7<VRhy#p=I z0}0i4Bu}P5g<kq0dem{o=dKu|hFM+D>p;`t`!WeZ%U*qSPrv0u`2F@QW{z=eqD}^d z`9Og&XogbCbOxW46rC-ts>cyY6E&;^pD1S~3Jvs`;9?fm_*0f^OO=iEe4T5tU2WZ0 znmXPzowCi!dr{lslV@A9G-SeWk+nmY{7_<B@<nbyAkzi8OU%ERWAqGfw(Pp&LHO=v zb55?OPkNMIzHgO$VHsH(7^$bo{z-wl{(#LSz9l2ov>lQubm6<6A+>8E&7hi*VeC^E z=mN{y&zUJ_!FTJf&^odKqaxCvfNxn9ZKK-r4YxbQzH-de1z<d--tRZ>FQpX7Ppx8{ z4K7tB^%WE?Smp1SpMKW-F;Vc!qQT_u+*tsLOYei^Vo;?@+<O%(5p&l(Q{<^1C&Ndc zuA$FS`c8okY+d~;(@<D?^o6pZz}04xOb1PCqttX8Na5wO(C4Qf5_J#c&!{X2o3$0r z!sv?VHU=_mLm9M*g4kZjlW2dUG@iIG?VO>%5?06Y@+omQ^;>8w*Pk~_EnPEajCE67 zth!T}AhHfkc_$m&$(iE97P(|hazk{A-%xbh!{n>pbZk#YlzWEJ;(c>Vg_~}4w|1sT zF06gWu3XVnd2ma7L*6lSW<YLyba*we#mMS-%LrVm=!<Cc<IihjeNKfp!eAYB^p>D@ zl=_r~GCo-6^@{^~!evJ|KR8~s$YQoLoFPgh3LVvVm^zStwv`KVM5!yJyh=vJrtzw+ zy$)FLDr8E{pTU24hwV%F(!2b&A7JDQ_A$o*CTnYToeQ$Do|k<hDY0d?ZCxGCMz9p6 zC|x_vdQOqXNs^hV^Lv4bpT{M)H-^-9{^@9ycSY~^Z#u+@%i6x&-v-o+#v)ta#IT}u zl`1Ep#vhbZiigL)MHV>9X<<hn)@~1f9i#dKSyps=VaQ$OdnJZuDbKeafV@Uh&`;Ie z&%iU-z#!hsbYHOV{Ts^GZaysC*c{qmg>SA1KZs-zafAT-BU2W1uS^Xds?pq*%m#*f z-3g@$No!_4Bzw-(JeGtNfhBqg#>=($$0#0$5wwcpR!$1|OAyRACM4nan{d6?7~j8B zBT$^`LO03RLO~Dek?<ntdPe-J8rja-9fQ-%8AH(6)8us&-dpDEsi#0RjCNvbo)NmU z^P_@wtF74HX$wDic$kX1Q7DEQRxj>we<RtDigvXBZPN0Yl-n8yK}WL`zUEp-)toib zTHJNoL@`yyEC}qlD`i7(Uw}&`PuT)cIS)9DlyhhQsHLjx+$<m<6TDqXZ(`$&<r$<H z^4?w9XOemEtuC~=B1v>q8_H+M<Odm81|=GzKEck54Dpnm@d}etZA6F8`sGl=C^8C( zM9m^|KCdO_XnY`?&Qo;Qbnj+7lM1H79vQtnKdVoXyqcrY*)Fyv&$zx0WXcV)Hc|T~ z6zp0mHhn2BqtfADI?S$jgL>K@)ch%DU-xm=P8|D|0RpspBME_|<i;j_6vPNKJJa_h z4f&|9pY?osUA%PZ@a~}#SwxyUc;jtviua9mDr)iv-W6Slf7*V|Aq3w)>g{3m;)xM- zYfj!#f$1ojYH5J6s>S>KUV^(f?q?nS&i6|m0d7d}LqEl7Qqa~p&UWy+J=&$pyn3+! z;uj~HgAly2SdqivW~9?%60B!$ieWChmeK^OqE|l6UQ1Ioy1`k4x)Rh{kB2NFSs2}v zNN;Kr`lzqHsVe+W_@LLq+@&tCVW{j4lss);$*w=AgvTdOMWu{~0^L?*Kj*o}5;ljv zVOFDjj8FFv$DHFtDU3@MjdYAykn!L=E`p&pr@%aHI*qFTAaxYkiv&71=|!W<Pp}24 z1wC;st37UC1>&w27sOk8T^%W)+=Zu6Abn}5HX+@AYXT)hsj;>~Q9P%VD0^y&iqh)x zW~D%w;c*KYLDwaNqg1@^!rR=APLWqa%24VA{`vK*`;+xRMI%e8DVlH+?m0=+Rjb_- zA1_8zL(SHpb?EU$T?la#cTr!LT9LyHejjn9(!Jhunh&hR$2|!O`<-(yqOE(j*kPv$ z=?QW{A1uJpo|b%b*!HCRgO>ebY@4!4&PDKqsuW?rpE*^Ianw8+VCJd)F>`0*6`f_B z&bzn;yo$QZT0v_WBJfb{sVEy?)el=fR{vW4M>1M7&DZ`S{)?iw4VJ#V3?+SB?oW@d z6nO89Mm`qhl+8`ZT560;?b<L*2-pl-XxL%-OE9;(uqGuPAj+N_w=2_m@|P66bmnvV zHXcLjeZmUOQ(I>poPT#3`#P^y{QH8g(-*<k$+Oi{uUl__5mD`uX`hm5eu*t7e>5S( zY^54S#lvT&Wb{{%7pbX9*;1IzvI{*FfF>7YFK7oE!2`1c?GI2jF49sVE^(CV971W? zlr^$S$QoMGxji;*YN*9gzCaR`g0Nk3k^-GjhRihJfkmtm&Y@=&UulzIe4s+G_Zo-& zmu|(s5WoL+>HR-)EB@st{LQWSS33Rw&8_$wTT0Oe0#DKZR~G)?DgS@z_$&Qh%E<#> zF2MJn<GB4wU;p2U_`u%^9e<_i!_)u&E-v`np($nRZffsj^;@6c3ZA4~Elk~=TxnVF z9}t`oAPANS?g(}Wv<N&1TnIq;MT@`*|9bs(as5XNE(CJ~Cj<vLN*E5Ig?O053oKb} z+~ELPPG?gOdnZmF5Qx_BZ;3O+lFQAK%hAG})85I;)XK(!%iPJq@F~2nvJ*u57gU#3 zT8J0G!v_EYKs<aPK_DMHfR_aTV1YOJOF<ha$N#3?|5+4oR|`ul04<Oo3r;8g*9VSf z2Ej`kENTCif#I}fc!|ZY53S?BWPjiNY6t$e49>R(!&mFicE1R`f5>=$)ld8(1OB2` z{~`N@PX1FSAn=dAJUjsShWNcN4<GNJG9VBn058S(qunpK_#ZMLFPy0Thl~dd_-8vl zo<GOq;pgFpmsk8Y2M>G!;oIyF8Sobo`F9yF_!oHncNzE>FZ_2I9~^-Gr+&PG0{s6N zkDu=sR{6K_fPDP_m;=ZM1pfIPd~llgpSl2nz<>4y!gu1Ia{&3_$nQVq1@rxi9sI5f zKN$QEnE?MkbOFN4j{jVH5C9Hb`C~i~?>}_`@$te@<G;`Q_ucP%7Yu)q;MU|f{oow+ ze>@uq$p6cV{MPR8yFbPQgZ{C1!8}00Kc5%O17Daw#sh=-|FMVQEOt0~{onJtyPCp5 z{jOMlVY1~NEuCopV!p#|s)m!3I~=$Ew^h@$@wNE%YIDm=(;A5InVUh(;jnqQ#o)CN zFojq^AUx(^fGNNNVr~i$1c-^4TJQiZEqN^fro0d^1k7)0DadPSYGDZk@q+{fAUtA! b)e*V5o4UIHWjDb503aBPiAhFP7VCcjJB?-c literal 0 HcmV?d00001 diff --git a/pdfbox/src/test/resources/input/eu-001.pdf-sorted.txt b/pdfbox/src/test/resources/input/eu-001.pdf-sorted.txt new file mode 100644 index 0000000..c64993b --- /dev/null +++ b/pdfbox/src/test/resources/input/eu-001.pdf-sorted.txt @@ -0,0 +1,159 @@ +E-PRTR pollutants and their thresholds + +A facility has to report data under E-PRTR if it fulfils the following criteria: +• the facility falls under at least one of the 65 E-PRTR economic activities. The +activities are also reported using a statistical classification of economic activities +(NACE rev 2) +• the facility has a capacity exceeding at least one of the E-PRTR capacity +thresholds +• the facility releases pollutants or transfers waste off-site which exceed specific +thresholds set out in Article 5 of the E-PRTR Regulation. These thresholds for +releases of pollutants are specified for each media - air, water and land - in Annex +II of the E-PRTR Regulation. + +In the following tables you will find the 91 E-PRTR pollutants and their thresholds broken +down by the 7 groups used in all the searches of the E-PRTR website. + + +Greenhouse gases + + THRESHOLD FOR RELEASES + to air to water to land +kg/year kg/year kg/year +Carbon dioxide (CO2) 100 million - - +Hydro-fluorocarbons (HFCs) 100 - - +Methane (CH4) 100 000 - - +Nitrous oxide (N2O) 10 000 - - +Perfluorocarbons (PFCs) 100 - - +Sulphur hexafluoride (SF6) 50 - - + +Other gases + + THRESHOLD FOR RELEASES + to air to water to land +kg/year kg/year kg/year +Ammonia (NH3) 10 000 - - +Carbon monoxide (CO) 500 000 - - +Chlorine and inorganic compounds +(as HCl) 10 000 - - +Chlorofluorocarbons (CFCs) 1 - - +Flourine and inorganic compounds +(as HF) 5 000 - - +Halons 1 - - +Hydrochlorofluorocarbons (HCFCs) 1 - - +Hydrogen Cyanide (HCN) 200 - - +Nitrogen oxides (NOx/NO2) 100 000 - - +Non-methane volatile organic +compounds (NMVOC) 100 000 - - +Sulphur oxides (SOx/SO2) 150 000 - - + +Heavy metals + + THRESHOLD FOR RELEASES + to air to water to land +kg/year kg/year kg/year +Arsenic and compounds (as As) 20 5 5 +Cadmium and compounds (as Cd) 10 5 5 +Chromium and compounds (as Cr) 100 50 50 +Copper and compounds (as Cu) 100 50 50 +Lead and compounds (as Pb) 200 20 20 +Mercury and compounds (as Hg) 10 1 1 +Nickel and compounds (as Ni) 50 20 20 +Zinc and compounds (as Zn) 200 100 100 + +Pesticides + + THRESHOLD FOR RELEASES + to air to water to land +kg/year kg/year kg/year +1,2,3,4,5,6- hexachlorocyclohexane +(HCH) 10 1 1 +Alachlor - 1 1 +Aldrin 1 1 1 +Atrazine - 1 1 +Chlordane 1 1 1 +Chlordecone 1 1 1 +Chlorfenvinphos - 1 1 +Chlorpyrifos - 1 1 +DDT 1 1 1 +Diuron - 1 1 +Endosulphan - 1 1 +Endrin 1 1 1 +Heptachlor 1 1 1 +Isodrin - 1 - +Isoproturon - 1 1 +Lindane 1 1 1 +Mirex 1 1 1 +Simazine - 1 1 +Toxaphene 1 1 1 +Tributylin and compounds - 1 1 +Trifluralin - 1 1 +Triphenyltin and compounds - 1 1 + +Chlorinated organic substances + + THRESHOLD FOR RELEASES + to air to water to land +kg/year kg/year kg/year +1,1,1-trichloroethane 100 - - +1,1,2,2-tetrachloroethane 50 - - +1,2-dichloroethane (EDC) 1 000 10 10 +Brominated diphenylethers (PBDE) - 1 1 +Chloro-alkanes, C10-C13 - 1 1 +Dichloromethane (DCM) 1 000 10 10 +Dieldrin 1 1 1 +Halogenated Organic Compounds (AOX) - 1 000 1 000 +Hexabromobifenyl 0,1 0,1 0,1 +Hexachlorobenzene (HCB) 10 1 1 +Hexachlorobutadiene (HCBD) - 1 1 +PCDD+PCFD (Dioxins+furans) (as Teq) 0,0001 0,0001 0,0001 +Pentachlorobenzene 1 1 1 +Pentachlorophenol (PCP) 10 1 1 +Polychlorinated biphenyls (PCB) 0,1 0,1 0,1 +Tetrachloroethylene (PER) 2 000 10 - +Tetrachloromethane (TCM) 100 1 - +Trichlorobenzenes (TCBs) (all isomers) 10 1 - +Trichloroethylene 2 000 10 - +Trichloromethane 500 10 - +Vynil chloride 1 000 10 10 + + +Other organic substances + + THRESHOLD FOR RELEASES + to air to water to land +kg/year kg/year kg/year +Anthracene 50 1 1 +Benzene 1 000 200 (as 200 (as BTEX) BTEX) +Benzo(g,h,i)perylene - 1 - +Di-(2-ethyl hexyl) phthalate (DEHP) 10 1 1 +Ethyl benzene - 200 (as 200 (as BTEX) BTEX) +Ethylene oxide 1 000 10 10 +Fluoranthene - 1 - +Naphthalene 100 10 10 +Nonylphenol and Nonylphenol ethoxylates +(NP/NPEs) - 1 1 +Octylphenols and octylphenol ethoxylates - 1 - +Organotin compounds (as total Sn) - 50 50 +Phenols (as total C) - 20 20 +Polycyclic Aromatic hydrocarbons (PAHs) 50 5 5 +Toluene - 200 (as 200 (as BTEX) BTEX) +Total Organic Carbon (TOC) (as total C or +COD/3) - 50 000 - +Xylenes - 200 (as 200 (as BTEX) BTEX) + + +Inorganic substances + + THRESHOLD FOR RELEASES + to air to water to land +kg/year kg/year kg/year +Asbestos 1 1 1 +Chlorides (as total Cl) - 2 million 2 million +Cyanides (as total CN) - 50 50 +Fluorides (as total F) - 2 000 2 000 +Particulate matter (PM10) 50 000 - - +Total Nitrogen - 50 000 50 000 +Total Phosphorus - 5 000 5 000 + + diff --git a/pdfbox/src/test/resources/input/eu-001.pdf-tabula.txt b/pdfbox/src/test/resources/input/eu-001.pdf-tabula.txt new file mode 100644 index 0000000..ffdbba1 --- /dev/null +++ b/pdfbox/src/test/resources/input/eu-001.pdf-tabula.txt @@ -0,0 +1,209 @@ +E-PRTR pollutants and their thresholds + +A facility has to report data under E-PRTR if it fulfils the following criteria: +• the facility falls under at least one of the 65 E-PRTR economic activities. The +activities are also reported using a statistical classification of economic activities +(NACE rev 2) +• the facility has a capacity exceeding at least one of the E-PRTR capacity +thresholds +• the facility releases pollutants or transfers waste off-site which exceed specific +thresholds set out in Article 5 of the E-PRTR Regulation. These thresholds for +releases of pollutants are specified for each media - air, water and land - in Annex +II of the E-PRTR Regulation. + +In the following tables you will find the 91 E-PRTR pollutants and their thresholds broken +down by the 7 groups used in all the searches of the E-PRTR website. + + +Greenhouse gases + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +Carbon dioxide (CO2) 100 million - - +Hydro-fluorocarbons (HFCs) 100 - - +Methane (CH4) 100 000 - - +Nitrous oxide (N2O) 10 000 - - +Perfluorocarbons (PFCs) 100 - - +Sulphur hexafluoride (SF6) 50 - - + +Other gases + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +Ammonia (NH3) 10 000 - - +Carbon monoxide (CO) 500 000 - - +Chlorine and inorganic compounds +(as HCl) +10 000 - - +Chlorofluorocarbons (CFCs) 1 - - +Flourine and inorganic compounds +(as HF) +5 000 - - +Halons 1 - - +Hydrochlorofluorocarbons (HCFCs) 1 - - +Hydrogen Cyanide (HCN) 200 - - +Nitrogen oxides (NOx/NO2) 100 000 - - +Non-methane volatile organic +compounds (NMVOC) +100 000 - - +Sulphur oxides (SOx/SO2) 150 000 - - + +Heavy metals + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +Arsenic and compounds (as As) 20 5 5 +Cadmium and compounds (as Cd) 10 5 5 +Chromium and compounds (as Cr) 100 50 50 +Copper and compounds (as Cu) 100 50 50 +Lead and compounds (as Pb) 200 20 20 +Mercury and compounds (as Hg) 10 1 1 +Nickel and compounds (as Ni) 50 20 20 +Zinc and compounds (as Zn) 200 100 100 + +Pesticides + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +1,2,3,4,5,6- hexachlorocyclohexane +(HCH) +10 1 1 +Alachlor - 1 1 +Aldrin 1 1 1 +Atrazine - 1 1 +Chlordane 1 1 1 +Chlordecone 1 1 1 +Chlorfenvinphos - 1 1 +Chlorpyrifos - 1 1 +DDT 1 1 1 +Diuron - 1 1 +Endosulphan - 1 1 +Endrin 1 1 1 +Heptachlor 1 1 1 +Isodrin - 1 - +Isoproturon - 1 1 +Lindane 1 1 1 +Mirex 1 1 1 +Simazine - 1 1 +Toxaphene 1 1 1 +Tributylin and compounds - 1 1 +Trifluralin - 1 1 +Triphenyltin and compounds - 1 1 + +Chlorinated organic substances + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +1,1,1-trichloroethane 100 - - +1,1,2,2-tetrachloroethane 50 - - +1,2-dichloroethane (EDC) 1 000 10 10 +Brominated diphenylethers (PBDE) - 1 1 +Chloro-alkanes, C10-C13 - 1 1 +Dichloromethane (DCM) 1 000 10 10 +Dieldrin 1 1 1 +Halogenated Organic Compounds (AOX) - 1 000 1 000 +Hexabromobifenyl 0,1 0,1 0,1 +Hexachlorobenzene (HCB) 10 1 1 +Hexachlorobutadiene (HCBD) - 1 1 +PCDD+PCFD (Dioxins+furans) (as Teq) 0,0001 0,0001 0,0001 +Pentachlorobenzene 1 1 1 +Pentachlorophenol (PCP) 10 1 1 +Polychlorinated biphenyls (PCB) 0,1 0,1 0,1 +Tetrachloroethylene (PER) 2 000 10 - +Tetrachloromethane (TCM) 100 1 - +Trichlorobenzenes (TCBs) (all isomers) 10 1 - +Trichloroethylene 2 000 10 - +Trichloromethane 500 10 - +Vynil chloride 1 000 10 10 + + +Other organic substances + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +Anthracene 50 1 1 +Benzene 1 000 +200 (as +BTEX) +200 (as +BTEX) +Benzo(g,h,i)perylene - 1 - +Di-(2-ethyl hexyl) phthalate (DEHP) 10 1 1 +Ethyl benzene - +200 (as +BTEX) +200 (as +BTEX) +Ethylene oxide 1 000 10 10 +Fluoranthene - 1 - +Naphthalene 100 10 10 +Nonylphenol and Nonylphenol ethoxylates +(NP/NPEs) +- 1 1 +Octylphenols and octylphenol ethoxylates - 1 - +Organotin compounds (as total Sn) - 50 50 +Phenols (as total C) - 20 20 +Polycyclic Aromatic hydrocarbons (PAHs) 50 5 5 +Toluene - +200 (as +BTEX) +200 (as +BTEX) +Total Organic Carbon (TOC) (as total C or +COD/3) +- 50 000 - +Xylenes - +200 (as +BTEX) +200 (as +BTEX) + + +Inorganic substances + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +Asbestos 1 1 1 +Chlorides (as total Cl) - 2 million 2 million +Cyanides (as total CN) - 50 50 +Fluorides (as total F) - 2 000 2 000 +Particulate matter (PM10) 50 000 - - +Total Nitrogen - 50 000 50 000 +Total Phosphorus - 5 000 5 000 + + diff --git a/pdfbox/src/test/resources/input/eu-001.pdf.txt b/pdfbox/src/test/resources/input/eu-001.pdf.txt new file mode 100644 index 0000000..7ce680d --- /dev/null +++ b/pdfbox/src/test/resources/input/eu-001.pdf.txt @@ -0,0 +1,195 @@ +E-PRTR pollutants and their thresholds + +A facility has to report data under E-PRTR if it fulfils the following criteria: +• the facility falls under at least one of the 65 E-PRTR economic activities. The +activities are also reported using a statistical classification of economic activities +(NACE rev 2) +• the facility has a capacity exceeding at least one of the E-PRTR capacity +thresholds +• the facility releases pollutants or transfers waste off-site which exceed specific +thresholds set out in Article 5 of the E-PRTR Regulation. These thresholds for +releases of pollutants are specified for each media - air, water and land - in Annex +II of the E-PRTR Regulation. + +In the following tables you will find the 91 E-PRTR pollutants and their thresholds broken +down by the 7 groups used in all the searches of the E-PRTR website. + + +Greenhouse gases + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +Carbon dioxide (CO2) 100 million - - +Hydro-fluorocarbons (HFCs) 100 - - +Methane (CH4) 100 000 - - +Nitrous oxide (N2O) 10 000 - - +Perfluorocarbons (PFCs) 100 - - +Sulphur hexafluoride (SF6) 50 - - + +Other gases + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +Ammonia (NH3) 10 000 - - +Carbon monoxide (CO) 500 000 - - +Chlorine and inorganic compounds +(as HCl) 10 000 - - +Chlorofluorocarbons (CFCs) 1 - - +Flourine and inorganic compounds +(as HF) 5 000 - - +Halons 1 - - +Hydrochlorofluorocarbons (HCFCs) 1 - - +Hydrogen Cyanide (HCN) 200 - - +Nitrogen oxides (NOx/NO2) 100 000 - - +Non-methane volatile organic +compounds (NMVOC) 100 000 - - +Sulphur oxides (SOx/SO2) 150 000 - - + +Heavy metals + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +Arsenic and compounds (as As) 20 5 5 +Cadmium and compounds (as Cd) 10 5 5 +Chromium and compounds (as Cr) 100 50 50 +Copper and compounds (as Cu) 100 50 50 +Lead and compounds (as Pb) 200 20 20 +Mercury and compounds (as Hg) 10 1 1 +Nickel and compounds (as Ni) 50 20 20 +Zinc and compounds (as Zn) 200 100 100 + +Pesticides + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +1,2,3,4,5,6- hexachlorocyclohexane +(HCH) 10 1 1 +Alachlor - 1 1 +Aldrin 1 1 1 +Atrazine - 1 1 +Chlordane 1 1 1 +Chlordecone 1 1 1 +Chlorfenvinphos - 1 1 +Chlorpyrifos - 1 1 +DDT 1 1 1 +Diuron - 1 1 +Endosulphan - 1 1 +Endrin 1 1 1 +Heptachlor 1 1 1 +Isodrin - 1 - +Isoproturon - 1 1 +Lindane 1 1 1 +Mirex 1 1 1 +Simazine - 1 1 +Toxaphene 1 1 1 +Tributylin and compounds - 1 1 +Trifluralin - 1 1 +Triphenyltin and compounds - 1 1 + +Chlorinated organic substances + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +1,1,1-trichloroethane 100 - - +1,1,2,2-tetrachloroethane 50 - - +1,2-dichloroethane (EDC) 1 000 10 10 +Brominated diphenylethers (PBDE) - 1 1 +Chloro-alkanes, C10-C13 - 1 1 +Dichloromethane (DCM) 1 000 10 10 +Dieldrin 1 1 1 +Halogenated Organic Compounds (AOX) - 1 000 1 000 +Hexabromobifenyl 0,1 0,1 0,1 +Hexachlorobenzene (HCB) 10 1 1 +Hexachlorobutadiene (HCBD) - 1 1 +PCDD+PCFD (Dioxins+furans) (as Teq) 0,0001 0,0001 0,0001 +Pentachlorobenzene 1 1 1 +Pentachlorophenol (PCP) 10 1 1 +Polychlorinated biphenyls (PCB) 0,1 0,1 0,1 +Tetrachloroethylene (PER) 2 000 10 - +Tetrachloromethane (TCM) 100 1 - +Trichlorobenzenes (TCBs) (all isomers) 10 1 - +Trichloroethylene 2 000 10 - +Trichloromethane 500 10 - +Vynil chloride 1 000 10 10 + + +Other organic substances + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +Anthracene 50 1 1 +Benzene 1 000 200 (as BTEX) +200 (as +BTEX) +Benzo(g,h,i)perylene - 1 - +Di-(2-ethyl hexyl) phthalate (DEHP) 10 1 1 +Ethyl benzene - 200 (as BTEX) +200 (as +BTEX) +Ethylene oxide 1 000 10 10 +Fluoranthene - 1 - +Naphthalene 100 10 10 +Nonylphenol and Nonylphenol ethoxylates +(NP/NPEs) - 1 1 +Octylphenols and octylphenol ethoxylates - 1 - +Organotin compounds (as total Sn) - 50 50 +Phenols (as total C) - 20 20 +Polycyclic Aromatic hydrocarbons (PAHs) 50 5 5 +Toluene - 200 (as BTEX) +200 (as +BTEX) +Total Organic Carbon (TOC) (as total C or +COD/3) - 50 000 - +Xylenes - 200 (as BTEX) +200 (as +BTEX) + + +Inorganic substances + + THRESHOLD FOR RELEASES + to air +kg/year +to water +kg/year +to land +kg/year +Asbestos 1 1 1 +Chlorides (as total Cl) - 2 million 2 million +Cyanides (as total CN) - 50 50 +Fluorides (as total F) - 2 000 2 000 +Particulate matter (PM10) 50 000 - - +Total Nitrogen - 50 000 50 000 +Total Phosphorus - 5 000 5 000 + + diff --git a/pdfbox/src/test/resources/org/apache/pdfbox/filter/PDFBOX-1777.bin b/pdfbox/src/test/resources/org/apache/pdfbox/filter/PDFBOX-1977.bin similarity index 100% rename from pdfbox/src/test/resources/org/apache/pdfbox/filter/PDFBOX-1777.bin rename to pdfbox/src/test/resources/org/apache/pdfbox/filter/PDFBOX-1977.bin diff --git a/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/interactive/form/PDFBOX-3835-input-acrobat-wrap.pdf b/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/interactive/form/PDFBOX-3835-input-acrobat-wrap.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d9aa1a91baad01ba2dc4fad194ba68985e172d29 GIT binary patch literal 9924 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 BOPl}z literal 0 HcmV?d00001 diff --git a/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/interactive/form/PDFBOX3812-acrobat-multiline-auto.pdf b/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/interactive/form/PDFBOX3812-acrobat-multiline-auto.pdf new file mode 100644 index 0000000000000000000000000000000000000000..785a43934d0334c73d05f45ae50e4dd9624b8dd5 GIT binary patch literal 29737 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 literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml index dd4b457..5bd8693 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>parent/pom.xml</relativePath> </parent> @@ -34,12 +34,12 @@ <scm> <connection> - scm:svn:http://svn.apache.org/repos/asf/pdfbox/tags/2.0.20 + scm:svn:http://svn.apache.org/repos/asf/pdfbox/tags/2.0.21 </connection> <developerConnection> - scm:svn:https://svn.apache.org/repos/asf/pdfbox/tags/2.0.20 + scm:svn:https://svn.apache.org/repos/asf/pdfbox/tags/2.0.21 </developerConnection> - <url>http://svn.apache.org/viewvc/pdfbox/tags/2.0.20</url> + <url>http://svn.apache.org/viewvc/pdfbox/tags/2.0.21</url> </scm> <modules> diff --git a/preflight-app/pom.xml b/preflight-app/pom.xml index 95190a0..03dd8ae 100644 --- a/preflight-app/pom.xml +++ b/preflight-app/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>../parent/pom.xml</relativePath> </parent> diff --git a/preflight/pom.xml b/preflight/pom.xml index 3e9926a..6c619fe 100644 --- a/preflight/pom.xml +++ b/preflight/pom.xml @@ -26,7 +26,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>../parent/pom.xml</relativePath> </parent> diff --git a/preflight/src/main/java/org/apache/pdfbox/preflight/content/StubOperator.java b/preflight/src/main/java/org/apache/pdfbox/preflight/content/StubOperator.java index 9c4524e..60b3d08 100644 --- a/preflight/src/main/java/org/apache/pdfbox/preflight/content/StubOperator.java +++ b/preflight/src/main/java/org/apache/pdfbox/preflight/content/StubOperator.java @@ -255,7 +255,8 @@ public class StubOperator extends OperatorProcessor } if (arg instanceof COSFloat - && (((COSFloat) arg).doubleValue() > MAX_POSITIVE_FLOAT || ((COSFloat) arg).doubleValue() < MAX_NEGATIVE_FLOAT)) + && (((COSFloat) arg).floatValue() > MAX_POSITIVE_FLOAT + || ((COSFloat) arg).floatValue() < MAX_NEGATIVE_FLOAT)) { throw createLimitError(ERROR_SYNTAX_NUMERIC_RANGE, "Invalid float range in a Number operand"); } diff --git a/preflight/src/main/java/org/apache/pdfbox/preflight/metadata/UniquePropertiesValidation.java b/preflight/src/main/java/org/apache/pdfbox/preflight/metadata/UniquePropertiesValidation.java new file mode 100644 index 0000000..f3d01e3 --- /dev/null +++ b/preflight/src/main/java/org/apache/pdfbox/preflight/metadata/UniquePropertiesValidation.java @@ -0,0 +1,101 @@ +/** *************************************************************************** + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + *************************************************************************** */ +package org.apache.pdfbox.preflight.metadata; + +import java.util.ArrayList; +import java.util.List; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.preflight.PreflightConstants; +import org.apache.pdfbox.preflight.ValidationResult; +import org.apache.pdfbox.preflight.ValidationResult.ValidationError; +import org.apache.pdfbox.preflight.exception.ValidationException; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.schema.AdobePDFSchema; +import org.apache.xmpbox.schema.DublinCoreSchema; +import org.apache.xmpbox.schema.XMPBasicSchema; +import org.apache.xmpbox.schema.XMPSchema; +import org.apache.xmpbox.type.AbstractField; + +/** + * Class which checks that certain metadata properties are unique, see PDFBOX-4860. + * + * @author Tilman Hausherr + * + */ +public class UniquePropertiesValidation +{ + + /** + * Checks that certain metadata properties are unique. + * + * @param document the PDF Document + * @param metadata the XMP MetaData + * @return List of validation errors + * @throws ValidationException + */ + public List<ValidationResult.ValidationError> validatePropertiesUniqueness(PDDocument document, XMPMetadata metadata) + throws ValidationException + { + List<ValidationResult.ValidationError> ve = new ArrayList<ValidationResult.ValidationError>(); + + if (document == null) + { + throw new ValidationException("Document provided is null"); + } + analyzePropertyUniqueness(metadata.getDublinCoreSchema(), DublinCoreSchema.CREATOR, ve); + analyzePropertyUniqueness(metadata.getDublinCoreSchema(), DublinCoreSchema.TITLE, ve); + analyzePropertyUniqueness(metadata.getDublinCoreSchema(), DublinCoreSchema.DESCRIPTION, ve); + + analyzePropertyUniqueness(metadata.getAdobePDFSchema(), AdobePDFSchema.PRODUCER, ve); + analyzePropertyUniqueness(metadata.getAdobePDFSchema(), AdobePDFSchema.KEYWORDS, ve); + + analyzePropertyUniqueness(metadata.getXMPBasicSchema(), XMPBasicSchema.CREATORTOOL, ve); + analyzePropertyUniqueness(metadata.getXMPBasicSchema(), XMPBasicSchema.CREATEDATE, ve); + analyzePropertyUniqueness(metadata.getXMPBasicSchema(), XMPBasicSchema.MODIFYDATE, ve); + + // should any other properties be checked for uniqueness? Let us know. + + return ve; + } + + private static void analyzePropertyUniqueness(XMPSchema schema, String propertyName, + List<ValidationResult.ValidationError> ve) + { + if (schema == null) + { + return; + } + int count = 0; + for (AbstractField field : schema.getAllProperties()) + { + if (propertyName.equals(field.getPropertyName())) + { + ++count; + } + } + if (count > 1) + { + ve.add(new ValidationError(PreflightConstants.ERROR_METADATA_PROPERTY_FORMAT, + "property '" + schema.getPrefix() + ":" + propertyName + + "' occurs multiple times")); + } + } +} diff --git a/preflight/src/main/java/org/apache/pdfbox/preflight/parser/PreflightParser.java b/preflight/src/main/java/org/apache/pdfbox/preflight/parser/PreflightParser.java index c473c8c..bc9ee41 100644 --- a/preflight/src/main/java/org/apache/pdfbox/preflight/parser/PreflightParser.java +++ b/preflight/src/main/java/org/apache/pdfbox/preflight/parser/PreflightParser.java @@ -681,7 +681,7 @@ public class PreflightParser extends PDFParser COSNumber number = (COSNumber) result; if (number instanceof COSFloat) { - Double real = number.doubleValue(); + Float real = number.floatValue(); if (real > MAX_POSITIVE_FLOAT || real < MAX_NEGATIVE_FLOAT) { addValidationError(new ValidationError(ERROR_SYNTAX_NUMERIC_RANGE, diff --git a/preflight/src/main/java/org/apache/pdfbox/preflight/process/MetadataValidationProcess.java b/preflight/src/main/java/org/apache/pdfbox/preflight/process/MetadataValidationProcess.java index b6d3a29..3030b7d 100644 --- a/preflight/src/main/java/org/apache/pdfbox/preflight/process/MetadataValidationProcess.java +++ b/preflight/src/main/java/org/apache/pdfbox/preflight/process/MetadataValidationProcess.java @@ -29,10 +29,7 @@ import java.util.ArrayList; import java.util.List; import javax.imageio.ImageIO; import org.apache.pdfbox.cos.COSBase; -import org.apache.pdfbox.cos.COSDictionary; -import org.apache.pdfbox.cos.COSDocument; import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.cos.COSObject; import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; @@ -45,8 +42,8 @@ import org.apache.pdfbox.preflight.metadata.PDFAIdentificationValidation; import org.apache.pdfbox.preflight.metadata.RDFAboutAttributeConcordanceValidation; import org.apache.pdfbox.preflight.metadata.RDFAboutAttributeConcordanceValidation.DifferentRDFAboutException; import org.apache.pdfbox.preflight.metadata.SynchronizedMetaDataValidation; +import org.apache.pdfbox.preflight.metadata.UniquePropertiesValidation; import org.apache.pdfbox.preflight.metadata.XpacketParsingException; -import org.apache.pdfbox.preflight.utils.COSUtils; import org.apache.pdfbox.util.Hex; import org.apache.xmpbox.XMPMetadata; import org.apache.xmpbox.schema.XMPBasicSchema; @@ -90,6 +87,10 @@ public class MetadataValidationProcess extends AbstractProcess addValidationErrors(ctx, new SynchronizedMetaDataValidation().validateMetadataSynchronization(document, metadata)); + // Call metadata uniqueness checking + addValidationErrors(ctx, + new UniquePropertiesValidation().validatePropertiesUniqueness(document, metadata)); + // Call PDF/A Identifier checking addValidationErrors(ctx, new PDFAIdentificationValidation().validatePDFAIdentifer(metadata)); diff --git a/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ExtGStateValidationProcess.java b/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ExtGStateValidationProcess.java index 89c8c1e..e0f8003 100644 --- a/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ExtGStateValidationProcess.java +++ b/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/ExtGStateValidationProcess.java @@ -31,12 +31,6 @@ import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_GRAPHIC_UNEXP import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_TRANSPARENCY_EXT_GS_BLEND_MODE; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_TRANSPARENCY_EXT_GS_CA; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_TRANSPARENCY_EXT_GS_SOFT_MASK; -import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_BLEND_MODE; -import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_LOWER_CA; -import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_KEY_UPPER_CA; -import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_VALUE_BM_COMPATIBLE; -import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_VALUE_BM_NORMAL; -import static org.apache.pdfbox.preflight.PreflightConstants.TRANSPARENCY_DICTIONARY_VALUE_SOFT_MASK_NONE; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_COMMON; import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_NUMERIC_RANGE; import static org.apache.pdfbox.preflight.PreflightConstants.MAX_NEGATIVE_FLOAT; @@ -109,9 +103,8 @@ public class ExtGStateValidationProcess extends AbstractProcess if (extGStates != null) { - for (Object object : extGStates.keySet()) + for (COSName key : extGStates.keySet()) { - COSName key = (COSName) object; COSBase gsBase = extGStates.getItem(key); COSDictionary gsDict = COSUtils.getAsDictionary(gsBase, cosDocument); if (gsDict == null) @@ -227,9 +220,7 @@ public class ExtGStateValidationProcess extends AbstractProcess */ private void checkSoftMask(PreflightContext context, COSDictionary egs) { - COSBase smVal = egs.getItem(COSName.SMASK); - if (smVal != null && - !(smVal instanceof COSName && TRANSPARENCY_DICTIONARY_VALUE_SOFT_MASK_NONE.equals(((COSName) smVal).getName()))) + if (egs.containsKey(COSName.SMASK) && !COSName.NONE.equals(egs.getCOSName(COSName.SMASK))) { // ---- Soft Mask is valid only if it is a COSName equals to None context.addValidationError(new ValidationError(ERROR_TRANSPARENCY_EXT_GS_SOFT_MASK, @@ -245,16 +236,12 @@ public class ExtGStateValidationProcess extends AbstractProcess */ private void checkBlendMode(PreflightContext context, COSDictionary egs) { - COSBase bmVal = egs.getItem(TRANSPARENCY_DICTIONARY_KEY_BLEND_MODE); - if (bmVal != null) + COSName bmVal = egs.getCOSName(COSName.BM); + // ---- Blend Mode is valid only if it is equals to Normal or Compatible + if (bmVal != null && !(COSName.NORMAL.equals(bmVal) || COSName.COMPATIBLE.equals(bmVal))) { - // ---- Blend Mode is valid only if it is equals to Normal or Compatible - if (!(bmVal instanceof COSName && (TRANSPARENCY_DICTIONARY_VALUE_BM_NORMAL.equals(((COSName) bmVal) - .getName()) || TRANSPARENCY_DICTIONARY_VALUE_BM_COMPATIBLE.equals(((COSName) bmVal).getName())))) - { - context.addValidationError(new ValidationError(ERROR_TRANSPARENCY_EXT_GS_BLEND_MODE, - "BlendMode value isn't valid (only Normal and Compatible are authorized)")); - } + context.addValidationError(new ValidationError(ERROR_TRANSPARENCY_EXT_GS_BLEND_MODE, + "BlendMode value isn't valid (only Normal and Compatible are authorized)")); } } @@ -267,7 +254,7 @@ public class ExtGStateValidationProcess extends AbstractProcess */ private void checkUpperCA(PreflightContext context, COSDictionary egs) { - COSBase uCA = egs.getItem(TRANSPARENCY_DICTIONARY_KEY_UPPER_CA); + COSBase uCA = egs.getDictionaryObject(COSName.CA); if (uCA != null) { // ---- If CA is present only the value 1.0 is authorized @@ -291,7 +278,7 @@ public class ExtGStateValidationProcess extends AbstractProcess */ private void checkLowerCA(PreflightContext context, COSDictionary egs) { - COSBase lCA = egs.getItem(TRANSPARENCY_DICTIONARY_KEY_LOWER_CA); + COSBase lCA = egs.getDictionaryObject(COSName.CA_NS); if (lCA != null) { // ---- If ca is present only the value 1.0 is authorized @@ -314,7 +301,7 @@ public class ExtGStateValidationProcess extends AbstractProcess */ protected void checkTRKey(PreflightContext context, COSDictionary egs) { - if (egs.getItem(COSName.TR) != null) + if (egs.containsKey(COSName.TR)) { context.addValidationError(new ValidationError(ERROR_GRAPHIC_UNEXPECTED_KEY, "No TR key expected in Extended graphics state")); @@ -329,13 +316,14 @@ public class ExtGStateValidationProcess extends AbstractProcess */ protected void checkTR2Key(PreflightContext context, COSDictionary egs) { - if (egs.getItem("TR2") != null) + if (egs.containsKey(COSName.TR2)) { - String s = egs.getNameAsString("TR2"); + String s = egs.getNameAsString(COSName.TR2); if (!"Default".equals(s)) { - context.addValidationError(new ValidationError(ERROR_GRAPHIC_UNEXPECTED_VALUE_FOR_KEY, - "TR2 key only expect 'Default' value, not '" + s + "'")); + context.addValidationError( + new ValidationError(ERROR_GRAPHIC_UNEXPECTED_VALUE_FOR_KEY, + "TR2 key only expect 'Default' value, not '" + s + "'")); } } } diff --git a/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/SinglePageValidationProcess.java b/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/SinglePageValidationProcess.java index a24028d..d830910 100644 --- a/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/SinglePageValidationProcess.java +++ b/preflight/src/main/java/org/apache/pdfbox/preflight/process/reflect/SinglePageValidationProcess.java @@ -153,6 +153,12 @@ public class SinglePageValidationProcess extends AbstractProcess { thumbBase = ((COSObject) thumbBase).getObject(); } + if (!(thumbBase instanceof COSStream)) + { + context.addValidationError(new ValidationError(ERROR_GRAPHIC_INVALID, + "Thumb image must be a stream")); + return; + } PDXObject thumbImg = PDImageXObject.createThumbnail((COSStream)thumbBase); ContextHelper.validateElement(context, thumbImg, GRAPHIC_PROCESS); } diff --git a/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestGotoAction.java b/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestGotoAction.java index 8d64c91..18d63ab 100644 --- a/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestGotoAction.java +++ b/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestGotoAction.java @@ -21,7 +21,6 @@ package org.apache.pdfbox.preflight.action.pdfa1b; -import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo; @@ -39,7 +38,7 @@ public class TestGotoAction extends AbstractTestAction gotoAction.setDestination(new PDDestination() { @Override - public COSBase getCOSObject() + public COSName getCOSObject() { return COSName.getPDFName("ADest"); } @@ -55,7 +54,7 @@ public class TestGotoAction extends AbstractTestAction gotoAction.setDestination(new PDDestination() { @Override - public COSBase getCOSObject() + public COSDictionary getCOSObject() { return new COSDictionary(); } diff --git a/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestGotoRemoteAction.java b/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestGotoRemoteAction.java index 04ff17f..24f80d0 100644 --- a/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestGotoRemoteAction.java +++ b/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestGotoRemoteAction.java @@ -21,7 +21,6 @@ package org.apache.pdfbox.preflight.action.pdfa1b; -import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.common.filespecification.PDFileSpecification; @@ -39,7 +38,8 @@ public class TestGotoRemoteAction extends AbstractTestAction gotoAction.setD(COSName.getPDFName("ADest")); gotoAction.setFile(new PDFileSpecification() { - public COSBase getCOSObject() + @Override + public COSName getCOSObject() { return COSName.getPDFName("ADest"); } @@ -65,7 +65,8 @@ public class TestGotoRemoteAction extends AbstractTestAction gotoAction.setD(new COSDictionary()); gotoAction.setFile(new PDFileSpecification() { - public COSBase getCOSObject() + @Override + public COSName getCOSObject() { return COSName.getPDFName("ADest"); } @@ -90,7 +91,8 @@ public class TestGotoRemoteAction extends AbstractTestAction PDActionRemoteGoTo gotoAction = new PDActionRemoteGoTo(); gotoAction.setFile(new PDFileSpecification() { - public COSBase getCOSObject() + @Override + public COSName getCOSObject() { return COSName.getPDFName("ADest"); } diff --git a/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestSubmitAction.java b/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestSubmitAction.java index daf64cb..87bd657 100644 --- a/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestSubmitAction.java +++ b/preflight/src/test/java/org/apache/pdfbox/preflight/action/pdfa1b/TestSubmitAction.java @@ -21,7 +21,6 @@ package org.apache.pdfbox.preflight.action.pdfa1b; -import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.common.filespecification.PDFileSpecification; @@ -38,7 +37,8 @@ public class TestSubmitAction extends AbstractTestAction action.setItem(COSName.S, COSName.getPDFName("SubmitForm")); action.setItem(COSName.F, new PDFileSpecification() { - public COSBase getCOSObject() + @Override + public COSName getCOSObject() { return COSName.getPDFName("value"); } diff --git a/tools/pom.xml b/tools/pom.xml index 43e7de3..1729038 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -23,7 +23,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>../parent/pom.xml</relativePath> </parent> diff --git a/xmpbox/pom.xml b/xmpbox/pom.xml index eb650c5..82ef3a5 100644 --- a/xmpbox/pom.xml +++ b/xmpbox/pom.xml @@ -27,7 +27,7 @@ <parent> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-parent</artifactId> - <version>2.0.20</version> + <version>2.0.21</version> <relativePath>../parent/pom.xml</relativePath> </parent> diff --git a/xmpbox/src/main/java/org/apache/xmpbox/DateConverter.java b/xmpbox/src/main/java/org/apache/xmpbox/DateConverter.java index 0c94294..23b70ac 100644 --- a/xmpbox/src/main/java/org/apache/xmpbox/DateConverter.java +++ b/xmpbox/src/main/java/org/apache/xmpbox/DateConverter.java @@ -319,10 +319,9 @@ public final class DateConverter retval.append("+"); } timeZone = Math.abs(timeZone); - // milliseconds/1000 = seconds = seconds / 60 = minutes = minutes/60 = - // hours + // milliseconds/1000 = seconds; seconds / 60 = minutes; minutes/60 = hours int hours = timeZone / 1000 / 60 / 60; - int minutes = (timeZone - (hours * 1000 * 60 * 60)) / 1000 / 1000; + int minutes = (timeZone - (hours * 1000 * 60 * 60)) / 1000 / 60; if (hours < 10) { retval.append("0"); diff --git a/xmpbox/src/test/java/org/apache/xmpbox/DateConverterTest.java b/xmpbox/src/test/java/org/apache/xmpbox/DateConverterTest.java index fed87e4..62d8b43 100644 --- a/xmpbox/src/test/java/org/apache/xmpbox/DateConverterTest.java +++ b/xmpbox/src/test/java/org/apache/xmpbox/DateConverterTest.java @@ -79,6 +79,25 @@ public class DateConverterTest jaxbCal = javax.xml.bind.DatatypeConverter.parseDateTime("2015-02-02T16:37:19.192+01:00"); convDate = DateConverter.toCalendar("2015-02-02T16:37:19.192Europe/Berlin"); assertEquals(dateFormat.format(jaxbCal.getTime()), dateFormat.format(convDate.getTime())); + + // PDFBOX-4902: half-hour TZ + String time = "2015-02-02T16:37:19.192+05:30"; + jaxbCal = javax.xml.bind.DatatypeConverter.parseDateTime(time); + assertEquals(time, DateConverter.toISO8601(jaxbCal, true)); + convDate = DateConverter.toCalendar(time); + assertEquals(dateFormat.format(jaxbCal.getTime()), dateFormat.format(convDate.getTime())); + + time = "2015-02-02T16:37:19.192-05:30"; + jaxbCal = javax.xml.bind.DatatypeConverter.parseDateTime(time); + assertEquals(time, DateConverter.toISO8601(jaxbCal, true)); + convDate = DateConverter.toCalendar(time); + assertEquals(dateFormat.format(jaxbCal.getTime()), dateFormat.format(convDate.getTime())); + + time = "2015-02-02T16:37:19.192+10:30"; + jaxbCal = javax.xml.bind.DatatypeConverter.parseDateTime(time); + assertEquals(time, DateConverter.toISO8601(jaxbCal, true)); + convDate = DateConverter.toCalendar(time); + assertEquals(dateFormat.format(jaxbCal.getTime()), dateFormat.format(convDate.getTime())); } /** -- GitLab