From 3c6c52607eef0a61c51fe041168c8b3654e69a07 Mon Sep 17 00:00:00 2001 From: Tom Roush <4317255+TomRoush@users.noreply.github.com> Date: Mon, 16 May 2022 01:35:22 -0500 Subject: [PATCH] Upgrade PDFBox API to v2.0.21 (#404) --- .../pdfbox/pdmodel/font/PDFontTest.java | 53 ++++ ...> MultilineFieldsInstrumentationTest.java} | 13 +- .../form/PDAcroFormFlattenTest.java | 14 + .../tom_roush/fontbox/cmap/CMapParser.java | 20 +- .../fontbox/cmap/CodespaceRange.java | 15 +- .../com/tom_roush/fontbox/pfb/PfbParser.java | 10 + .../fontbox/ttf/BufferedRandomAccessFile.java | 41 ++- .../fontbox/ttf/GlyphSubstitutionTable.java | 8 +- .../fontbox/ttf/OS2WindowsMetricsTable.java | 2 +- .../tom_roush/fontbox/ttf/OpenTypeScript.java | 15 +- .../pdfbox/contentstream/PDFStreamEngine.java | 21 +- .../com/tom_roush/pdfbox/cos/COSFloat.java | 8 +- .../com/tom_roush/pdfbox/cos/COSInteger.java | 2 + .../com/tom_roush/pdfbox/cos/COSNumber.java | 46 ++- .../tom_roush/pdfbox/filter/Predictor.java | 49 --- .../tom_roush/pdfbox/multipdf/Overlay.java | 21 +- .../pdfbox/pdfparser/BaseParser.java | 88 ++---- .../tom_roush/pdfbox/pdfparser/COSParser.java | 10 +- .../pdfbox/pdfparser/PDFStreamParser.java | 109 +++---- .../pdfbox/pdmodel/common/COSArrayList.java | 2 +- .../pdfbox/pdmodel/common/PDNameTreeNode.java | 19 +- .../pdmodel/common/function/PDFunction.java | 2 +- .../common/function/PDFunctionType0.java | 5 +- .../encryption/StandardSecurityHandler.java | 18 +- .../pdfbox/pdmodel/font/FontMapperImpl.java | 15 +- .../tom_roush/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 +- .../graphics/image/PDImageXObject.java | 5 + .../PDOptionalContentProperties.java | 6 +- .../form/AppearanceGeneratorHelper.java | 4 +- .../pdmodel/interactive/form/PDAcroForm.java | 18 +- .../interactive/form/PDSignatureField.java | 9 +- .../pdmodel/interactive/form/PlainText.java | 36 ++- .../pdfbox/rendering/PageDrawer.java | 28 +- .../pdfbox/text/LegacyPDFStreamEngine.java | 123 +++++--- .../pdfbox/text/PDFTextStripper.java | 19 +- .../com/tom_roush/pdfbox/util/Matrix.java | 282 ++++++++---------- .../com/tom_roush/pdfbox/util/Version.java | 7 +- .../fontbox/cmap/TestCodespaceRange.java | 10 +- .../ttf/BufferedRandomAccessFileTest.java | 100 ++++++- .../tom_roush/pdfbox/cos/TestCOSFloat.java | 60 ++++ .../tom_roush/pdfbox/cos/TestCOSNumber.java | 28 +- .../tom_roush/pdfbox/filter/TestFilters.java | 6 +- .../pdmodel/common/COSArrayListTest.java | 4 +- .../pdmodel/common/TestPDNumberTreeNode.java | 3 +- .../interactive/form/MultilineFieldsTest.java | 135 +++++++++ .../pdfbox/text/TestTextStripper.java | 278 +++++++++++------ .../com/tom_roush/pdfbox/util/MatrixTest.java | 174 ++++++++++- .../tom_roush/pdfbox/util/TestDateUtil.java | 5 + .../tom_roush/pdfbox/util/TestHexUtil.java | 36 ++- .../{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 .../test/resources/pdfbox/input/eu-001.pdf | Bin 0 -> 68143 bytes .../pdfbox/input/eu-001.pdf-sorted.txt | 159 ++++++++++ .../pdfbox/input/eu-001.pdf-tabula.txt | 209 +++++++++++++ .../resources/pdfbox/input/eu-001.pdf.txt | 195 ++++++++++++ 64 files changed, 2072 insertions(+), 775 deletions(-) rename library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/{MultilineFieldsTest.java => MultilineFieldsInstrumentationTest.java} (91%) create mode 100644 library/src/test/java/com/tom_roush/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java rename library/src/test/resources/pdfbox/com/tom_roush/pdfbox/filter/{PDFBOX-1777.bin => PDFBOX-1977.bin} (100%) create mode 100644 library/src/test/resources/pdfbox/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFBOX-3835-input-acrobat-wrap.pdf create mode 100644 library/src/test/resources/pdfbox/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFBOX3812-acrobat-multiline-auto.pdf create mode 100644 library/src/test/resources/pdfbox/input/eu-001.pdf create mode 100644 library/src/test/resources/pdfbox/input/eu-001.pdf-sorted.txt create mode 100644 library/src/test/resources/pdfbox/input/eu-001.pdf-tabula.txt create mode 100644 library/src/test/resources/pdfbox/input/eu-001.pdf.txt diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/font/PDFontTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/font/PDFontTest.java index 66ccb5de..0b57f1c7 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/font/PDFontTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/font/PDFontTest.java @@ -18,6 +18,7 @@ package com.tom_roush.pdfbox.pdmodel.font; import android.content.Context; +import android.util.Log; import androidx.test.platform.app.InstrumentationRegistry; @@ -28,10 +29,15 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; import com.tom_roush.fontbox.ttf.TTFParser; +import com.tom_roush.fontbox.ttf.TrueTypeCollection; import com.tom_roush.fontbox.ttf.TrueTypeFont; +import com.tom_roush.fontbox.util.autodetect.FontFileFinder; import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; import com.tom_roush.pdfbox.android.TestResourceGenerator; import com.tom_roush.pdfbox.cos.COSName; @@ -205,6 +211,53 @@ public void testPDFox4318() throws IOException } } + @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); + Log.i("PdfBox-Android", "TrueType collection file: " + file); + ttc = new TrueTypeCollection(file); + break; + } + } + if (ttc == null) + { + Log.i("PdfBox-Android", "testFullEmbeddingTTC skipped, no .ttc files available"); + return; + } + + final List names = new ArrayList(); + ttc.processAllFonts(new TrueTypeCollection.TrueTypeFontProcessor() + { + @Override + public void process(TrueTypeFont ttf) throws IOException + { + Log.i("PdfBox-Android", "TrueType font in collection: " + ttf.getName()); + names.add(ttf.getName()); + } + }); + + TrueTypeFont ttf = ttc.getFontByName(names.get(0)); // take the first one + Log.i("PdfBox-Android", "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/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/MultilineFieldsInstrumentationTest.java similarity index 91% rename from library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java rename to library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/MultilineFieldsInstrumentationTest.java index 8b6f9b7c..10594d98 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/MultilineFieldsInstrumentationTest.java @@ -24,24 +24,23 @@ import java.io.File; import java.io.IOException; +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.rendering.TestRendering; -import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; import org.junit.After; import org.junit.Before; import org.junit.Test; -public class MultilineFieldsTest +public class MultilineFieldsInstrumentationTest { private static File OUT_DIR; private static final String IN_DIR = "pdfbox/com/tom_roush/pdfbox/pdmodel/interactive/form"; private static final String NAME_OF_PDF = "MultilineFields.pdf"; - private static final String TEST_VALUE = - "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, " + - "sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam"; + private static final String TEST_VALUE = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, " + + "sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam"; - Context testContext; + private Context testContext; private PDDocument document; private PDAcroForm acroForm; @@ -51,7 +50,6 @@ public void setUp() throws IOException { testContext = InstrumentationRegistry.getInstrumentation().getContext(); PDFBoxResourceLoader.init(testContext); - System.out.println("Working Directory = " + System.getProperty("user.dir")); document = PDDocument.load(testContext.getAssets().open(IN_DIR + "/" + NAME_OF_PDF)); acroForm = document.getDocumentCatalog().getAcroForm(); OUT_DIR = new File(testContext.getCacheDir(), "pdfbox-test-output"); @@ -110,4 +108,5 @@ public void tearDown() throws IOException { document.close(); } + } diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java index 3022256f..9b0117e0 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java @@ -284,6 +284,20 @@ public void testFlattenPDFBox4788() throws IOException 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/library/src/main/java/com/tom_roush/fontbox/cmap/CMapParser.java b/library/src/main/java/com/tom_roush/fontbox/cmap/CMapParser.java index f4b3fda5..ac182c48 100644 --- a/library/src/main/java/com/tom_roush/fontbox/cmap/CMapParser.java +++ b/library/src/main/java/com/tom_roush/fontbox/cmap/CMapParser.java @@ -16,6 +16,7 @@ */ package com.tom_roush.fontbox.cmap; +import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -122,27 +123,27 @@ public CMap parse(InputStream input) throws IOException 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); } @@ -452,12 +453,7 @@ protected InputStream getExternalCMap(String name) throws IOException return PDFBoxResourceLoader.getStream("com/tom_roush/fontbox/resources/cmap/" + name); } - InputStream is = getClass().getResourceAsStream("/com/tom_roush/fontbox/resources/cmap/" + name); - if (is == null) - { - throw new IOException("Error: Could not find referenced cmap stream " + name); - } - return is; + return new BufferedInputStream(getClass().getResourceAsStream("/com/tom_roush/fontbox/resources/cmap/" + name)); } private Object parseNextToken(PushbackInputStream is) throws IOException diff --git a/library/src/main/java/com/tom_roush/fontbox/cmap/CodespaceRange.java b/library/src/main/java/com/tom_roush/fontbox/cmap/CodespaceRange.java index 4e394fb5..65f47dc5 100644 --- a/library/src/main/java/com/tom_roush/fontbox/cmap/CodespaceRange.java +++ b/library/src/main/java/com/tom_roush/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/library/src/main/java/com/tom_roush/fontbox/pfb/PfbParser.java b/library/src/main/java/com/tom_roush/fontbox/pfb/PfbParser.java index be66df1d..6c99e94f 100644 --- a/library/src/main/java/com/tom_roush/fontbox/pfb/PfbParser.java +++ b/library/src/main/java/com/tom_roush/fontbox/pfb/PfbParser.java @@ -140,11 +140,21 @@ private void parsePfb(final byte[] pfb) throws IOException 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/library/src/main/java/com/tom_roush/fontbox/ttf/BufferedRandomAccessFile.java b/library/src/main/java/com/tom_roush/fontbox/ttf/BufferedRandomAccessFile.java index b9cab5c4..00ba94aa 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/BufferedRandomAccessFile.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/BufferedRandomAccessFile.java @@ -148,24 +148,37 @@ private void invalidate() throws IOException @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/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphSubstitutionTable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphSubstitutionTable.java index 012d0092..16675ceb 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphSubstitutionTable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphSubstitutionTable.java @@ -402,7 +402,7 @@ private Collection getLangSysTables(String scriptTag) * * @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 getFeatureRecords(Collection langSysTables, @@ -510,8 +510,8 @@ private int doLookup(LookupTable lookupTable, int gid) /** * 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 @@ -519,7 +519,7 @@ private int doLookup(LookupTable lookupTable, int gid) * * @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 enabledFeatures) { diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/OS2WindowsMetricsTable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/OS2WindowsMetricsTable.java index 87f7758d..a3cd2707 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/OS2WindowsMetricsTable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/OS2WindowsMetricsTable.java @@ -152,7 +152,7 @@ public class OS2WindowsMetricsTable extends TTFTable *

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/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeScript.java b/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeScript.java index d2bbef7b..feabbbeb 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeScript.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeScript.java @@ -18,6 +18,7 @@ import android.util.Log; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -222,21 +223,13 @@ public final class OpenTypeScript { if (PDFBoxResourceLoader.isReady()) { - input = PDFBoxResourceLoader.getStream(path); + input = new BufferedInputStream(PDFBoxResourceLoader.getStream(path)); } else { - // Fallback - input = OpenTypeScript.class.getResourceAsStream(path); - } - if (input != null) - { - parseScriptsFile(input); - } - else - { - Log.w("PdfBox-Android", "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/library/src/main/java/com/tom_roush/pdfbox/contentstream/PDFStreamEngine.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/PDFStreamEngine.java index 4015b9c9..a399e3de 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/PDFStreamEngine.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/PDFStreamEngine.java @@ -405,7 +405,12 @@ protected final void processTilingPattern(PDTilingPattern tilingPattern, PDColor // 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); @@ -735,13 +740,7 @@ protected void showText(byte[] string) throws IOException 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 @@ protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, 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 @@ protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, { // 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 @@ protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, * @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 @@ protected void showType3Glyph(Matrix textRenderingMatrix, PDType3Font font, int 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/library/src/main/java/com/tom_roush/pdfbox/cos/COSFloat.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSFloat.java index 68038897..94cfddb7 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSFloat.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSFloat.java @@ -107,13 +107,13 @@ private void checkMinMaxValues() // 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 float floatValue() * 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/library/src/main/java/com/tom_roush/pdfbox/cos/COSInteger.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSInteger.java index 2ed92c7a..76e4e8d2 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSInteger.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSInteger.java @@ -143,6 +143,8 @@ public float floatValue() * 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/library/src/main/java/com/tom_roush/pdfbox/cos/COSNumber.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSNumber.java index aca4fa48..101fba16 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSNumber.java +++ b/library/src/main/java/com/tom_roush/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(); @@ -82,7 +84,7 @@ public static COSNumber get( String number ) throws IOException 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 @@ else if (digit == '-' || digit == '.') throw new IOException("Not a number: " + number); } } - else if (number.indexOf('.') == -1 && (number.toLowerCase().indexOf('e') == -1)) + if (isFloat(number)) + { + return new COSFloat(number); + } + try { - 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); } + return null; } - else + } + + 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/library/src/main/java/com/tom_roush/pdfbox/filter/Predictor.java b/library/src/main/java/com/tom_roush/pdfbox/filter/Predictor.java index 31d8b278..aedc6261 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/Predictor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/Predictor.java @@ -17,13 +17,11 @@ import java.io.FilterOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; -import com.tom_roush.pdfbox.io.IOUtils; /** * Helper class to contain predictor decoding used by Flate and LZW filter. @@ -203,53 +201,6 @@ else if (absb <= absc) } } - 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) { final int bitsPerPixel = colors * bitsPerComponent; diff --git a/library/src/main/java/com/tom_roush/pdfbox/multipdf/Overlay.java b/library/src/main/java/com/tom_roush/pdfbox/multipdf/Overlay.java index 07965001..501179c5 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/multipdf/Overlay.java +++ b/library/src/main/java/com/tom_roush/pdfbox/multipdf/Overlay.java @@ -128,7 +128,8 @@ public PDDocument overlay(Map specificPageOverlayFile) throws I } /** - * 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 @@ -633,7 +634,8 @@ public void setDefaultOverlayFile(String defaultOverlayFile) } /** - * 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 */ @@ -663,7 +665,8 @@ public void setFirstPageOverlayFile(String firstPageOverlayFile) } /** - * 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 */ @@ -683,7 +686,8 @@ public void setLastPageOverlayFile(String lastPageOverlayFile) } /** - * 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 */ @@ -703,7 +707,8 @@ public void setAllPagesOverlayFile(String allPagesOverlayFile) } /** - * 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. @@ -724,7 +729,8 @@ public void setOddPageOverlayFile(String oddPageOverlayFile) } /** - * 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 */ @@ -744,7 +750,8 @@ public void setEvenPageOverlayFile(String evenPageOverlayFile) } /** - * 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/library/src/main/java/com/tom_roush/pdfbox/pdfparser/BaseParser.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/BaseParser.java index 9fefb8d6..4b29c23c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/BaseParser.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/BaseParser.java @@ -473,7 +473,6 @@ else if( ch == '\\' ) case '5': case '6': case '7': - { StringBuilder octal = new StringBuilder(); octal.append( next ); c = seqSource.read(); @@ -508,13 +507,10 @@ else if( ch == '\\' ) } out.write(character); break; - } default: - { // dropping the backslash // see 7.3.4.2 Literal Strings for further information out.write(next); - } } } else @@ -835,15 +831,11 @@ else if( c == 'f' ) */ 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 @@ -852,92 +844,57 @@ protected COSBase parseDirObject() throws IOException 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 { @@ -960,9 +917,26 @@ protected COSBase parseDirObject() throws IOException seqSource.unread(badString.getBytes(ISO_8859_1)); } } - } } - return retval; + 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; + } + if (ic != -1) + { + seqSource.unread(ic); + } + return COSNumber.get(buf.toString()); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/COSParser.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/COSParser.java index 676a5592..5ae70e61 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/COSParser.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/COSParser.java @@ -2744,9 +2744,13 @@ protected boolean parseXrefTable(long startByteOffset) throws IOException 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/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFStreamParser.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFStreamParser.java index 74a9e92e..c97597cd 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFStreamParser.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFStreamParser.java @@ -133,19 +133,15 @@ public List getTokens() */ 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(); @@ -157,74 +153,57 @@ public Object parseNextToken() throws IOException 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': @@ -238,7 +217,6 @@ else if( next.equals( "false" ) ) 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(); @@ -266,39 +244,35 @@ else if( next.equals( "false" ) ) 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.w("PdfBox-Android", "empty inline image at stream offset " + seqSource.getPosition()); + Log.w("PdfBox-Android", "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)) @@ -329,37 +303,26 @@ else if( next.equals( "false" ) ) 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayList.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayList.java index cc9953d6..8ad9f0d1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayList.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayList.java @@ -60,7 +60,7 @@ public COSArrayList() } /** - * Create the COSArrayList specifing the List and the backing COSArray. + * Create the COSArrayList specifying the List and the backing COSArray. * *

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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNameTreeNode.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNameTreeNode.java index 96f39417..752e1ed3 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNameTreeNode.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNameTreeNode.java @@ -112,7 +112,7 @@ public boolean isRootNode() public List> getKids() { List> retval = null; - COSArray kids = (COSArray)node.getDictionaryObject( COSName.KIDS ); + COSArray kids = node.getCOSArray(COSName.KIDS); if( kids != null ) { List> pdObjects = new ArrayList>(); @@ -255,13 +255,18 @@ public T getValue( String name ) throws IOException */ public Map getNames() throws IOException { - COSArray namesArray = (COSArray)node.getDictionaryObject( COSName.NAMES ); + COSArray namesArray = node.getCOSArray(COSName.NAMES); if( namesArray != null ) { Map names = new LinkedHashMap(); for( int i=0; i names ) 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 ); @@ -343,7 +348,7 @@ public String getUpperLimit() */ private void setUpperLimit( String upper ) { - COSArray arr = (COSArray)node.getDictionaryObject( COSName.LIMITS ); + COSArray arr = node.getCOSArray(COSName.LIMITS); if( arr == null ) { arr = new COSArray(); @@ -362,7 +367,7 @@ private void setUpperLimit( String upper ) 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 ); @@ -377,7 +382,7 @@ public String getLowerLimit() */ 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunction.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunction.java index 83698f97..ddabe022 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunction.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunction.java @@ -303,7 +303,7 @@ protected float[] clipToRange(float[] inputValues) { 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType0.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType0.java index 52576045..d2a3272d 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType0.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType0.java @@ -19,6 +19,7 @@ import android.util.Log; import java.io.IOException; +import java.io.InputStream; import com.tom_roush.harmony.javax.imageio.stream.ImageInputStream; import com.tom_roush.harmony.javax.imageio.stream.MemoryCacheImageInputStream; @@ -389,7 +390,8 @@ private int[][] getSamples() // 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++) @@ -400,6 +402,7 @@ private int[][] getSamples() index++; } mciis.close(); + inputStream.close(); } catch (IOException exception) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/StandardSecurityHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/StandardSecurityHandler.java index e8ce16fd..fa0c31c0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/StandardSecurityHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/StandardSecurityHandler.java @@ -1079,7 +1079,7 @@ private static byte[] computeHash2B(byte[] input, byte[] password, byte[] userKe { try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); + MessageDigest md = MessageDigests.getSHA256(); byte[] k = md.digest(input); byte[] e = null; @@ -1149,19 +1149,11 @@ private static byte[] computeHash2B(byte[] input, byte[] password, byte[] userKe } private static byte[] computeSHA256(byte[] input, byte[] password, byte[] userKey) - throws IOException { - 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) diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FontMapperImpl.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FontMapperImpl.java index 71ab6df0..33ad96ef 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FontMapperImpl.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FontMapperImpl.java @@ -18,6 +18,7 @@ import android.util.Log; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -115,21 +116,15 @@ final class FontMapperImpl implements FontMapper try { String ttfName = "com/tom_roush/pdfbox/resources/ttf/LiberationSans-Regular.ttf"; - InputStream ttfStream = null; + InputStream ttfStream; if (PDFBoxResourceLoader.isReady()) { ttfStream = PDFBoxResourceLoader.getStream(ttfName); } - - if (ttfStream == null) - { - // Fallback - ttfStream = FontMapper.class.getResourceAsStream("/" + ttfName); - } - - if (ttfStream == null) + else { - throw new IOException("Error loading resource: " + ttfName); + ttfStream = + new BufferedInputStream(FontMapper.class.getResourceAsStream("/" + ttfName)); } TTFParser ttfParser = new TTFParser(); lastResortFont = ttfParser.parse(ttfStream); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFont.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFont.java index 911f7cb3..c90db8b7 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFont.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFont.java @@ -391,7 +391,7 @@ public float getAverageFontWidth() { 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 @@ protected final List getWidths() { 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFontDescriptor.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFontDescriptor.java index 7d2b6d0b..74a126e1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFontDescriptor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFontDescriptor.java @@ -429,7 +429,7 @@ public void setFlags( int flags ) */ 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType0Font.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType0Font.java index f3fde873..dc1f5e97 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType0Font.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType0Font.java @@ -77,7 +77,7 @@ public static PDType0Font load(PDDocument doc, File file) throws IOException */ 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1Font.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1Font.java index cdfe6d18..4b15affb 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1Font.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1Font.java @@ -172,15 +172,7 @@ else if ("Symbol".equals(baseFont)) */ 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(); + this(doc, pfbIn, null); } /** @@ -194,7 +186,7 @@ public PDType1Font(PDDocument doc, InputStream pfbIn) throws IOException 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/Standard14Fonts.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/Standard14Fonts.java index a0a8026b..ec7a815e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/Standard14Fonts.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/Standard14Fonts.java @@ -17,11 +17,11 @@ package com.tom_roush.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; @@ -37,103 +37,99 @@ */ final class Standard14Fonts { - private static final Set STANDARD_14_NAMES = new HashSet(34); - private static final Map STANDARD_14_MAPPING = new HashMap(34); - private static final Map STANDARD14_AFM_MAP = new HashMap(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 ALIASES = new HashMap(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 FONTS = new HashMap(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"); + + // 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 Standard14Fonts() { } - private static void addAFM(String fontName) throws IOException - { - 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 = "com/tom_roush/pdfbox/resources/afm/" + afmName + ".afm"; + String resourceName = "com/tom_roush/pdfbox/resources/afm/" + fontName + ".afm"; InputStream afmStream; if (PDFBoxResourceLoader.isReady()) { - afmStream = PDFBoxResourceLoader.getStream(resourceName); + afmStream = new BufferedInputStream(PDFBoxResourceLoader.getStream(resourceName)); } else { - afmStream = PDType1Font.class.getResourceAsStream("/" + resourceName); - } - - if (afmStream == null) - { - throw new IOException(resourceName + " not found"); + 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 { @@ -142,37 +138,95 @@ private static void addAFM(String fontName, String afmName) throws IOException } /** - * 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 + */ + private static void mapName(String 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 */ - public static FontMetrics getAFM(String baseName) + private static void mapName(String alias, String baseName) { - return STANDARD14_AFM_MAP.get(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 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/TrueTypeEmbedder.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/TrueTypeEmbedder.java index b1db745b..82250d24 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/TrueTypeEmbedder.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/TrueTypeEmbedder.java @@ -93,7 +93,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); } @@ -140,15 +159,13 @@ private boolean isEmbeddingPermitted(TrueTypeFont ttf) throws IOException if (ttf.getOS2Windows() != null) { int fsType = ttf.getOS2Windows().getFsType(); - int exclusive = fsType & 0x8; // bits 0-3 are a set of exclusive bits - - if ((exclusive & OS2WindowsMetricsTable.FSTYPE_RESTRICTED) == + 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 @@ -184,7 +201,15 @@ private PDFontDescriptor createFontDescriptor(TrueTypeFont ttf) throws IOExcepti 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java index 53fb23c1..57862ef7 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java @@ -180,6 +180,11 @@ public COSBase getCOSObject() @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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/MacRomanEncoding.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/MacRomanEncoding.java index 0f2f7eab..faf7a772 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/MacRomanEncoding.java +++ b/library/src/main/java/com/tom_roush/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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/WinAnsiEncoding.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/WinAnsiEncoding.java index 31b93645..da6c888c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/WinAnsiEncoding.java +++ b/library/src/main/java/com/tom_roush/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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java index 130c30df..3655ff3d 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java @@ -528,6 +528,11 @@ private float[] extractMatte(PDImageXObject softMask) throws IOException // 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.e("PdfBox-Android", "Image /Matte entry not long enough for colorspace, skipped"); + return null; + } matte = getColorSpace().toRGB(matte); } return matte; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java index 03513e30..5fc0b6e2 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java @@ -224,7 +224,11 @@ public void setBaseState(BaseState state) */ 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java index 2259b31b..30cdf6fb 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java @@ -797,7 +797,7 @@ private float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOE { 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; @@ -817,7 +817,7 @@ private float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOE } 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroForm.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroForm.java index 710cba6d..07f93e93 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroForm.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroForm.java @@ -316,9 +316,7 @@ public void flatten(List fields, boolean refreshAppearances) throws IOE { 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; @@ -392,7 +390,21 @@ else if (!annotation.isInvisible() && !annotation.isHidden() && // 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; } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDSignatureField.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDSignatureField.java index 53f00fbc..45e8dac1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDSignatureField.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDSignatureField.java @@ -73,16 +73,13 @@ public PDSignatureField(PDAcroForm acroForm) throws IOException private String generatePartialName() { String fieldName = "Signature"; - Set sigNames = new HashSet(); + Set nameSet = new HashSet(); 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/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PlainText.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PlainText.java index 8659aa2b..372cfa12 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PlainText.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PlainText.java @@ -173,6 +173,9 @@ List getLines(PDFont font, float fontSize, float width) throws IOException String word = textContent.substring(start,end); float wordWidth = font.getStringWidth(word) * scale; + boolean wordNeedsSplit = false; + int splitOffset = end - start; + lineWidth = lineWidth + wordWidth; // check if the last word would fit without the whitespace ending it @@ -182,7 +185,7 @@ List getLines(PDFont font, float fontSize, float width) throws IOException 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 @@ List getLines(PDFont font, float fontSize, float width) throws IOException 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/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java b/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java index 1a93a93a..ca04596e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java +++ b/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java @@ -772,11 +772,12 @@ public void drawImage(PDImage pdImage) throws IOException 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, @@ -850,18 +851,16 @@ private void drawBitmap(Bitmap image, AffineTransform at) throws IOException // 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())); -// awtPaint = applySoftMaskToPaint(awtPaint, softMask); -// graphics.setPaint(awtPaint); - RectF unitRect = new RectF(0, 0, 1, 1); -// graphics.fill(at.createTransformedShape(unitRect)); + RectF rectangle = new RectF(0, 0, width, height); +// Paint awtPaint; TODO: PdfBox-Android } else { @@ -871,11 +870,6 @@ private void drawBitmap(Bitmap image, AffineTransform at) throws IOException image = applyTransferFunction(image, transfer); } - int width = image.getWidth(); - int height = image.getHeight(); - imageTransform.scale(1.0 / width, -1.0 / height); - imageTransform.translate(0, -height); - canvas.drawBitmap(image, imageTransform.toMatrix(), paint); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/text/LegacyPDFStreamEngine.java b/library/src/main/java/com/tom_roush/pdfbox/text/LegacyPDFStreamEngine.java index 13f8eec8..4c351546 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/text/LegacyPDFStreamEngine.java +++ b/library/src/main/java/com/tom_roush/pdfbox/text/LegacyPDFStreamEngine.java @@ -20,6 +20,8 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Map; +import java.util.WeakHashMap; import com.tom_roush.fontbox.ttf.TrueTypeFont; import com.tom_roush.fontbox.util.BoundingBox; @@ -47,6 +49,7 @@ import com.tom_roush.pdfbox.contentstream.operator.text.ShowTextAdjusted; import com.tom_roush.pdfbox.contentstream.operator.text.ShowTextLine; import com.tom_roush.pdfbox.contentstream.operator.text.ShowTextLineAndSpace; +import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.pdmodel.PDPage; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.font.PDCIDFont; @@ -78,6 +81,7 @@ class LegacyPDFStreamEngine extends PDFStreamEngine private PDRectangle pageSize; private Matrix translateMatrix; private final GlyphList glyphList; + private final Map fontHeightMap = new WeakHashMap(); /** * Constructor. @@ -150,7 +154,9 @@ public void processPage(PDPage page) throws IOException * 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 { // @@ -166,48 +172,6 @@ protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vect 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 @@ -257,7 +221,13 @@ else if (font instanceof PDType0Font) // (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 @@ -301,17 +271,17 @@ else if (font instanceof PDType0Font) 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 { @@ -337,10 +307,66 @@ else if (font instanceof PDType0Font) 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. @@ -351,5 +377,4 @@ protected void processTextPosition(TextPosition text) { // subclasses can override to provide specific functionality } - } diff --git a/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripper.java b/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripper.java index 0dfb44e2..4efb7269 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripper.java +++ b/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripper.java @@ -18,6 +18,7 @@ import android.util.Log; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -216,6 +217,11 @@ public PDFTextStripper() throws IOException * This will return the text of a document. See writeText.
* NOTE: The document must not be encrypted when coming into this method. * + *

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. * @throws IOException if the doc state is invalid or it is encrypted. @@ -1846,21 +1852,14 @@ private String handleDirection(String word) { if (PDFBoxResourceLoader.isReady()) { - input = PDFBoxResourceLoader.getStream(path); + input = new BufferedInputStream(PDFBoxResourceLoader.getStream(path)); } else { - input = PDFTextStripper.class.getResourceAsStream("/" + path); + input = new BufferedInputStream(PDFTextStripper.class.getResourceAsStream("/" + path)); } - if (input != null) - { - parseBidiFile(input); - } - else - { - Log.w("PdfBox-Android", "Could not find '" + path + "', mirroring char map will be empty: "); - } + parseBidiFile(input); } catch (IOException e) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/util/Matrix.java b/library/src/main/java/com/tom_roush/pdfbox/util/Matrix.java index c9ed3536..bdd69cfa 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/util/Matrix.java +++ b/library/src/main/java/com/tom_roush/pdfbox/util/Matrix.java @@ -33,32 +33,42 @@ */ 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(); @@ -74,6 +84,11 @@ public Matrix(COSArray array) * 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) @@ -87,7 +102,7 @@ public Matrix(COSArray array) */ 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; @@ -99,18 +114,23 @@ public Matrix(float a, float b, float c, float d, float e, float f) /** * 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; } /** @@ -152,7 +172,10 @@ public static Matrix createMatrix(COSBase base) @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; } /** @@ -269,8 +292,7 @@ public void concatenate(Matrix matrix) */ public void translate(Vector vector) { - Matrix m = Matrix.getTranslateInstance(vector.getX(), vector.getY()); - concatenate(m); + concatenate(Matrix.getTranslateInstance(vector.getX(), vector.getY())); } /** @@ -281,8 +303,7 @@ public void translate(Vector vector) */ public void translate(float tx, float ty) { - Matrix m = Matrix.getTranslateInstance(tx, ty); - concatenate(m); + concatenate(Matrix.getTranslateInstance(tx, ty)); } /** @@ -293,8 +314,7 @@ public void translate(float tx, float ty) */ public void scale(float sx, float sy) { - Matrix m = Matrix.getScaleInstance(sx, sy); - concatenate(m); + concatenate(Matrix.getScaleInstance(sx, sy)); } /** @@ -304,113 +324,81 @@ public void scale(float sx, float sy) */ 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. - *

- * 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; 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. * - * @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. */ + @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. * @@ -482,16 +470,18 @@ public Matrix extractScaling() /** * 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); } /** @@ -512,30 +502,34 @@ public Matrix extractTranslating() /** * 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); } /** @@ -551,14 +545,7 @@ public static Matrix getRotateInstance(double theta, float tx, float ty) 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); } /** @@ -569,9 +556,7 @@ public static Matrix getRotateInstance(double theta, float tx, float ty) */ public static Matrix concatenate(Matrix a, Matrix b) { - Matrix copy = a.clone(); - copy.concatenate(b); - return copy; + return b.multiply(a); } /** @@ -581,9 +566,7 @@ public static Matrix concatenate(Matrix a, Matrix b) @Override public Matrix clone() { - Matrix clone = new Matrix(); - System.arraycopy( single, 0, clone.single, 0, 9 ); - return clone; + return new Matrix(single.clone()); } /** @@ -593,8 +576,6 @@ public Matrix clone() */ public float getScalingFactorX() { - float xScale = single[0]; - /** * BM: if the trm is rotated, the calculation is a little more complicated * @@ -612,12 +593,12 @@ public float getScalingFactorX() * 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]; } /** @@ -627,13 +608,12 @@ public float getScalingFactorX() */ 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/library/src/main/java/com/tom_roush/pdfbox/util/Version.java b/library/src/main/java/com/tom_roush/pdfbox/util/Version.java index b670795a..cd42a5c0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/util/Version.java +++ b/library/src/main/java/com/tom_roush/pdfbox/util/Version.java @@ -17,6 +17,7 @@ package com.tom_roush.pdfbox.util; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Properties; @@ -44,11 +45,7 @@ public static String getVersion() 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/library/src/test/java/com/tom_roush/fontbox/cmap/TestCodespaceRange.java b/library/src/test/java/com/tom_roush/fontbox/cmap/TestCodespaceRange.java index 08e4dad9..f9cb3012 100644 --- a/library/src/test/java/com/tom_roush/fontbox/cmap/TestCodespaceRange.java +++ b/library/src/test/java/com/tom_roush/fontbox/cmap/TestCodespaceRange.java @@ -46,11 +46,17 @@ public void testCodeLength() */ public void testConstructor() { + // PDFBOX-4923 "1 begincodespacerange <00> 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/library/src/test/java/com/tom_roush/fontbox/ttf/BufferedRandomAccessFileTest.java b/library/src/test/java/com/tom_roush/fontbox/ttf/BufferedRandomAccessFileTest.java index c7864696..e34a13eb 100644 --- a/library/src/test/java/com/tom_roush/fontbox/ttf/BufferedRandomAccessFileTest.java +++ b/library/src/test/java/com/tom_roush/fontbox/ttf/BufferedRandomAccessFileTest.java @@ -21,12 +21,12 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; - import org.junit.Assert; import org.junit.Test; /** * @author Cameron Rollhieser + * @author Tilman Hausherr */ public class BufferedRandomAccessFileTest { @@ -48,14 +48,108 @@ public void ensureReadFinishes() throws IOException 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/library/src/test/java/com/tom_roush/pdfbox/cos/TestCOSFloat.java b/library/src/test/java/com/tom_roush/pdfbox/cos/TestCOSFloat.java index cb4555ea..c1e9c779 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/cos/TestCOSFloat.java +++ b/library/src/test/java/com/tom_roush/pdfbox/cos/TestCOSFloat.java @@ -204,6 +204,66 @@ void runTest(float num) } + 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/library/src/test/java/com/tom_roush/pdfbox/cos/TestCOSNumber.java b/library/src/test/java/com/tom_roush/pdfbox/cos/TestCOSNumber.java index 9dcecbb4..19d6616b 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/cos/TestCOSNumber.java +++ b/library/src/test/java/com/tom_roush/pdfbox/cos/TestCOSNumber.java @@ -78,11 +78,37 @@ public void testGet() { // 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/library/src/test/java/com/tom_roush/pdfbox/filter/TestFilters.java b/library/src/test/java/com/tom_roush/pdfbox/filter/TestFilters.java index 73d5f2ad..7a1863cb 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/filter/TestFilters.java +++ b/library/src/test/java/com/tom_roush/pdfbox/filter/TestFilters.java @@ -137,17 +137,17 @@ public void testPDFBOX4517() throws IOException } /** - * 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 */ @Test - 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/com/tom_roush/pdfbox/filter/PDFBOX-1777.bin")); + byte[] byteArray = IOUtils.toByteArray(this.getClass().getResourceAsStream("/pdfbox/com/tom_roush/pdfbox/filter/PDFBOX-1977.bin")); checkEncodeDecode(lzwFilter, byteArray); } diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayListTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayListTest.java index e387eb8e..404b9250 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayListTest.java +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayListTest.java @@ -47,7 +47,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 tbcAnnotationsList; @@ -64,7 +64,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/library/src/test/java/com/tom_roush/pdfbox/pdmodel/common/TestPDNumberTreeNode.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/common/TestPDNumberTreeNode.java index 96a7471d..8973c1dd 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/common/TestPDNumberTreeNode.java +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/common/TestPDNumberTreeNode.java @@ -22,7 +22,6 @@ import java.util.Map; import java.util.TreeMap; -import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSInteger; import org.junit.Assert; @@ -59,7 +58,7 @@ public PDTest(COSInteger cosInt) } @Override - public COSBase getCOSObject() + public COSInteger getCOSObject() { return COSInteger.get( value ); } diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java new file mode 100644 index 00000000..7aa22ce2 --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/interactive/form/MultilineFieldsTest.java @@ -0,0 +1,135 @@ +/* + * 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 com.tom_roush.pdfbox.pdmodel.interactive.form; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSNumber; +import com.tom_roush.pdfbox.cos.COSString; +import com.tom_roush.pdfbox.pdfparser.PDFStreamParser; +import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class MultilineFieldsTest +{ + private static final File IN_DIR = new File("src/test/resources/pdfbox/com/tom_roush/pdfbox/pdmodel/interactive/form"); + + // 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 acrobatLines = getTextLinesFromAppearanceStream(fieldInput); + fieldInput.setValue(fieldValue); + List 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 getTextLinesFromAppearanceStream(PDField field) throws IOException + { + PDAnnotationWidget widget = field.getWidgets().get(0); + PDFStreamParser parser = new PDFStreamParser(widget.getNormalAppearanceStream()); + + Object token = parser.parseNextToken(); + + List lines = new ArrayList(); + + while (token != null) + { + if (token instanceof COSString) + { + lines.add(((COSString) token).getString()); + } + token = parser.parseNextToken(); + } + return lines; + } + +} diff --git a/library/src/test/java/com/tom_roush/pdfbox/text/TestTextStripper.java b/library/src/test/java/com/tom_roush/pdfbox/text/TestTextStripper.java index fedcf194..7d881367 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/text/TestTextStripper.java +++ b/library/src/test/java/com/tom_roush/pdfbox/text/TestTextStripper.java @@ -34,8 +34,12 @@ import java.util.LinkedList; import java.util.List; +import com.tom_roush.fontbox.util.BoundingBox; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.TestPDPageTree; +import com.tom_roush.pdfbox.pdmodel.font.PDFont; +import com.tom_roush.pdfbox.pdmodel.font.PDFontDescriptor; +import com.tom_roush.pdfbox.pdmodel.font.PDType3Font; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; @@ -120,7 +124,7 @@ public void setUp() } catch (IOException e) { - System.out.println(e.getMessage()); + e.printStackTrace(); } // If you want to test a single file using DEBUG logging, from an IDE, // you can do something like this: @@ -242,7 +246,6 @@ public void doTestFile(File inFile, File outDir, boolean bLogResult, boolean bSo } } - //System.out.println(" " + inFile + (bSort ? " (sorted)" : "")); PDDocument document = PDDocument.load(inFile); try { @@ -305,116 +308,117 @@ public void doTestFile(File inFile, File outDir, boolean bLogResult, boolean bSo return; } - boolean localFail = false; + compareResult(expectedFile, outFile, inFile, bSort, diffFile); + } + finally + { + document.close(); + } + } - LineNumberReader expectedReader = - new LineNumberReader(new InputStreamReader(new FileInputStream(expectedFile), ENCODING)); - LineNumberReader actualReader = - new LineNumberReader(new InputStreamReader(new FileInputStream(outFile), ENCODING)); + private void compareResult(File expectedFile, File outFile, File inFile, boolean bSort, File diffFile) + throws IOException + { + boolean localFail = false; - while (true) - { - 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 ) - { - actualLine = actualReader.readLine(); - } - if (!stringsEqual(expectedLine, actualLine)) - { - this.bFail = true; - localFail = true; - System.out.println("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; - } + LineNumberReader expectedReader = + new LineNumberReader(new InputStreamReader(new FileInputStream(expectedFile), ENCODING)); + LineNumberReader actualReader = + new LineNumberReader(new InputStreamReader(new FileInputStream(outFile), ENCODING)); - if( expectedLine == null || actualLine==null) - { - break; - } + while (true) + { + String expectedLine = expectedReader.readLine(); + while( expectedLine != null && expectedLine.trim().length() == 0 ) + { + expectedLine = expectedReader.readLine(); } - expectedReader.close(); - actualReader.close(); - if (!localFail) + String actualLine = actualReader.readLine(); + while( actualLine != null && actualLine.trim().length() == 0 ) { - outFile.delete(); + actualLine = actualReader.readLine(); } - else + if (!stringsEqual(expectedLine, actualLine)) + { + this.bFail = true; + localFail = true; + System.out.println("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) { - // https://code.google.com/p/java-diff-utils/wiki/SampleUsage - List original = fileToLines(expectedFile); - List revised = fileToLines(outFile); + break; + } + } + expectedReader.close(); + actualReader.close(); + if (!localFail) + { + outFile.delete(); + } + else + { + // https://code.google.com/p/java-diff-utils/wiki/SampleUsage + List original = fileToLines(expectedFile); + List revised = fileToLines(outFile); - // Compute diff. Get the Patch object. Patch is the container for computed deltas. - Patch patch = DiffUtils.diff(original, revised); + // 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()) + PrintStream diffPS = new PrintStream(diffFile, ENCODING); + for (Object delta : patch.getDeltas()) + { + if (delta instanceof ChangeDelta) { - 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); - } + 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.close(); } - } - finally - { - document.close(); + diffPS.close(); } } // Helper method for get the file content - private static List fileToLines(File file) + private static List fileToLines(File file) throws IOException { List lines = new LinkedList(); - 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; } @@ -440,6 +444,7 @@ private int findOutlineItemDestPageNum(PDDocument doc, PDOutlineItem oi) throws * must be empty. * * @throws IOException + * @throws URISyntaxException */ @Test public void testStripByOutlineItems() throws IOException, URISyntaxException @@ -604,4 +609,93 @@ public void testExtract() throws Exception fail("One or more failures, see test log for details"); } } + + @Test + public void testTabula() throws IOException + { + File pdfFile = new File("src/test/resources/pdfbox/input", "eu-001.pdf"); + File outFile = new File("target/test-output", "eu-001.pdf-tabula.txt"); + File expectedOutFile = new File("src/test/resources/pdfbox/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; + } + } } diff --git a/library/src/test/java/com/tom_roush/pdfbox/util/MatrixTest.java b/library/src/test/java/com/tom_roush/pdfbox/util/MatrixTest.java index aae3cdab..8d113a5a 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/util/MatrixTest.java +++ b/library/src/test/java/com/tom_roush/pdfbox/util/MatrixTest.java @@ -17,6 +17,7 @@ import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSFloat; +import com.tom_roush.pdfbox.cos.COSName; import org.junit.Test; import static org.junit.Assert.*; @@ -40,7 +41,105 @@ public void testConstructionAndCopy() throws Exception } @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 void testPdfbox2872() } + @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 @@ private void assertMatrixValuesEqualTo(float[] values, Matrix m) } } + //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/library/src/test/java/com/tom_roush/pdfbox/util/TestDateUtil.java b/library/src/test/java/com/tom_roush/pdfbox/util/TestDateUtil.java index 2ba26f72..e51ef49a 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/util/TestDateUtil.java +++ b/library/src/test/java/com/tom_roush/pdfbox/util/TestDateUtil.java @@ -167,6 +167,10 @@ public void testDateConverter() throws Exception // 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 void testDateConverter() throws Exception 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/library/src/test/java/com/tom_roush/pdfbox/util/TestHexUtil.java b/library/src/test/java/com/tom_roush/pdfbox/util/TestHexUtil.java index d4300b90..3e44b4f6 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/util/TestHexUtil.java +++ b/library/src/test/java/com/tom_roush/pdfbox/util/TestHexUtil.java @@ -15,9 +15,12 @@ */ package com.tom_roush.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 void testGetCharsUTF16BE() 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/library/src/test/resources/pdfbox/com/tom_roush/pdfbox/filter/PDFBOX-1777.bin b/library/src/test/resources/pdfbox/com/tom_roush/pdfbox/filter/PDFBOX-1977.bin similarity index 100% rename from library/src/test/resources/pdfbox/com/tom_roush/pdfbox/filter/PDFBOX-1777.bin rename to library/src/test/resources/pdfbox/com/tom_roush/pdfbox/filter/PDFBOX-1977.bin diff --git a/library/src/test/resources/pdfbox/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFBOX-3835-input-acrobat-wrap.pdf b/library/src/test/resources/pdfbox/com/tom_roush/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/library/src/test/resources/pdfbox/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFBOX3812-acrobat-multiline-auto.pdf b/library/src/test/resources/pdfbox/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFBOX3812-acrobat-multiline-auto.pdf new file mode 100644 index 0000000000000000000000000000000000000000..785a43934d0334c73d05f45ae50e4dd9624b8dd5 GIT binary patch literal 29737 zcmeHQ30M=?+6GayDJbFwT6M&V@JllLmI9IlazTkeK&hovvJ4P`1d||WYZWTu(pIla z-79MA)=gWNwrZ8S)g48ptzNa-+p5>Ct#zx`|4c|igrG^)s(s?~h~wns%y-`N%{epg z`<_Fux8jgcd zl%U}O;B*>9Aq)-2Lm3c3P!LKXnbFbyoLxh3@VX= ze+elrM-fs^L`UMRG!~qeA~H0NmJ?EzqU2J!ltSYel$7FlbS9jj*K#Eppy4ckoNa(m z0>r0;6FGg3ITs>GKA7QrI~iDMFqlCjEuW_XWFU-y(?%3>@QX?=8&1zaAq+wR0w%MO zQx*6Na*wOk+JLMqb=I#wfqtuE%2suXzjauZrPj`B8K}t`8*nGM_Lfg8#A}ec1wbF* zX8ooWL6cc!G?y5;Y<~gbFX+`PE=le$u%YTN!0>t`ok0Yl2w`gnjeyRhNH`V38F&!H zpm#-?m6g>jMCS`-MQ$D=2r8XbUb@(O@SD#}3=SF@%mlq7nK3L^I^#rHya|(CPo%4e zp5L?@HMO#sUaKEbu8fvr4Nnt02A&N;s*= zoDcHN1kslNCaZG5Fs zQYZ#~(k%Fy2`fNisha#0WFn^rU0iozC1)}e88sZpO^}32gBheBgw*voAE84qOnop; zdP1%bgzM>Tb*L z%niRZaQ?#bz>j3tVqYztT03xO(8XefET!eip=WbP&3rp=_6O?BQtkiE{LnY0!xw2} zSi-u1-nTX^sJV3bbo!Z=3#vc5dJQc{V;3x(J3;Yg>cN{yW4ygqZ%>%(EIC|3%$~j| zxYO=|7?zGCuS`Z(rFltvw&>Dj)X%pLZU}548?|oFqEmsD(@UjOzDf$27pxpSWyPf- zFHM~`d*(~|IV}&JY;{O5^_lpHel5-s6=Az0b|xGy+?B9r_n=nsiAjf>_bHk{jg9|T ztlV2?s(fq2`z*WaX4TD$H+tL{KHSUO*K0+`-V!G*!(3?@W6K2!C-2p=*QW2)A4U9j z4B3{1Y)?3_B&IEt5f!ny+AHtX+={Nb6V{gR8nj|^{&zK#JGQG`nt6W8tvm4()h~-C z#-+ToZSRV(nQ>pd)S+BH37jc5?f-q|1E&yE^#$lr>?mZ}z!5eaq!p=Ssu@ zk-se7lN9vb-t2eJbZE0T>NL3>I^XAb?H%rtqGrlcNy_yvHk5r|^Y562#Xe!D2cGNF ze{{_H@%`G)Jae#mQPRe3?W@-4E9uKG2QFGQXX0VuhTfgRSMF90d)5D=9X)>;aU{8S zUU=E|wY^5ae)jujgY#NfHLrX{b7suATSGrQy|ua|?LgrR1sJt-&7rMrcF$cqsd{dY z7R;2%^ZR?Z6c!ZToKt}Z2X73LX8&(XU(-)lf+u$)e?N1g5X0*5-+a+BTnXYwstEHoOuuW5en0>6KHve{yo`c5+DRtBV)P_aEuJ zWb3Z30}jsn?aLm~D}zfuru(n__`m;I^~R*j6S~joeEHCv%@sZTqgEUXSo3bH)>Mnx z18-fBo+=IgxySJ5=0>E22VPkoR@>*B=%`ml?OPJxC9m!EI{x|xa?00ggZ+g2m z?e-2J*3ZtSbt}HDdSm+0Nw2}b-cJ0g@3WyRlBhXvL{+^ax;D5vN3tO2%x_ya#9v-+ zdN+OcYx`e#>8Mv&M8BY%h<=0TDYLry{CeUy%r|gW^CiKF?YLM?tR}lNt{cAtiD!Eo zdmHI#>p#m{w3?lBpoc8JS*Ul7Ut+sqQ?vHSn9O6Few=2$akYTD(>%y4bk~K2P7>Q; zy0OlVI7!oRYLVJ(nFJMybt9T>Y_A{FBKwvUsx*0=8B%jOI(<~grS+RaAe}ZUB%M?s zib5HetBW6LXGii*vLbJ}GhxmF`YD1AZDkQ`{f%Dh4wyU7fej&X{WND*FV<>=2RE01q zLSTE%eB}TWvBIF$Wsisj6H+vWV2BW*geaYcAz}m-V@Mwa6C+3@+@NIrl`VD1z(Wdn zo@!7B%8E%=jM9#EIFxKqhpnp{*5Wi0>y&RP+n^rH6%58SgCRe)GU~TEhzt9I-jlByBi;Ve}p4Mt$E}ts^j|3*r4d~t7pw)=8!2^xT z9IYxW%-3mDd{4uL+U!WU{-cK-CFoIw!XBv8Tq!8HJ5I}2#IXiV5#Nx6*yy4nomR}L zaYRM1S|LMVjF3=iHA1ykOAARAqSbJmnkK2b1X3s*zk7GLfzJ~3;2~A7;cWF%1WIbO zOtuhb*lZzzsc0dC61b4SX&l#*l!l@SOK0Bw-G_}6=mzH~fe%1e;&(#?PKuL_25pgs zGd7WacNKZq(W6s%Q@hqXudr@kSN?i!HiB5r}}fy)DzTL`#S z*ppozxZFa(t-_w{dT4M7>fiDJe;E~04Bp|KyhZZ#s+G5Ek}4x@>iU@WH~w zok{%$7AA^zCH0F)L5OM`qYoaswl_&q)B1V;xBO0i?=$h&)PB=ahn-pX8BY|KF9?lr z(h8oEwqp83Q0k3nbdqdLo}=V_$#bt}q4IZz`iA;$&xk3z5jy3h$!FLfW14S4E6?}u zA26x3tsMDN_5)V>`I~+`A=hnn%TOoiIMC=QO4*5Y51sK8&>5tdKpm+F2jZiC=>A*i zj7E`o=nOl-WqbFt5NHl$j0?oqlTBx6wQ5$yVXTm(84QpZ6eUzKDpsfg+zEr>8V<;! z9y-JR2vJ98S{VlC1dNByu%iVI>c=q0vk&>$a}l=y^}ywU%Pj=lD(uNF4_s~`;8tNz zc0Dw>T%k~Ai_>H9Az==5h9ntS>VRj);$;HW`XqeT?LLL^3r7=(ss zps!OnmT7kZr0cXfoEc6FOV#Of@;Tns6<+tAFX3HU@SreBc)f87b1+WX0_OcQO5XM7 za&ROiqH&UefXFB!faZ@eASfCH$>JgcBb@}ND9ShM_}C;xW`jwi%D-oj4vs}zkQS?% zTS3|ov;}EHu<0P{5p*R=OV;ruPzGi}5V9nL5`e+u31NIAl$kbjCeAm&N*PHV{cspY zi9jAeC;%0~h)kGw?eP{o-UV9ciDYqDp)wnFz&2^M*zqS@X+e)P1a5{-*UkXf!Jk^}GU0m#{%<}i0PNyP)> ztaT8=Fc!>iSluuIMu`R<+QJhkc<8O!8ZmC`OQ?q%O$_8RLMqFnmTxb!yYKyAESpFIMW6CZQTcPgQCoghz`~}T#>nWj4ren?j_gx?Bl=yz4Bg=8V$k{FgOrtfri&zf zq~ZIno7Zo>R6H53L2iy0vB!U6_NHDA|8mAM*^433y^DtKne64`6?$sGO=lQ72mwuE zXct%hUO7Xd@cW^moH1^HDDJEo4&LrZt!H2qPGhkW9FLX7Q8+;`gcOxAa+b!JSdx}7 z_9X&fO61oDfFCqJ@qh^r%s%`Na1Kn9PSy_4Ox@Z6AUZTwKY$JYeoF_YU*ElX>vie; z(zBbsjGR*Ro3~FpA0MBdHGRjezcV}NFznS!-+9coF`ssRE2iUVWk7N8_-dcP#LmDn zx3E?3mt(dL^Bn_HWxBBSB_|j=iXs@WM|xzDr^V$79GWO9#_`85i#UzMBa8g=Wf77l zv%w-3Es#ZULP)R(DP%OPMyTaw6156NvJs6(7O_(i9$CZ=g=+^M8p}1Dtff3~dEjyj z0k;Z!vdaUPTL`#S*pppP7hDe@i&%(g_{oq(2pfU#mMj8GW%gy|fb-@#>qjP&)U*GP z5R&UkIPJZ46T(KVH>sQ4H~L4Qqu7NO&nX(V}sk;1sEb?Eh?kyUGM}T^p&^w5v zF32-r&mPMrOU@U+#3rUZTZRPP>T_ao^lHPtFM5s7+ScmpfG)p7UrkT-RY&bvIr_xd z%8K=;FMh7^3)2R7oUs(YeE7(Sw<|(6FAhEtfL3p8QxVXiFs<|NWh+{Cdp~l?bGxT? zzj)QZSNqp{kJb{ zy=#Pd`08oTqdFbp%Db{ zH`q67PT;f4o#5&yO|ck-d8DbQLz-gJP)dw2VuE?x(o_?Xc%-R+zBEMu=_wm!RYHu> zqCx_}D509razd~v35qft!CM?X(v+QZ@<>y5C|o=6@RhFNWG&@^%LA8N2)I?)lU*LT z+(N*u!k+AUy5MqAnsTz{gs`l1aiTjg1cF!=#N1bRcVN=fcj*6)Hm6SL)w4H_USb2gnn7+@;ygup^M+&-rs#(2B1GDyd5fY zLemirTIQpsKPi?)a6oyY2oU>NLc)Tfij_*i{{NI5lSn0vUHTTj6AUoUt@(iDv~25- z^88|Bz7uRa)zF;Zkng-Qc>mVV-~ad7(T7sN_C!5nUh^CO!K|QJD@v~<@UHub%UXWo z1TzO}6C=RN0}r+N6j7TfCPp8->)t|bHjczYZT|DAO$Mhpl+|d28j{us35G<4YBXCb zL|IZrYZP76K;gEZ;j&UJ;MV(w*A#E6Ht zDfVH#DmOb~r>hbMJez zW@gR1v)29o=%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~5wmk~zC+~s8#J!}e?wzoW+Ue0Vj*VXWO;|k^*3nT|COP!|1TAimE|20 zJ0~$K$Gc*3vi=Pk&wpiTEUf=aZ0y`z#GGvG#2l~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;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;E1J*Tt5?56WW6I z81i9*wRZbEB1Z&9cf(vrIuiQARDRYmM3oB+HiYDds3L|Y6^0Li(-KCEq1%CY6g8$s z4GUJzp)Ur|epZdaWEEkUfGvhP3J%I4n6N*%pHMmgcSURg_k)lZM*4pKJ((GF+KA8= zsTp=T7Z2JgH z36h>J&4Gnc?!Je#%asV5Jrha}m6ZC_$P@z3R@=K2X`>calN8xh`4Qngl0ES~GR$wg z{c+W%DxWj6dD>WSX4M zV9&7c;XTqMcVIk~^&zT)?{(I|9r`m&2raWL)(i!W-$EbBi8fm=3tnjbiGajFUSPqu zO!0_sQ^t{f{^7%z0DC30Dn$&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_;bwKiXYRh7MAa6SoUm z3fdBm8}-8OyT*4`N1cuAz$RfU;j%9ZkgsIi4!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`Tb~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~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 zQMIKGA;3 z0qa5Ap~T_Dk5@E^N|b9i``4-OW;++HO_U>4g1Z{+mE+v zcXoHr_hAn>553n50ETMT}g`Oo>^3o1)5k|EBrdsbAE=*1=iD(a6M%_>acQfAMKZ+r7IO z{%yF5iK_l-w*J#V=>goK1-#MIOj6x7tLtjw&e|7y<% z>;j-b0~Em6!9Yj>peP_o){5CACH zpOwe}5O5GMP;gKPC`f1sFnG52N)#|~5>yrlVI?CpQm47|PwoZ0yco zCPkqr;#4j+FHKxHFmrzdQi{c^nu`C-bLGUEQrj}Ss{PtC?Y4a_L6wmItuC>5X6L4$ zzHfH-R^8k^D5Np}044CT{xYK@k&1GhuZ(PK*uAj6x~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~Sgmd(&yWpmH=Re zI*_1d{5!)8T$9fs{C4~geP)s&o@I-HfR(b&(}4hQgSwXJ*P+(86_5LZdujg*jWx-$ zQ_9m$q&9kP(hd@_!&a4p_>5g#-d7 z2PuF6>wp_PAV8A53OS&1TLG8F)= z=BlC6C&yQY-e831yhBCHYpr5k{wu!*Eymj)q9af2ChtRz(G~5yFOGa)rs27^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}oOWTCDtS1yS8Z-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&ZnGg-3eiSd<=%iVj;B zE*n*ngUZV}qSciV{{r{8oHVuAS~@Fy9^+KI04f`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)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&XXXPoZ zq~AO=Ov-KG(=uze5TiaFJZE6yd2`I0prw9jBxypQRmsxc=OX#C+EFFfFrML0Fryp|c%kSuKJ zmXi+tYjx8}PytrHce?lxxM#>+eMaki+=Txc0Wlrs`5ctZ6VuyoJI3QEP&ttaO~r$hh}&9xfd1v#rsbxs^U# z`DWTfef3&)42=|HxmC4oqYcUP<}1}ldya$%>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)P5g`yI-khlsZ|_MM1n z^7NhdOAC`a8BeQP@JMy+2ivDkMBeHS4m;?8^OuqpX%|gBBHASVY55+~5g)pSd3+_k zDpHVDSsj}}-aw0Swf4|Z2?o*@T05QgaRh+97jAQ}vtpwewYqF3N 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=7^VX`K$gh5?lUN zz(c+c*y@M`(wS)M8Vg@L_yte@x?fJLqbw;+6CD+$=n&^Qa(ab2r!BmJ^ zgk(`Q*-~h)^sy~DIyZ705qVCyPTq~v-xb2X|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;?jcUJRZsDObM%0f`UUcM~dZ*rQKc50M^cPu*;nj5Z#%cJ*(s z3P?mZDi0=$>OM99qLdPX>QwBDy8H4y#3>-)jDYBFTBun^nijTpwdAj3 z8vJN6c}z38vw4gQ6Ziu^(^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>+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&3Iuq|+yT5iLeIH@af%&ZRI(6(P{`2RrgK@uyTir{a zE|-OQ?4?nPZr8u;m9z$GPlAe)hhu$%E<`33ZlC6)kwqoiuaW+ 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)DT3yGl4-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;^`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^^zrNbn 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#t6X5uoqhdSmjbBo7rtWOps!!_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}ZT4E}bd^;UNh@nmaFYn3lbT`={OzsKT&lW_yA@|;Y! zpI~KoPxDqhK7PuI{r+A$?H64ydO=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`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-iI0d?Fb73CyXbUuq$OTS{loHcH@Sr0Cd5+v8i#vXG0efv(mZeDqZt z+kEfS7Fj;i;p|-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%~LQCCTX3Khu*uO-6z#Ly34 zkONbwtNlIPA5fIg{8*eQ*Ln7{T*k*&n%VBxP|xBY2~TIQjBg%6IN+61UBiwxmn|2C zzZJJ5GWgzVi+oE~4_CXPtRmzb2hcA%Kut1ihSE%H$`7-afdFxuaymX>42fTny|wV^>dLhHo>c$5 zPdDL(1nTcxh7nCuxI!-^g?*kck)_9umL9?WZ*~L=x}f z<3_F`EC$1s=QrUQbNw1jzhm_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_(3Q8 zI{}4CQi;^}iG>g4T&R&&l64;q`;U(V+RXC6e zAAXK#O;C@Pd9V9*^Op7S{P>RGTcpV2L2I#}4Y(IVl=Qi3p^X5)>tROpYx* zS1(jTPU4PSIb{Q1rMR}NC?7zK77en%SunzX!zsrg%T^n!RV3{yqc0N7oCxnEZ}}XJKZ3 zKj^;af?Bu`v;Tum;~$an?96|*GqSU?^&+PJN2l+3y1zS9dCvuPHL)aSQgCp#GkPCN z%GO)7nz%og@8rfQz2-{oOn*BMvh=_yd?`sJd-hp#)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>teC1=Ys^8>0M}`^G7`e3Y zrNz^4y47L~WcMDhbO}L+cXIfAHn(`sxy(mYY@pf*Ol-mVwDy6g#d5F*37*zqZsai= zJ9NSM%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%QEta1vf?}8=$6>W7i9fPW+`=^Rsz*pa>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#;-&QmXQGyCDK{1&7O%nbzvel^05X+ z3$6!l+a)gwWxsJr*u3lPd(2sT_?#eVJyO*N{Bt7?5oI+^;-h* zl<}ZxR-|!=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}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^=8Rq`0M#5~w zz;dr5PvgWBF5<^5Bfi6DX}@bHcF6S|{j2Km^>s6Rccz$Dz8j}Ah^zWpo#W~&p6$wj zjiI-$6rzHRy$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(qmy1s^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#hLt ze)D<&naZWVAZ6;ere$3Du?Tg49y9%JDI2)u#{6;oZ_boSh1V*Xfs!? zSWMCl4*TtrT1TnAQf|7TQkyriIG*_e%9=~&By(HIYRUWbI`jO&cYkI5{<-6N^;mIy zBzw|;=XwJ^Esqk+B0NbfACme;N`QKDbvg2>bz`vdzIEbhjC5*}CT8ZDzl6%hAY_*g~r{^U>TA9eHZtU^xY)s=HHIdBCuP~!$Rw63tSxsyxpQ*}C zH!Q$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

|9mMt5qmuo*k~S3%R)@c{}~62@*8+|4IaEi+f<-%+h8tAFvHw0_7t`(q-bDD|j`-xiA+yuI%v;<9>a%k_}#c7$X&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?~r0mnoWE=L8DINR>hp3Aro#F*jzhjD+H=P(kIMsA4FjH zosleX3=4}v!Stv>^pQVuc~<+9O}od&RqW(2%WFkBU$ieO&4lzen))z7HR#hvY%wJ= z%fkPIuy+8GrCHmx*Vr>_Y}>YN+qP}nwr$(S8hedx+xX|*``dB$7jfeJk)4&w0e2&9Jw=-);WxL3VJ|HNfn@1-^`C;iTYzd_V=jxK)abP<1##vLj~-$VW!t zm7<-^{I5<`tm}06enVsciXogAU&(z>kTSmt6xac%Eh~mcqlUh% zq^e8_(u$^2S?L?4z9ojC*}_@CXAgMew`F&^<0*T-Ky$1EM}Za^TXbFZHNo|H2S3C6wtmvh+1q27>Di(tG%g-CNbjn(oq#&QUH!W7c>(l7 z#_=Ouof|d%>+ADyLQL{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^DB?U~q$ZMv;&EiM7(dvQHTm#s&rzb9i{fn`gA_$L`g}+_ibUZAXa4r)n{3c# zVdPmM$KRh>WZm|U=ltsEH0M%kXSjgt{@RD>)d(I%`TN51=Y4*r$=1(z>vcE7 z`BrcBQqIb~k2T9|^O7bt<%aCp$0aNdIX^>D_H3v#1DXtkdB^$Dd>RjvB6pF~X%_@Zh3oAu*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+DU95yg^-Q`g-)-P?DO8n$QD{cZ zqXE*U(bJt6E6-$@=pvH!bKN9V@y>jJlUq+x~H73XCK+uo>`)9ehrwtSyS^;mKTb_+fGpRZE=M6pvBMwO+n*kql9J35fx+ zSFbNsNg6HK%TSKuGTCKZD9i%I5p&AGTOnV4Esn&jf&{`1ZdhqH)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_l1cKmKI|L4^%_&+ZeC=GL=NZH-y!9fngz zmdlc9feo(d3uD7PLzk#yui=FhljScv(`>4VjEbtGtEnrZD6UvcSPOC804m&8!`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{Mqg61hKfJ zu_nUs+34D)!v{gL*xY76d56j)|4K6&rSB zy60hr_=Eeqv!Gs*M}C6QP-{N#oS&dIDxDNCudPa+>`3n_%NR`X``h1Uc!@uyCf?q>>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}TN4%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_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{LBsEP2p)Rr2!r zkmi77bo(kJ`_spH*nKB2x|Qds=59R=&vE0#Ip}@BDkvyP#I@O#Mx4xX6XM!np0p&1 z$F{26J(DmhP;aI0H!8;Kp+Fuj<`mjCZZLP4MeIz7 zTPp;bUe&KmOu-oZLhd(Ek?|H%r7T?ncgqerT)tx1`toXNwsdohOwDWuG7cdG`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-&-mN7yTxTZTP6mTA=9s)IVn)u z+$0Kf(GJC86vXHGBbmayaE0(v1e1)LoJiup4w(sq>AiTs1<274K7a!S5(#$SoaV+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&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^0E8jyUh0Uyczp>G|`?VU^Y@ug@C6PQRY^_`fs1O4v{47 z(TrtqVpki2)!PQ6p$99voy>+XP98^?P{g73zEMDuBx3aympjQ7)pHBy1b6C$w&vp+ zL%q9~^_%Ce-O?S&>&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?Tgf!Y9-7X7qkh&F!^YX9;dL6F!2Tr zM2D1$O=g~uO*$Q*9tp^x9+mXZ$8YCuIu&Vs68$O$(Q*0wldHZUbpP-%A~{h80r3=A z>~NKsH*pv34xKG 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&nFw!_~_+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_I<&cjo0t-kn@SPfFl%yx5_n1HmCX_S@VmPlM_z*Ffr_I6woksGQ z-P?(!4J8)2op!r651=ueHZAxr$E&JtWmj%qdmVFbhp&o?;7+XX=+wrzR_TxIx48`__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^CdWN10H>YTg2Kjwo;PjG;ZD^+w+6p>rAW2B&(j? z0W}(j`$7GvuiHW9HXFr;jSh=!Kgmh{oK=sW4v+8C#p~bc+`=#()2CfQBzczD6imfL zfk0k%S8Yj3 zKi1eYw$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@qvqwwZQT2CQ$6 z50ym#t$Ylv0GDJ%1xAgORMn}LBYHu@?Nc-sOmbryl^F<75g5^$5JMG2X~$sjQ!g@w z*9<5;>@*SIfn$PHZD6Z@rbL+%c{!AXwwzasv&blA{4}V>lHrz++qrGdJ z%AtH4uW|yz&0R}zq%PF!ezoxURMhWXY{VFAS05hY6CYIgTzt6$Eu5Q7mAR2`Zq+;K z2n+K;l@nh^n$?)@oQTqU{xC zEszCW1;kH~pxc*DDsnB^L1Fo-Z*D2Jlwxz$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^jR!Hl5A{QZ*Hx~8lw;;QXZL(W=59OHH>$@`gP+KI|axE1=F$mGz zJxKTj01-9d5&fJ&`DIQ@N!Vg@VSHr2Y!oEUzyaZF?3AdPA0jCvl8wxBvUSW>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>Jqmi_qC7@u(QmaH+Bq$hZN(dR_|QkV1XRAFr)Eg z3|aJcfHfEATAge(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=Du!i%0{htAooTP+49;pmI$f-hw9uw7Hj!#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=*w`B=#R1 z=N~-d-&7LAKPbfye(`^yk{JJ|;D2LDYX5@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=etB+Yfj3Px=%8*QNSzibvJl zhTq1~{6ARFk7dmr{>Sz|lkC5-tpCLs{lCE;R@NVm=-*yr=zl08R)&8~f`1F*{75BM zsleDNsUq_{U-qoTKS3h|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&#jbp0y}lL3 zFP9{;l2GGo$nhDyTdbdvuIaPpfYsc)t?x@)`(ChlXPHsGwNP0@x9k12&q(sw{#wEO zqnFM_xLvw%sX2GR{Km^I1C@IKKl2mNQlEo^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?E*0T5v;$#k)z(Zw-);FF5Q#ERjgifWaQR>rLi<(RPI(yxj$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}WghSRz$?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|Yq4xBn3tK$_>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^l6wtzHjqF7%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#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+UpD1qzD(iQyK+d!rqTu< zO!!v|uVa>RXIBMLo^>u6p|z08yUdKd-yU8hRc`@>TVHn9QZt2Vi>$%Ydp|4PU*KOP z1=5Ua7xdq<`b$i>X-)S|-4SO5kc)plt^SJUmC ziG1Q$)S(=`YkYnissNRTJ#*S$I-#s{{K7oT8+B_Ho=!@mJ=y|`<=w3^Y3pKBfu5;zDC(sZ_T@^|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`Nbr=LB;#l) zAr6g|p)GdvJ0(?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>dl7DMEmGpv2aYhSZXSfs{qYQ+_HFd{89hdlv5d7-3nsn2Q>t0MM8N4Ar^FO zD6;cxKmYhWCt85v+HatsWJWz!>UfmM4q_9Ue2$JG&-uX}iIS>{=+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?iXV9xUHs6Ier+|7kYQrsB#kljR2qL;Bc$CRP^WnwI z8WsP6zm+3ca%o`?8@x@T5?8!Rs3}xf203Z%2#ZOhd8VyZK$v72AzuJ44K-m$W%r)O z$RBA}Q9FkoK|ilZA8uq22 zLI6mNZ?SA6&bCHDRAfy+>r&pG+5^$2M&Io>ODx;KXSsYQKDIAZi@7 zh#;gLqR~fXs4|>IMk0)dRzzr1^dm|wQ_^AkJY zL$#_c?`7y{)Hk^^=vHvJnVLp!a~9i8s_#i68U(rkq!N#zGCf&(KHHM6q5 zT%zGyY~lg51WCLmGIv}mc$CsJ40|%lE#=8d^n>jI$s&HPs5V*Xi8@>OanUyfmK!?T zPUs_O9lm+CNO10yv7nV$*l%U4WEO)mVg##dedlG7mZjc6?yR< 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$&5ZN0x1=R!M%2|}>aNe<3YItP*4Vjy>d5Mf*O{{GXvMX%oLGoFhxqmyJa!Ca zz=(ZEy2{Ue<&RD!eX#aBKbi(3PXs1kkCWadPK6uk2=Wq z1M>3Gw#c`S*;mKn24H2&!OJZoESUn3Mn9@05sb|(Svcc0T?k%Kn8 zR1Lgkkx0FKgp^Cc0ll|}iHQjID*DU&CEMSzUA2aVLxP=1Eex!W^_!Ll(e(rQjnFL! zDe3&U4*+LJeN-FiZ^>KhzkH9S_!3Y@{RAr{jWTy|_>* zhO*MkD((T|rVYUE4gf)k?E=BjOP|hyS%~aST4T3Xs`B@5rfzPBz*X;y$TD;6 z2)v$L-&vK7vg4RTV!dREP8UlgCoLw<&M%OYH6s%W5$HiOVF)gfte-ELzfu@rmyk{7 zOGtR*)K^AHIQso7SaEvM|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;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@_Z^#IkcHlmkXx&SP|8bdLR zMWQ?%nqLtMJX~1JSz+qme z)%8*5A!yBuJQ*X}(}g^M389>Eg6>j4mBqu*UskC6Q2mI0GLAkGHT%8xJR_NTkV+NZ zK0R=jaYhMtJ^tNqf55+r4qZm8{BU0ME1trDxFrL>C^rYzFzte;IDZ((~!w83ZL!wMBR;dAgbp* z-}aif6_RO2pX*WYSA_Rzw&_sBX_>5*qsgq5gUGlQ2nZ$!O*KGbh&DP<9kfqyeZIWt ztRSS{PEN$w`Lhu>>17fA+zfq-KGI(PG3WgaCy&ubO}`v9wZ)8_f!B?6-;~({Tt|U zzo%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%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_}YER1cdHI-NLUEYYwj50*tLFfVm#zs6Rp!ewP*FzR!;mS*^yi7 z7UfHc(3S$CJGdXzrMDWdn(jGWEphgy%L04&B*I>WdY{tOPSs``W!nlSaFB8^9>;(HLh ziK$rH@I;!~&BkTC7pZNbS#;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?|3w0dYULac4FDTJ~^;~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?#@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=;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<4EEW}Ij*r+utjNfp}9k6eRLZbz_6s<_HL&aniWx8AVn z=zHPlT#VH#@QSdCP=e%jey2WHd`dq!s)@Nch`p~GZ9q(Ao$W$WreiEjABRR%bn;cW zJT0I=HFNCA%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>ZM#2Hj-1t*a$WibD9lC@jM}=DAE&=LF*Kt94Xg^{US9=~_NnCAYYwvHec_IF zz5<(7c!hAx#6_vuLK|ecI@wu$grbAFMa&Aek;shc*W`+M{6wU 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{ohxCof1rHJGKSMtPQv(iJcu{@x$aNfa9V=F-W>o7!@n*l56@-JtKYLGrRi z%ng^?u7|vfrqCljha^s{+XooGQbEENr6`cWsi?u{{a{4MI7# z_Y{bcm%kZhGqefCAVE&oyFC`}rJFCIr>guAz}zJ)I-Y3ZtPGJJBK=<9<-Ggjd}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&COhKZvhqunKpsXe*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#OvC907N+hi*Iks2|ciliJ0cie8nxx?GgWf+Gp}mP|5X{=cpYd25={A-;c=|FTnQFTVww zCYCWvIw~|nY1cqVd~{DJUAO^`JrH^bRzyA?R!Aw%^VSrXb+N(jfvVae^g@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#t42ESYew37R@-W`%b5}%UOS9zPlZcS zp^)HQfIMWEyt#w~l%oBS)oRfm?0vJTxhS68cz|#e1ZSv>zaM&JI6gd6cGv*VY%W|+ z09ybgbg${aj4^igz-Kx0P@EMK$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?lSVsx^+QBaxP*O|+R$!xFLIxEf4R9|5#x&_S(UyRzX}`5y5OEhIWbW0hu5l-r3cAV zB}bSH;9sS)5>16c?Rab>fss`Vwi?`Agi(@Vh>T==Uf75MTXdHT{?UY1%r4Uwb*6q#B^CVTj!yskTGO!;$&b&VX@mfjb9ECiVsct#IZfwdDo*cV+v zVfCwRQV1TmR9+^t8`>axeUDUppY9Npff~SjUV}o|)(E>ZX^+hcBRl`ELXz;LD)E=i+1>oTUFYp)~vUQDoNurex0F4sWJmEOsb90EgO zkebY3xgy+GJBd%Ro7$Ylh{X}d=t6XgKtM&L5F9XHVWLU~oI=;GkQ^}W!Ui!sfgYwv z9}ZQxjs{&gEFW!^mIj{L1D}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%Ay3ewD3;$AME`%|P)v-B|Aj-b03aMXS~|wRF*g4VC;e|Y)NiKepC8KKaj5^thw?8> z6oCFx_=lO3;{V7*{r<%Nm5E~f2dDH8=7@u*5 zma_m@CBP#qU`@K;YyBQ0%O5W5uSaG8zx1#DFf+3Pcq%r4{q$eY%)ePK0RHv+H5NL0 zJcd78$;?9cXO51RiS^IR%&Y)V3hoNWBYH%v46+*{VUG*r-lD_n2!zs&i#*$V^qw{ zzhORlCIFp93veEzrDguxCja}r_iv_S0N(81?z{iBf&biFe{arzu(STdAa-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|1{Oa3lmw$}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#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*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#(AYXdlNilh8Dj17B0o}!KR{?jgwezdi--N8ch z%^^L*px1RJkqmku?cmJArir&&?l%}#iK6X7O;V-Z%DTKAS)lnoo1U7bf)bI`t%z#9 zd}Km$-?JvxCOymhZ<*a(c&V1a;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;}<T(Nb_^22zN-oMA`Y?fZp1F zWoss+Q(JcjLq*NjtK00$cj#4MLKo}x&)AE7>E_>|P}t^;@eBeEa>zB>s_?LSr z@a%5*!b~`#d`^_!rwQ``P78GHpuG6o74xq^j`vT4Oh9(^FNd}Noy2Bj_}ihNQbo#U zml?k6MCFnc1-_p~Zmw>!eH|65&!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*W3oP%%``eM6L?(eFGV&gmR))*BiOtNGHTc2 zYLf$p3L-fx9hPdF8>i|+-da5^pnx+WqKygeyL2HIWdXFqV;n$Gs=C_3wrYrT#bpfEJQlvag;8(>w>!B>-9D0WI%oB_3hoW&zBnk3#U81v$ z!6Ov7pae153tpv)L8y#u0@v_C5Bb=QWU2b9(;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-o7e@G|$xv8WIV|s!;9)5$<++$JWZcnIl>_rs$m3nb`co-+TH!Fn## zmnfv<3+w2MlOd>SI2n@rXdRt6?n@P9*w@=?(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~+Wzz8j3zX6Nik2d=57jN9U3G&Zy28_?VQu+`#}HLKPyj-vk91k@;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$EX>Wlxc^G9inUTcY zu3=j7)7C2a?DS(5&QM27Gn>+lwU=B^>avdCLr5C##f(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)d7VJ9BWtZiyRMpv?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&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=;^5AREzh69M!1G&$f!2%JVJoO>VXM@+~bv_rvbp3Rtn7hr6OOlB8KWdyi ziU$t(T;uRc1VRR)*QDS|RKTR!qes#+IoczwgUuL(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`eWvH8f8IaWnNx5DY$AR zmQPq^O$f+uFR&_f!KIW!5k0YoESkoT8{_DCUIZM%9)wMWl6b;NdXWSY9)8g*li%gy zYx;i5RzY>P{6v174rY*!@&)JP2wI7@k5acTlc(-GrdAauT@-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>!~|0 z$hS(Kngf z?(u!w01OK~(qcm)p>-?t^x6|Sj$J{m#Z71{6Dwj1q{xmjXId8eE?>k7l{lnM$#ABYgXJ?J#*ql%qs%ZKUqIm`*UnMq3s*4cj+;-q z_m<12cQ+H<$~()}(-^{1`Di@U6G|>OVzmrNtrab2|vf2UYYd6b85Ukq4#;nrM^C~4%SdmSU!K7 zhr{d=R)v1fxnm}G#PQ=uSp_yDG6l=TE@aGp?~n*VtZqchb04SXGKiqo3Lsc5w*KEzCk`V?ODyip6zd}&`(ggk(@=3GTCzDc zL-^g9Dv%)uTW`yz1c0i>o`Qna3;Ya|px=SeRCd-lst7 zUHay)8(iCN<&Ov5g;uRK?J`5{BrDExNf@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`cHq^_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+WkEp%ayr_M#xw!od@Q@wwE_o^P5KOSk7!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$uaYShslH1+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=tXySdT@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(ahROS2oybF3g_{IkVF|VQ-B{c88IsdR)-#=BDR8Z~(RJ*YTMcC=3<9S6{KAaKKt~~5h4mh0n*Oi*`)jkSR68S`LJs1Q}+I6?7%AQKX@DLbu zk_iwJm2_I}OLL0|-`02Cb`4kf%eE>;KeqFW$4>?D*1Brqm&_ZRkRgtEnTG zJl!%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&%3LZoWe;sTbXRX=Br=}0*|H&=zmjlB``1a6&zhK4O`#!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~SWSu8EFeJIsjVG@O`yu7|x)XdEZ#agQ;OsGPe-#=m(M(I#E?qSM&f 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(%J5aPkGYPWua1+^LTcpUxcxeoswme%pkVWD2g)BAI3tYlWEOxpHnFcqX>W9z3i zRHuSTTv%olv8c*b%yPrAW&U01ZJJt+P*7))gYi?;y9sMCT#S~PwX{43FwLQ;sAtRc zs9|X@<`6g8HWiuSfc0;SW(^fD&MRBWTTbgLGw?jbvxB{r zsmU^n4;QSSBF#9+HH1Ll>pUa~?v}caG7t^N9_`$SQppbY^233SU-`mXMA~ea#)MAx zWn&BvuL2x)WZW+vP$>iCsG_hefYa}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<>}jaRuA_-=-mFMZ)5zAMGeeM49tH^_&QUh z5>{Igf-jy?IQ_|>hG@V#kuhApec2D*i$%h@af&99I;T4`$!|Lwqt*0mbrts3>gKt!VQIq>)3ooer1Um^ zToch!5rpFgwRrBnF)HqqZqb^GkuIz>Jh?MD%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!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>SqweN~ZKaEbt zJo%+E_s7J2^Onzx#>Ldw7oqUv>&(Q(ZH7#VA{4& zixt!A^&^Ec^Y>3CCN1Ic@65(0W${@##}f~<29s!^Z){1(3xkVYZDv{|-KKHFc{ENqkrDGbAD-5!Iu-fut8)aEE-16eYjlsNykL>4U zcHJ^<*iNGiOwM`==OvpSFW&FOYKhaI)f3Yj13;uN-F0x0M(olG%it6zXAzJ0{;ZHC%DAm)<{x#h<=kx~@Bm zl2OhGE{o11H!@1GtT&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;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;Lto@D*>hzY6)swH3vY~7ysUjh zCoWu1%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=~2QhDnXl<%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#yra`$zimb}a z2ZEQLtqv55kMG%=4D+k-E^^s;>OrsH1vi#u|4eZQd;@t||iHSh8LPvgbFYSn|dm$B~m4FS&B z@q!ywJ$CfgwR)+M&kO1XdC|L@ZYt#XR6gv41o>Miysc>nDNa7ZZQ6*fPbQ4bEZ)EW zj7XOR7 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@na!8;5rnXDN7#$44~`!4If;c)3fhCc+g_lb2+{+arxW`u zA_3Rt^%#wP%bsaCoo5=*;zx8{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^RQRGs13@Gi|+QFz!TX)Y0pud1yQVpUo$q4eIzqB1A4L`RfH2fx8N zp&boHKesMU$4s?-7?G)m%lfm$UP}!pjaI@>A?@S$-6Sh~ISRW|g$pjZGE8p~cS9`D zRwB^=Pd2m3q}#UNHI>D|*i`V=w>U6;w^?XBj5= z>OJVsdo<{ns3a{|Zz^$aqhKIXL8bu_sXo;_GS(m{Jx>CH~DwcM4ZtXyzi8 zCW7`h$d=_^gw(lWYAs6tJBmJ=w~@7Cn-Zyp z{W;#lo)WGyej9q#_&$}mbbw*L`D>9lU%8y53*d;>^yU$z*Wf1X16#BRx`Ul$1xp44 zHovKV0KH=^mW;8KOy~PD64TjNFs#12*dlGZjqa 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!LQM`h;BOXI>aqVvdJm82H@Rt<6W|4zgXdz1j< zBGw%Q<0WEwAlgXZ(3qnE$~nX$8@Fl7kw(PHi6Oky`C47 z8#TSO{&>W2wSj7iVKYTAu;@iUSqhRRG4u{+j_1-pLmRi_`DyZzXD1`D%!1BC+ie9dvRjmpl{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?wCwl9yQX&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?0c*fn1B3G&j9^Mmd z6CI_#3~c*p4t|FDleXtsJ(|daP^HhNhmp<#>hzk9mYy4SX;kwRB=cjzIp|$LEwk4L z)JdY&xO{m{@t~N>rHjS)ZS%QE@EdMf%;E+f_AQoG`_tR6 z$k=XBo`kuPSKNwJDSRK9_%M*S#uZFbPyaf@06}ws(*z>})IO_=JRL*Yu5ij_LTZl9 z5ZsU58N7OtiuSp%;ESVmp)97gdgy1(@Z?jY z^oQ#tr>mMx*m8!|*H?L?cMcrKaGNk=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&EBJ{&o4RONAI89nVmQrB&X0fe|qV8qcN4tp`hahacf>kdB}Qjn4ZoC4`k99 zdW05cu2J82p5m$~MY-P3;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*=tTxtCMPv-xt1^lvmmk;P=xk*_sI-yr7*d^)8o4Fc5e|hufBUB z6DN`LdzP(!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?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 zWrpOK{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-3oewD%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$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{vVH3aiOA*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`{ zyfhJP6vuWb7Rd&~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#%`!jPCXcs=MjA)rZoY#2ecg( z{fOgp^y19fqO`3SKw6SdSMP$|>BtZ?`kM%oD&8NpfB6_ffWyzeA+*vLlP6@-em1$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|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`fUJg7X-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_sBqY5F6NIQ7p;`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_lQubm6<6A+>8E&7hi*VeC^E z=mN{y&zUJ_!FTJf&^odKqaxCvfNxn9ZKK-r4YxbQzH-de1zFQpX7Ppx8{ z4K7tB^%WE?Smp1SpMKW-F;Vc!qQT_u+*tsLOYei^Vo;?@+%5?06Y@+omQ^;>8w*Pk~_EnPEajCE67 zth!T}AhHfkc_$m&$(iE97P(|hazk{A-%xbh!{n>pbZk#YlzWEJ;(c>Vg_~}4w|1sT zF06gWu3XVnd2ma7L*6lSWD@ zl=_r~GCo-6^@{^~!evJ|KR8~s$YQoLoFPgh3LVvVm^zStwv`KVM5!yJyh=vJrtzw+ zy$)FLDr8E{pTU24hwV%F(!2b&A7JDQ_A$o*CTnYToeQ$Do|k9X<SA1KZs-zafAT-BU2W1uS^Xds?pq*%m#*f z-3g@$No!_4Bzw-(JeGtNfhBqg#>=($$0#0$5wwcpR!$1|OAyRACM4nan{d6?7~j8B zBT$^`LO03RLO~Dek?weq8_H+MK@)ch%DU-xm=P8|D|0RpspBME_|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&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;?bpoPT#3`#P^y{QH8g(-*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<#> zF2MJnR(!&mFicE1R`f5>=$)ld8(1OB2` z{~`N@PX1FSAn=dAJUjsShWNcN4G!;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@@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/library/src/test/resources/pdfbox/input/eu-001.pdf-sorted.txt b/library/src/test/resources/pdfbox/input/eu-001.pdf-sorted.txt new file mode 100644 index 00000000..c64993be --- /dev/null +++ b/library/src/test/resources/pdfbox/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/library/src/test/resources/pdfbox/input/eu-001.pdf-tabula.txt b/library/src/test/resources/pdfbox/input/eu-001.pdf-tabula.txt new file mode 100644 index 00000000..ffdbba11 --- /dev/null +++ b/library/src/test/resources/pdfbox/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/library/src/test/resources/pdfbox/input/eu-001.pdf.txt b/library/src/test/resources/pdfbox/input/eu-001.pdf.txt new file mode 100644 index 00000000..7ce680d3 --- /dev/null +++ b/library/src/test/resources/pdfbox/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 + +