From fa63b019dc19be9efaeee36493ae22c42995232b Mon Sep 17 00:00:00 2001 From: Kanstantsin Valeitsenak <59480976+ainceborn@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:13:59 +0300 Subject: [PATCH 1/6] 3.0.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: restore clipping path support in PageDrawer The Note box text in certain PDFs (e.g. WoW manual) was invisible because the decorative frame image was painted over the text without the intended clipping restriction. Root cause 1 — W/W* operators were silently ignored clip() only stored clipWindingRule but never called intersectClippingPath(). The clipping path in the graphics state was therefore never updated, so images were drawn over the full page instead of just the frame/border area. Fixed by immediately calling intersectClippingPath(linePath) inside clip(), matching the behaviour of the original Apache PDFBox. Root cause 2 — drawImage() never applied the current clipping path Even after fixing clip(), images were drawn without respecting the active clip because drawImage() never called setClip() before painting. Fixed by adding setClip() at the start of drawImage(). Regression fix — image flipping on pages with tiling patterns After the above fixes, images on pages that use a Pattern fill (/P1 scn) were rendered upside-down. The cause was drawTilingPattern(): it switches to a temporary Canvas for the pattern, calls setClip() which updates clipSaveCount to a save-level valid only for that temporary Canvas, then returns without restoring clipSaveCount. The next setClip() call on the main Canvas called canvas.restoreToCount() with a stale value, rolling the main Canvas back past the page y-flip transform, which flipped every subsequent image. Fixed by saving and restoring clipSaveCount around the tiling pattern rendering, the same way lastClip is already handled. --- README.md | 2 +- gradle.properties | 2 +- .../pdfbox/rendering/PageDrawer.java | 20 +++++++++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6a01619a..57889bf5 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ repositories { } ... dependencies { - implementation 'com.github.ainceborn:PdfBox-Android-3:3.0.1' + implementation 'com.github.ainceborn:PdfBox-Android-3:3.0.2' } ``` diff --git a/gradle.properties b/gradle.properties index 03a73c84..e8e5b388 100755 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ org.gradle.jvmargs=-Xmx4096M -XX:MaxMetaspaceSize=1024m -Dkotlin.daemon.jvm.opti org.gradle.parallel=true org.gradle.caching=true -VERSION_NAME=3.0.1 +VERSION_NAME=3.0.2 VERSION_CODE=1 ANDROID_BUILD_MIN_SDK_VERSION=26 diff --git a/library/src/main/java/com/ainceborn/pdfbox/rendering/PageDrawer.java b/library/src/main/java/com/ainceborn/pdfbox/rendering/PageDrawer.java index cc0280a1..2f0bb937 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/rendering/PageDrawer.java +++ b/library/src/main/java/com/ainceborn/pdfbox/rendering/PageDrawer.java @@ -281,6 +281,7 @@ public void drawTilingPattern(Canvas canvas, Path savedLinePath = this.linePath; Path.FillType savedClipFillType = this.clipWindingRule; Region savedLastClip = this.lastClip; + int savedClipSaveCount = this.clipSaveCount; // must save/restore to avoid corrupting main canvas state Path savedInitialClip = this.initialClip; boolean savedFlipTG = this.flipTG; @@ -288,6 +289,7 @@ public void drawTilingPattern(Canvas canvas, this.linePath = new Path(); this.clipWindingRule = Path.FillType.WINDING; this.lastClip = null; + this.clipSaveCount = 0; // reset for the tiling pattern's canvas this.initialClip = null; this.flipTG = true; setRenderingHints(); @@ -298,6 +300,7 @@ public void drawTilingPattern(Canvas canvas, this.canvas = savedCanvas; this.linePath = savedLinePath; this.lastClip = savedLastClip; + this.clipSaveCount = savedClipSaveCount; // restore main canvas clip state this.initialClip = savedInitialClip; this.clipWindingRule = savedClipFillType; } @@ -761,8 +764,16 @@ public void fillAndStrokePath(Path.FillType windingRule) throws IOException @Override public void clip(Path.FillType windingRule) { - // the clipping path will not be updated until the succeeding painting operator is called - clipWindingRule = windingRule; + // Apply clip immediately like original PDFBox (not deferred to endPath) + // PDFBOX-4949: don't clip if path is empty ("W n" only, no actual path) + if (!linePath.isEmpty()) + { + linePath.setFillType(windingRule); + getGraphicsState().intersectClippingPath(linePath); + } + // PDFBOX-3836: reset lastClip so setClip() re-evaluates on next draw call + lastClip = null; + clipWindingRule = null; } @Override @@ -855,6 +866,9 @@ public void drawImage(PDImage pdImage) throws IOException } } + // Apply current clipping path before drawing image (matches original PDFBox behaviour) + setClip(); + if (pdImage.isStencil()) { if (graphicsState.getNonStrokingColor().getColorSpace() instanceof PDPattern) @@ -1010,8 +1024,6 @@ else if (scaleX != 0 && scaleY != 0) // the setRenderingHint method, so we re-set all hints, see PDFBOX-2302 setRenderingHints(); } - - canvas.save(); } private void drawBufferedImageV2(PDImage pdImage, Bitmap image, AffineTransform at, Canvas canvas) throws IOException From 038f9d04e6778d8760c22a5d828a88df80384699 Mon Sep 17 00:00:00 2001 From: Kanstantsin Valeitsenak Date: Tue, 14 Jul 2026 17:00:58 +0200 Subject: [PATCH 2/6] Image rendering: checkboxes and small images with soft masks no longer disappear. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Small images with an `/SMask` (soft mask) — commonly used to render checkboxes, form widgets, and icons in PDF forms — were not being drawn on the page starting from version 3.0.1. Root cause: the new `drawBufferedImageV2()` code path (introduced in 3.0.1) included a downscaling optimization that reset the Canvas transformation matrix via `canvas.setMatrix(new Matrix())` and attempted to reconstruct positioning from the CTM only. Because the DPI/flip transform applied in `PageDrawer.drawPage()` was not taken into account, the images ended up being drawn in the wrong coordinate space (typically outside the visible canvas), so nothing appeared on the rendered page. Fix: non-stencil images (regular XObject images, including those with soft masks such as checkboxes) are now drawn via `drawBitmap()` again — the same simple, correct path used in 3.0.0. Stencil images with patterns still use the newer code path where it is needed for correct pattern masking. --- .../main/java/com/ainceborn/pdfbox/rendering/PageDrawer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/ainceborn/pdfbox/rendering/PageDrawer.java b/library/src/main/java/com/ainceborn/pdfbox/rendering/PageDrawer.java index 2f0bb937..a2069fa0 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/rendering/PageDrawer.java +++ b/library/src/main/java/com/ainceborn/pdfbox/rendering/PageDrawer.java @@ -1009,12 +1009,12 @@ else if (scaleX != 0 && scaleY != 0) { int subsampling = getSubsampling(pdImage, at); // draw the subsampled image - drawBufferedImageV2(pdImage, pdImage.getImage(null, subsampling), at , canvas); + drawBitmap(pdImage.getImage(null, subsampling), at); } else { // subsampling not allowed, draw the image - drawBufferedImageV2(pdImage, pdImage.getImage(), at, canvas); + drawBitmap(pdImage.getImage(), at); } } From aa4d724028168196c0a874391d99f799d180d4cb Mon Sep 17 00:00:00 2001 From: Kanstantsin Valeitsenak Date: Tue, 14 Jul 2026 20:18:07 +0200 Subject: [PATCH 3/6] move latest fixes from 3.0.8 --- .../com/ainceborn/fontbox/ttf/TTFParser.java | 10 +- .../com/ainceborn/pdfbox/cos/COSArray.java | 129 ++- .../ainceborn/pdfbox/cos/COSDictionary.java | 62 +- .../pdfbox/filter/CCITTFaxFilter.java | 87 ++- .../com/ainceborn/pdfbox/filter/Filter.java | 10 + .../pdfbox/multipdf/PDFMergerUtility.java | 3 +- .../ainceborn/pdfbox/multipdf/Splitter.java | 739 +++++++++++++++++- .../ainceborn/pdfbox/pdfwriter/COSWriter.java | 19 +- .../ContentStreamForGlyphLayoutInterface.java | 49 ++ .../GlyphLayoutProcessorInterface.java | 28 + .../pdfbox/pdmodel/GlyphsAndPositions.java | 98 +++ .../pdmodel/PDAbstractContentStream.java | 206 ++++- .../ainceborn/pdfbox/pdmodel/PDDocument.java | 49 +- .../pdfbox/pdmodel/PDDocumentCatalog.java | 6 + .../com/ainceborn/pdfbox/pdmodel/PDPage.java | 24 +- .../ainceborn/pdfbox/pdmodel/PDResources.java | 93 ++- .../AcroFormOrphanWidgetsProcessor.java | 32 +- .../pdmodel/font/PDType1FontEmbedder.java | 1 - .../pdfbox/pdmodel/font/TrueTypeEmbedder.java | 4 +- .../pdmodel/graphics/blend/BlendMode.java | 459 +++++------ .../graphics/blend/NonSeparableBlendMode.java | 31 - .../graphics/blend/SeparableBlendMode.java | 31 - .../state/PDExtendedGraphicsState.java | 68 +- .../graphics/state/PDGraphicsState.java | 67 +- .../pdmodel/graphics/state/PDSoftMask.java | 51 +- .../pdfbox/pdmodel/interactive/TextAlign.java | 30 + .../interactive/annotation/PDAnnotation.java | 14 + .../annotation/PDAppearanceDictionary.java | 30 +- .../annotation/PDAppearanceEntry.java | 30 +- .../form/AppearanceGeneratorHelper.java | 653 +++++++++------- .../pdmodel/interactive/form/PDAcroForm.java | 281 ++++--- .../pdmodel/interactive/form/PDButton.java | 199 ++--- .../pdmodel/interactive/form/PDChoice.java | 79 +- .../pdmodel/interactive/form/PDComboBox.java | 16 +- .../form/PDDefaultAppearanceString.java | 44 +- .../pdmodel/interactive/form/PDField.java | 48 +- .../interactive/form/PDTerminalField.java | 28 +- .../interactive/form/PDVariableText.java | 33 +- .../interactive/form/PlainTextFormatter.java | 73 +- 39 files changed, 2610 insertions(+), 1304 deletions(-) create mode 100644 library/src/main/java/com/ainceborn/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java create mode 100644 library/src/main/java/com/ainceborn/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java create mode 100644 library/src/main/java/com/ainceborn/pdfbox/pdmodel/GlyphsAndPositions.java delete mode 100644 library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/NonSeparableBlendMode.java delete mode 100644 library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/SeparableBlendMode.java create mode 100644 library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/TextAlign.java diff --git a/library/src/main/java/com/ainceborn/fontbox/ttf/TTFParser.java b/library/src/main/java/com/ainceborn/fontbox/ttf/TTFParser.java index 68e7fd40..93145127 100644 --- a/library/src/main/java/com/ainceborn/fontbox/ttf/TTFParser.java +++ b/library/src/main/java/com/ainceborn/fontbox/ttf/TTFParser.java @@ -265,14 +265,19 @@ FontHeaders parseTableHeaders(TTFDataStream raf) throws IOException // panose = os2WindowsMetricsTable.getPanose(); outHeaders.setOs2Windows(font.getOS2Windows()); - boolean isOTFAndPostScript; + boolean isOTFAndPostScript = false; if (font instanceof OpenTypeFont && ((OpenTypeFont) font).isPostScript()) { - isOTFAndPostScript = true; if (((OpenTypeFont) font).isSupportedOTF()) { + isOTFAndPostScript = true; font.readTableHeaders(CFFTable.TAG, outHeaders); // calls CFFTable.readHeaders(); } + else + { + outHeaders.setError("OpenType fonts using CFF2 outlines are not supported"); + return outHeaders; + } } else if (!(font instanceof OpenTypeFont) && font.tables.containsKey(CFFTable.TAG)) { @@ -281,7 +286,6 @@ else if (!(font instanceof OpenTypeFont) && font.tables.containsKey(CFFTable.TAG } else { - isOTFAndPostScript = false; TTFTable gcid = font.getTableMap().get("gcid"); if (gcid != null && gcid.getLength() >= FontHeaders.BYTES_GCID) { diff --git a/library/src/main/java/com/ainceborn/pdfbox/cos/COSArray.java b/library/src/main/java/com/ainceborn/pdfbox/cos/COSArray.java index 1a2dc3f3..264b66ef 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/cos/COSArray.java +++ b/library/src/main/java/com/ainceborn/pdfbox/cos/COSArray.java @@ -61,10 +61,10 @@ public COSArray() public COSArray(List cosObjectables) { this( - cosObjectables.stream() - .map(co -> co == null ? null : co.getCOSObject()) - .collect(Collectors.toCollection(ArrayList::new)), - true + cosObjectables.stream() + .map(co -> co == null ? null : co.getCOSObject()) + .collect(Collectors.toCollection(ArrayList::new)), + true ); } @@ -82,18 +82,9 @@ private COSArray(ArrayList cosObjects, boolean direct) */ public void add(COSBase object) { - if ((object instanceof COSDictionary || object instanceof COSArray) && !object.isDirect() - && object.getKey() != null) - { - COSObject cosObject = new COSObject(object, object.getKey()); - objects.add(cosObject); - getUpdateState().update(cosObject); - } - else - { - objects.add(object); - getUpdateState().update(object); - } + COSBase objectToAdd = maybeWrap(object); + objects.add(objectToAdd); + getUpdateState().update(objectToAdd); } /** @@ -120,18 +111,9 @@ public void add( COSObjectable object ) */ public void add( int i, COSBase object) { - if ((object instanceof COSDictionary || object instanceof COSArray) && !object.isDirect() - && object.getKey() != null) - { - COSObject cosObject = new COSObject(object, object.getKey()); - objects.add(i, cosObject); - getUpdateState().update(cosObject); - } - else - { - objects.add(i, object); - getUpdateState().update(object); - } + COSBase objectToAdd = maybeWrap(object); + objects.add(i, objectToAdd); + getUpdateState().update(objectToAdd); } /** @@ -185,14 +167,16 @@ public void addAll( Collection objectsList ) * * @param objectList The list of objects to add. */ - public void addAll( COSArray objectList ) + public void addAll(COSArray objectList) { - if( objectList != null ) + if (objectList == null) { - if (objects.addAll(objectList.objects)) - { - getUpdateState().update(objectList); - } + return; + } + + if (objects.addAll(objectList.objects)) + { + getUpdateState().update(objectList); } } @@ -217,20 +201,11 @@ public void addAll( int i, Collection objectList ) * @param index zero based index into array. * @param object The object to set. */ - public void set( int index, COSBase object) + public void set(int index, COSBase object) { - if ((object instanceof COSDictionary || object instanceof COSArray) && !object.isDirect() - && object.getKey() != null) - { - COSObject cosObject = new COSObject(object, object.getKey()); - objects.set(index, cosObject); - getUpdateState().update(cosObject); - } - else - { - objects.set(index, object); - getUpdateState().update(object); - } + COSBase objectToAdd = maybeWrap(object); + objects.set(index, objectToAdd); + getUpdateState().update(objectToAdd); } /** @@ -617,7 +592,6 @@ public void growToSize( int size, COSBase object ) while( size() < size ) { add( object ); - getUpdateState().update(object); } getUpdateState().update(); } @@ -800,18 +774,18 @@ public COSUpdateState getUpdateState() /** * Collects all indirect objects numbers within this COSArray and all included dictionaries. It is used to avoid - * mixed up object numbers when importing an existing page to another pdf. + * overlapping object numbers when importing an existing page to another pdf. * * Expert use only. You might run into an endless recursion if choosing a wrong starting point. * * @param indirectObjects a collection of already found indirect objects. * */ - public void getIndirectObjectKeys(Collection indirectObjects) + protected Collection resetObjectKeys(Collection indirectObjects) { if (indirectObjects == null) { - return; + return indirectObjects; } COSObjectKey key = getKey(); if (key != null) @@ -819,46 +793,59 @@ public void getIndirectObjectKeys(Collection indirectObjects) // avoid endless recursions if (indirectObjects.contains(key)) { - return; - } - else - { - indirectObjects.add(key); + return indirectObjects; } + indirectObjects.add(key); + // reset key + setKey(null); } - for (COSBase cosBase : objects) { if (cosBase == null) { continue; } - COSObjectKey cosBaseKey = cosBase.getKey(); - if (cosBaseKey != null && indirectObjects.contains(cosBaseKey)) + COSObjectKey indirectObjectKey = cosBase instanceof COSObject ? cosBase.getKey() : null; + if (indirectObjectKey != null) { - continue; - } - if (cosBase instanceof COSObject) - { - // dereference object - cosBase = ((COSObject) cosBase).getObject(); + if (indirectObjects.contains(indirectObjectKey)) + { + continue; + } + // dereference object first + COSBase dereferencedObject = ((COSObject) cosBase).getObject(); + // reset key + cosBase.setKey(null); + cosBase = dereferencedObject; } if (cosBase instanceof COSDictionary) { - // descend to included dictionary to collect all included indirect objects - ((COSDictionary) cosBase).getIndirectObjectKeys(indirectObjects); + // descend to included dictionary to reset all included indirect objects + ((COSDictionary) cosBase).resetObjectKeys(indirectObjects); } else if (cosBase instanceof COSArray) { - // descend to included array to collect all included indirect objects - ((COSArray) cosBase).getIndirectObjectKeys(indirectObjects); + // descend to included array to reset all included indirect objects + ((COSArray) cosBase).resetObjectKeys(indirectObjects); } - else if (cosBaseKey != null) + else if (indirectObjectKey != null) { // add key for all indirect objects other than COSDictionary/COSArray - indirectObjects.add(cosBaseKey); + indirectObjects.add(indirectObjectKey); } } + return indirectObjects; } + // wrap indirect objects + private COSBase maybeWrap(COSBase object) + { + COSBase objectToAdd = object; + if ((object instanceof COSDictionary || object instanceof COSArray) && !object.isDirect() + && object.getKey() != null) + { + objectToAdd = new COSObject(object, object.getKey()); + } + return objectToAdd; + } } diff --git a/library/src/main/java/com/ainceborn/pdfbox/cos/COSDictionary.java b/library/src/main/java/com/ainceborn/pdfbox/cos/COSDictionary.java index 9ae7f6fa..0e41e188 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/cos/COSDictionary.java +++ b/library/src/main/java/com/ainceborn/pdfbox/cos/COSDictionary.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Collection; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -1424,20 +1425,28 @@ public COSUpdateState getUpdateState() return updateState; } + /** + * Reset all object keys to avoid overlapping numbers when saving the new pdf. + */ + public void resetImportedObjectKeys() + { + resetObjectKeys(new HashSet<>()).clear(); + } + /** * Collects all indirect objects numbers within this dictionary and all included dictionaries. It is used to avoid - * mixed up object numbers when importing an existing page to another pdf. + * overlapping object numbers when importing an existing page to another pdf. * * Expert use only. You might run into an endless recursion if choosing a wrong starting point. * * @param indirectObjects a collection of already found indirect objects. * */ - public void getIndirectObjectKeys(Collection indirectObjects) + protected Collection resetObjectKeys(Collection indirectObjects) { if (indirectObjects == null) { - return; + return indirectObjects; } COSObjectKey key = getKey(); if (key != null) @@ -1445,43 +1454,50 @@ public void getIndirectObjectKeys(Collection indirectObjects) // avoid endless recursions if (indirectObjects.contains(key)) { - return; - } - else - { - indirectObjects.add(key); + return indirectObjects; } + indirectObjects.add(key); + // reset object key + setKey(null); } for (Map.Entry entry : items.entrySet()) { COSBase cosBase = entry.getValue(); - COSObjectKey cosBaseKey = cosBase != null ? cosBase.getKey() : null; - // avoid endless recursions - if (COSName.PARENT.equals(entry.getKey()) - || (cosBaseKey != null && indirectObjects.contains(cosBaseKey))) - { - continue; - } - if (cosBase instanceof COSObject) + COSObjectKey indirectObjectKey = cosBase instanceof COSObject ? cosBase.getKey() : null; + if (indirectObjectKey != null) { - // dereference object + // avoid endless recursions + if (indirectObjects.contains(indirectObjectKey)) + { + continue; + } + // dereference object first cosBase = ((COSObject) cosBase).getObject(); + // reset object key + entry.getValue().setKey(null); } if (cosBase instanceof COSDictionary) { - // descend to included dictionary to collect all included indirect objects - ((COSDictionary) cosBase).getIndirectObjectKeys(indirectObjects); + COSName entryKey = entry.getKey(); + // descend to included dictionary to reset all included indirect objects + // skip PARENT and P references to avoid recursions + if (!COSName.PARENT.equals(entryKey) && !COSName.P.equals(entryKey)) + { + ((COSDictionary) cosBase).resetObjectKeys(indirectObjects); + } } else if (cosBase instanceof COSArray) { - // descend to included array to collect all included indirect objects - ((COSArray) cosBase).getIndirectObjectKeys(indirectObjects); + // descend to included array to reset all included indirect objects + ((COSArray) cosBase).resetObjectKeys(indirectObjects); } - else if (cosBaseKey != null) + else if (indirectObjectKey != null) { // add key for all indirect objects other than COSDictionary/COSArray - indirectObjects.add(cosBaseKey); + indirectObjects.add(indirectObjectKey); } } + return indirectObjects; } + } diff --git a/library/src/main/java/com/ainceborn/pdfbox/filter/CCITTFaxFilter.java b/library/src/main/java/com/ainceborn/pdfbox/filter/CCITTFaxFilter.java index c1e05ea5..969f4d70 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/filter/CCITTFaxFilter.java +++ b/library/src/main/java/com/ainceborn/pdfbox/filter/CCITTFaxFilter.java @@ -37,7 +37,7 @@ final class CCITTFaxFilter extends Filter { @Override public DecodeResult decode(InputStream encoded, OutputStream decoded, - COSDictionary parameters, int index) throws IOException + COSDictionary parameters, int index) throws IOException { // get decode parameters COSDictionary decodeParms = getDecodeParams(parameters, index); @@ -60,33 +60,82 @@ public DecodeResult decode(InputStream encoded, OutputStream decoded, // decompress data int k = decodeParms.getInt(COSName.K, 0); boolean encodedByteAlign = decodeParms.getBoolean(COSName.ENCODED_BYTE_ALIGN, false); - int arraySize = (cols + 7) / 8 * rows; - // TODO possible options?? + if (cols <= 0 || rows <= 0) + { + throw new IOException("Invalid CCITT image dimensions: cols=" + cols + ", rows=" + rows); + } + + long arraySizeLong = ((long) cols + 7) / 8 * rows; + + long maxBytes = 256 * 1024 * 1024L; + String sysProp = System.getProperty(Filter.SYSPROP_CCITTFAX_MAXBYTES); + + if (sysProp != null) + { + try + { + long parsed = Long.parseLong(sysProp); + if (parsed > 0) + { + maxBytes = parsed; + } + // else ignore zero/negative values + } + catch (NumberFormatException e) + { + // ignore invalid value, keep default + } + } + + if (arraySizeLong > maxBytes) + { + throw new IOException( + "CCITT decode buffer too large (" + arraySizeLong + " bytes) for cols=" + cols + + ", rows=" + rows + "; max allowed=" + maxBytes + + "; increase " + Filter.SYSPROP_CCITTFAX_MAXBYTES + " to override" + ); + } + + int arraySize = (int) arraySizeLong; byte[] decompressed = new byte[arraySize]; CCITTFaxDecoderStream s; int type; long tiffOptions = 0; if (k == 0) { - type = TIFFExtension.COMPRESSION_CCITT_T4; // Group 3 1D - byte[] streamData = new byte[20]; - int bytesRead = encoded.read(streamData); - PushbackInputStream pushbackInputStream = new PushbackInputStream(encoded, streamData.length); - pushbackInputStream.unread(streamData, 0, bytesRead); - encoded = pushbackInputStream; - if (streamData[0] != 0 || (streamData[1] >> 4 != 1 && streamData[1] != 1)) + if (decodeParms.containsKey(COSName.END_OF_LINE)) + { + // PDFBOX-6080: respect the parameter if it exists + boolean hasEndOfLine = decodeParms.getBoolean(COSName.END_OF_LINE, false); + type = hasEndOfLine ? TIFFExtension.COMPRESSION_CCITT_T4 : TIFFExtension.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE; + } + else { - // leading EOL (0b000000000001) not found, search further and try RLE if not - // found - type = TIFFExtension.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE; - short b = (short) (((streamData[0] << 8) + (streamData[1] & 0xff)) >> 4); - for (int i = 12; i < bytesRead * 8; i++) + // In twelvemonkeys, this part is found in CCITTFaxDecoderStream.findCompressionType() + // needed for 015315-p8-ccitt.pdf, PDFBOX-2123-1bit.pdf, PDFBOX-2778.pdf + type = TIFFExtension.COMPRESSION_CCITT_T4; // Group 3 1D + byte[] streamData = new byte[20]; + int bytesRead = encoded.read(streamData); + if (bytesRead == -1) + { + throw new IOException("EOF while reading CCITT header"); + } + PushbackInputStream pushbackInputStream = new PushbackInputStream(encoded, streamData.length); + pushbackInputStream.unread(streamData, 0, bytesRead); + encoded = pushbackInputStream; + if (streamData[0] != 0 || (streamData[1] >> 4 != 1 && streamData[1] != 1)) { - b = (short) ((b << 1) + ((streamData[(i / 8)] >> (7 - (i % 8))) & 0x01)); - if ((b & 0xFFF) == 1) + // leading EOL (0b000000000001) not found, search further and try RLE if not found + type = TIFFExtension.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE; + short b = (short) (((streamData[0] << 8) + (streamData[1] & 0xff)) >> 4); + for (int i = 12; i < bytesRead * 8; i++) { - type = TIFFExtension.COMPRESSION_CCITT_T4; - break; + b = (short) ((b << 1) + ((streamData[(i / 8)] >> (7 - (i % 8))) & 0x01)); + if ((b & 0xFFF) == 1) + { + type = TIFFExtension.COMPRESSION_CCITT_T4; + break; + } } } } diff --git a/library/src/main/java/com/ainceborn/pdfbox/filter/Filter.java b/library/src/main/java/com/ainceborn/pdfbox/filter/Filter.java index ac326e80..8bb86ecb 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/filter/Filter.java +++ b/library/src/main/java/com/ainceborn/pdfbox/filter/Filter.java @@ -54,6 +54,16 @@ public abstract class Filter */ public static final String SYSPROP_DEFLATELEVEL = "com.ainceborn.pdfbox.filter.deflatelevel"; + /** + * CCITTFax decode buffer size cap System Property. Sets the maximum number of bytes that + * CCITTFaxFilter is allowed to pre-allocate for a single image decode buffer. PDF-controlled + * /Columns and /Rows values are validated against this limit before allocation to prevent + * denial-of-service via crafted image dimensions. The default is 256 MB. To raise the cap for + * high-resolution legitimate documents, use + * {@code System.setProperty(Filter.SYSPROP_CCITTFAX_MAXBYTES, String.valueOf(512 * 1024 * 1024L));} + */ + public static final String SYSPROP_CCITTFAX_MAXBYTES = "com.ainceborn.pdfbox.filter.ccittmaxbytes"; + /** * Constructor. */ diff --git a/library/src/main/java/com/ainceborn/pdfbox/multipdf/PDFMergerUtility.java b/library/src/main/java/com/ainceborn/pdfbox/multipdf/PDFMergerUtility.java index abc13239..754294aa 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/multipdf/PDFMergerUtility.java +++ b/library/src/main/java/com/ainceborn/pdfbox/multipdf/PDFMergerUtility.java @@ -1350,7 +1350,8 @@ private void mergeOutputIntents(PDDocumentCatalog srcCatalog, PDDocumentCatalog boolean skip = false; for (PDOutputIntent dstOI : dstOutputIntents) { - if (dstOI.getOutputConditionIdentifier().equals(srcOCI)) + // PDFBOX-6173: reverse to avoid NPE + if (srcOCI.equals(dstOI.getOutputConditionIdentifier())) { skip = true; break; diff --git a/library/src/main/java/com/ainceborn/pdfbox/multipdf/Splitter.java b/library/src/main/java/com/ainceborn/pdfbox/multipdf/Splitter.java index 416f9b69..452e9ad7 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/multipdf/Splitter.java +++ b/library/src/main/java/com/ainceborn/pdfbox/multipdf/Splitter.java @@ -20,20 +20,49 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import com.ainceborn.pdfbox.cos.COSArray; import com.ainceborn.pdfbox.cos.COSBase; import com.ainceborn.pdfbox.cos.COSDictionary; +import com.ainceborn.pdfbox.cos.COSInteger; import com.ainceborn.pdfbox.cos.COSName; +import com.ainceborn.pdfbox.cos.COSObject; import com.ainceborn.pdfbox.io.MemoryUsageSetting; +import com.ainceborn.pdfbox.io.RandomAccessStreamCache; +import com.ainceborn.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction; import com.ainceborn.pdfbox.pdmodel.PDDocument; +import com.ainceborn.pdfbox.pdmodel.PDDocumentCatalog; import com.ainceborn.pdfbox.pdmodel.PDDocumentInformation; import com.ainceborn.pdfbox.pdmodel.PDPage; +import com.ainceborn.pdfbox.pdmodel.PDPageTree; +import com.ainceborn.pdfbox.pdmodel.PDResources; +import com.ainceborn.pdfbox.pdmodel.PDStructureElementNameTreeNode; +import com.ainceborn.pdfbox.pdmodel.common.COSObjectable; +import com.ainceborn.pdfbox.pdmodel.common.PDNameTreeNode; +import com.ainceborn.pdfbox.pdmodel.common.PDNumberTreeNode; +import com.ainceborn.pdfbox.pdmodel.documentinterchange.logicalstructure.PDParentTreeValue; +import com.ainceborn.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; +import com.ainceborn.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; +import com.ainceborn.pdfbox.pdmodel.graphics.PDXObject; +import com.ainceborn.pdfbox.pdmodel.graphics.form.PDFormXObject; +import com.ainceborn.pdfbox.pdmodel.graphics.image.PDImageXObject; import com.ainceborn.pdfbox.pdmodel.interactive.action.PDAction; +import com.ainceborn.pdfbox.pdmodel.interactive.action.PDActionFactory; import com.ainceborn.pdfbox.pdmodel.interactive.action.PDActionGoTo; import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAnnotationMarkup; +import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAnnotationPopup; +import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; +import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import com.ainceborn.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination; +import com.ainceborn.pdfbox.pdmodel.interactive.documentnavigation.destination.PDNamedDestination; import com.ainceborn.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination; /** @@ -41,6 +70,7 @@ * * @author Mario Ivankovits * @author Ben Litchfield + * @author Tilman Hausherr */ public class Splitter { @@ -51,27 +81,35 @@ public class Splitter private int startPage = Integer.MIN_VALUE; private int endPage = Integer.MAX_VALUE; private List destinationDocuments; + private Map pageDictMap; // map old page => new page for the current destination document + private List> pageDictMaps; // list of these maps for all destination documents + private Map structDictMap; + private List> annotDictMaps; // map old annotation => new annotation for the current destination document + private Map annotDictMap; // list of these maps for all destination documents + private Map destToFixMap; + private Set idSet; + private Set roleSet; private int currentPageNumber; - private MemoryUsageSetting memoryUsageSetting = null; + private StreamCacheCreateFunction streamCacheCreateFunction = null; /** - * @return the current memory setting. + * @return the current function to be used to create an instance of stream cache. */ - public MemoryUsageSetting getMemoryUsageSetting() + public StreamCacheCreateFunction getStreamCacheCreateFunction() { - return memoryUsageSetting; + return streamCacheCreateFunction; } /** - * Set the memory setting. + * Set the current function to be used to create an instance of stream cache. * - * @param memoryUsageSetting The memory setting. + * @param streamCacheCreateFunction the current function to be used to create an instance of stream cache. */ - public void setMemoryUsageSetting(MemoryUsageSetting memoryUsageSetting) + public void setStreamCacheCreateFunction(StreamCacheCreateFunction streamCacheCreateFunction) { - this.memoryUsageSetting = memoryUsageSetting; + this.streamCacheCreateFunction = streamCacheCreateFunction; } /** @@ -90,12 +128,501 @@ public List split(PDDocument document) throws IOException { // reset the currentPageNumber for a case if the split method will be used several times currentPageNumber = 0; - destinationDocuments = new ArrayList(); + destinationDocuments = new ArrayList<>(); sourceDocument = document; + pageDictMaps = new ArrayList<>(); + annotDictMaps = new ArrayList<>(); + destToFixMap = new HashMap<>(); + idSet = new HashSet<>(); + roleSet = new HashSet<>(); + processPages(); + + for (int i = 0; i < destinationDocuments.size(); ++i) + { + PDDocument destinationDocument = destinationDocuments.get(i); + pageDictMap = pageDictMaps.get(i); + annotDictMap = annotDictMaps.get(i); + cloneStructureTree(destinationDocument); + fixDestinations(destinationDocument); + } + return destinationDocuments; } + /** + * Replace the page destinations, if the source and destination pages are in the target + * document. This must be called after all pages (and its annotations) are processed. + * + * @param destinationDocument + */ + private void fixDestinations(PDDocument destinationDocument) + { + PDPageTree pageTree = destinationDocument.getPages(); + for (Map.Entry entry : destToFixMap.entrySet()) + { + PDPageDestination pageDestination = entry.getKey(); + // Find whether source page is inside or outside + PDPage srcPage = entry.getValue(); + if (pageTree.indexOf(srcPage) < 0) + { + continue; + } + COSDictionary srcPageDict = pageDestination.getPage().getCOSObject(); + COSDictionary dstPageDict = pageDictMap.get(srcPageDict); + PDPage dstPage = new PDPage(dstPageDict); + // Find whether destination page is inside or outside + if (pageTree.indexOf(dstPage) >= 0) + { + pageDestination.setPage(dstPage); + } + else + { + pageDestination.setPage(null); + } + } + } + + /** + * Clone the structure tree from the source to the current destination document. This must be + * called after all pages are processed. + * + * @param destinationDocument + * @throws IOException + */ + private void cloneStructureTree(PDDocument destinationDocument) throws IOException + { + PDStructureTreeRoot srcStructureTreeRoot = sourceDocument.getDocumentCatalog().getStructureTreeRoot(); + if (srcStructureTreeRoot == null) + { + return; + } + structDictMap = new HashMap<>(); + PDStructureTreeRoot dstStructureTreeRoot = new PDStructureTreeRoot(); + PDPageTree dstPageTree = destinationDocument.getPages(); + + // clone /K, also fills dictMap + COSBase k1 = srcStructureTreeRoot.getK(); + COSBase k2 = new KCloner(dstPageTree).createClone(k1, dstStructureTreeRoot.getCOSObject(), null); + dstStructureTreeRoot.setK(k2); + + // transfer ParentTree using the map because the dictionaries are all found in the /K structure. + PDNumberTreeNode srcParentTree = srcStructureTreeRoot.getParentTree(); + Map srcNumberTreeAsMap = PDFMergerUtility.getNumberTreeAsMap(srcParentTree); + Map dstNumberTreeAsMap = new LinkedHashMap<>(); + int dstPageTreeCount = dstPageTree.getCount(); + for (int p = 0; p < dstPageTreeCount; ++p) + { + PDPage page = dstPageTree.get(p); + int sp1 = page.getStructParents(); + if (sp1 != -1) + { + cloneTreeElement(srcNumberTreeAsMap, dstNumberTreeAsMap, sp1); + } + for (PDAnnotation ann : page.getAnnotations()) + { + int sp2 = ann.getStructParent(); + if (sp2 != -1) + { + cloneTreeElement(srcNumberTreeAsMap, dstNumberTreeAsMap, sp2); + } + PDAppearanceStream normalAppearanceStream = ann.getNormalAppearanceStream(); + if (normalAppearanceStream != null) + { + processResources(normalAppearanceStream.getResources(), srcNumberTreeAsMap, dstNumberTreeAsMap, new HashSet<>()); + } + } + processResources(page.getResources(), srcNumberTreeAsMap, dstNumberTreeAsMap, new HashSet<>()); + } + PDNumberTreeNode dstNumberTreeNode = new PDNumberTreeNode(PDParentTreeValue.class); + dstNumberTreeNode.setNumbers(dstNumberTreeAsMap); + dstStructureTreeRoot.setParentTree(dstNumberTreeNode); + Integer upperLimit = dstNumberTreeNode.getUpperLimit(); + if (upperLimit != null) + { + dstStructureTreeRoot.setParentTreeNextKey(upperLimit + 1); + } + dstStructureTreeRoot.setClassMap(srcStructureTreeRoot.getClassMap()); + cloneRoleMap(srcStructureTreeRoot, dstStructureTreeRoot); + cloneIDTree(srcStructureTreeRoot, dstStructureTreeRoot); + + destinationDocument.getDocumentCatalog().setStructureTreeRoot(dstStructureTreeRoot); + } + + private void cloneIDTree(PDStructureTreeRoot srcStructTree, PDStructureTreeRoot destStructTree) + throws IOException + { + PDNameTreeNode srcIDTree = srcStructTree.getIDTree(); + if (srcIDTree == null) + { + return; + } + Map srcIDTreeAsMap = PDFMergerUtility.getIDTreeAsMap(srcIDTree); + Map destNames = new HashMap<>(); + srcIDTreeAsMap.forEach((key, val) -> + { + if (!idSet.contains(key)) + { + return; + } + COSDictionary dstDict = structDictMap.get(val.getCOSObject()); + if (dstDict != null) + { + destNames.put(key, new PDStructureElement(dstDict)); + } + }); + PDNameTreeNode destIDTree = new PDStructureElementNameTreeNode(); + destIDTree.setNames(destNames); + destStructTree.setIDTree(destIDTree); + // See comment at the end of PDFMergerUtility.mergeIDTree() + } + + // needed because getRoleMap() and setRoleMap() habe different map types?! + private void cloneRoleMap(PDStructureTreeRoot srcStructTree, PDStructureTreeRoot destStructTree) + { + COSDictionary srcDict = srcStructTree.getCOSObject().getCOSDictionary(COSName.ROLE_MAP); + if (srcDict == null) + { + return; + } + COSDictionary dstDict = new COSDictionary(); + for (Map.Entry entry : srcDict.entrySet()) + { + if (roleSet.contains(entry.getKey())) + { + dstDict.setItem(entry.getKey(), entry.getValue()); + } + } + destStructTree.getCOSObject().setItem(COSName.ROLE_MAP, dstDict); + } + + // clone tree element using the map so that structure elements are replaced + private void cloneTreeElement( + Map srcNumberTreeAsMap, + Map dstNumberTreeAsMap, + int sp) + { + COSObjectable srcObj = srcNumberTreeAsMap.get(sp); // this is a PDParentTreeValue class + COSObjectable dstObj = null; + if (srcObj != null) + { + COSBase actualSrcObj = srcObj.getCOSObject(); + // structure element or array + if (actualSrcObj instanceof COSArray) + { + // create a clone of the array + COSArray srcArray = (COSArray) actualSrcObj; + COSArray dstArray = new COSArray(); + for (int i = 0; i < srcArray.size(); ++i) + { + COSBase srcElement = srcArray.getObject(i); + dstArray.add(structDictMap.get(srcElement)); // may be null + } + dstObj = dstArray; + } + else if (actualSrcObj instanceof COSDictionary) + { + // get the clone from the map + dstObj = structDictMap.get(actualSrcObj); + if (dstObj == null) + { + // 164421.pdf, structure tree is weird. + // also 250052.pdf, 250198.pdf, 257012.pdf, 271459.pdf (multiple), + // 670045.pdf (multiple) + // In 71459.pdf annotations on page 1 have StructParent numbers + // that point to structure elements in the /ParentTree that point to + // a different page. + Log.w("PdfBox-Android", "ParentTree index " + sp + " dictionary not found in /K"); + } + } + else + { + Log.w("PdfBox-Android", + "ParentTree index " + + sp + + " tree element neither dictionary nor array, but " + + (actualSrcObj == null ? "(null)" : actualSrcObj.getClass().getSimpleName())) + ; + } + if (dstObj != null) + { + dstNumberTreeAsMap.put(sp, dstObj); + } + } + } + + /** + * Class to help clone the /K tree. It clones structure elements and fills the structure + * elements map. Pages are replaced with the help of the page map. Elements with pages that + * don't belong to the destination are removed from the clone. + */ + private class KCloner + { + PDPageTree dstPageTree; + + KCloner(PDPageTree dstPageTree) + { + this.dstPageTree = dstPageTree; + } + + /** + * Creates a clone of the source. + * + * @param src source dictionary or array. + * @param dstParent for the /P entry; parameter needed because arrays don't keep a parent. + * @param currentPageDict used to remember whether we have a page parent somewhere or not. + * Starts with null. + * @return a clone, or null if source is null or if there is no clone because it belongs to a + * different page or to no page. + */ + COSBase createClone(COSBase src, COSBase dstParent, COSDictionary currentPageDict) + { + if (src instanceof COSArray) + { + return createArrayClone(src, dstParent, currentPageDict); + } + else if (src instanceof COSDictionary) + { + return createDictionaryClone(src, dstParent, currentPageDict); + } + else + { + return src; + } + } + + private COSBase createArrayClone(COSBase src, COSBase dstParent, COSDictionary currentPageDict) + { + COSArray dst = new COSArray(); + for (COSBase base2 : (COSArray) src) + { + COSBase rc; + if (base2 instanceof COSObject) + { + rc = createClone(((COSObject) base2).getObject(), dstParent, currentPageDict); + } + else + { + rc = createClone(base2, dstParent, currentPageDict); + } + // if this is null then they don't belong to the destination document + if (rc != null) + { + dst.add(rc); + } + } + return dst.isEmpty() ? null : dst; + } + + private COSBase createDictionaryClone(COSBase src, COSBase dstParent, COSDictionary currentPageDict) + { + COSDictionary srcDict = (COSDictionary) src; + COSDictionary dstDict = structDictMap.get(srcDict); + if (dstDict != null) + { + return dstDict; + } + COSDictionary srcPageDict = srcDict.getCOSDictionary(COSName.PG); + COSDictionary dstPageDict = null; + COSBase kid = srcDict.getDictionaryObject(COSName.K); + COSName type = srcDict.getCOSName(COSName.TYPE); + if (srcPageDict != null) + { + dstPageDict = pageDictMap.get(srcPageDict); + if (dstPageDict != null) + { + PDPage dstPage = new PDPage(dstPageDict); + if (dstPageTree.indexOf(dstPage) == -1) + { + return null; + } + } + else + { + // PDFBOX-6009: "wrong" /Pg entry + // quit if MCIDs because these need a /Pg entry + // or if MCR/OBJR dicts + if (COSName.MCR.equals(type) || COSName.OBJR.equals(type) || hasMCIDs(kid)) + { + return null; + } + // else keep this as an intermediate element for now + } + } + + // special handling for MCR items ("marked-content reference dictionary") + if (COSName.MCR.equals(type) && dstPageDict == null && + dstParent instanceof COSDictionary && ((COSDictionary) dstParent).getCOSDictionary(COSName.PG) == null) + { + // PAC: Pg entry of marked-content reference and of parent structure is null + return null; + } + + // Create and fill clone + dstDict = new COSDictionary(); + structDictMap.put(srcDict, dstDict); + for (Map.Entry entry : srcDict.entrySet()) + { + COSName key = entry.getKey(); + if (!COSName.K.equals(key) && + !COSName.PG.equals(key) && + !COSName.P.equals(key)) + { + dstDict.setItem(key, entry.getValue()); + } + } + + // special handling for OBJR items ("object reference dictionary") + // see e.g. file 488300.pdf and Root/StructTreeRoot/K/K/[2]/K/[1]/K/[0]/Obj + if (COSName.OBJR.equals(type)) + { + COSDictionary srcObj = srcDict.getCOSDictionary(COSName.OBJ); + COSDictionary dstObj = annotDictMap.get(srcObj); + if (dstObj != null) + { + // replace annotation with clone + dstDict.setItem(COSName.OBJ, dstObj); + } + else if (srcObj != null) // 079177.pdf + { + removePossibleOrphanAnnotation(srcObj, srcDict, currentPageDict, dstDict); + } + if (dstDict.size() == 1) + { + return null; + } + if (dstPageDict == null && + dstParent instanceof COSDictionary && ((COSDictionary) dstParent).getCOSDictionary(COSName.PG) == null) + { + // Pg entry of object reference dictionary and of parent structure is null + return null; + } + } + + if (!COSName.OBJR.equals(type) && !COSName.MCR.equals(type)) + { + // /P not needed for OBJR or MCR items + dstDict.setItem(COSName.P, dstParent); + } + + dstDict.setItem(COSName.PG, dstPageDict); + + // stack overflow here with 207658.pdf and 113484.pdf, too complex; works with -Xss50m + COSBase cloneKid = createClone(kid, dstDict, dstPageDict != null ? dstPageDict : currentPageDict); + if (cloneKid == null && kid != null) + { + return null; // kids array wasn't empty, but is empty now => ignore + } + + // removes orphan nodes, example: + // Root/StructTreeRoot/K/[7]/K/[3]/K/[5]/K/[2] in 271459.pdf + // decide about keeping source dictionaries with no /K and no /PG + if (dstPageDict == null && cloneKid == null && currentPageDict == null) + { + // if no parent page and no page here and no kids, assume this is an orphan + return null; + } + dstDict.setItem(COSName.K, cloneKid); + String id = dstDict.getString(COSName.ID); + if (id != null) + { + idSet.add(id); + } + COSName s = dstDict.getCOSName(COSName.S); + if (s != null) + { + roleSet.add(s); + } + return dstDict; + } + + private boolean hasMCIDs(COSBase kid) + { + if (kid instanceof COSInteger) + { + return true; + } + if (kid instanceof COSArray) + { + COSArray ar = (COSArray) kid; + for (int i = 0; i < ar.size(); ++i) + { + if (ar.getObject(i) instanceof COSInteger) + { + return true; + } + } + } + return false; + } + + private void removePossibleOrphanAnnotation(COSDictionary srcObj, COSDictionary srcDict, + COSDictionary currentPageDict, COSDictionary dstDict) + { + // PDFBOX-5929: Check whether this is an "orphan" annotation that isn't in the page + COSBase objType = srcObj.getDictionaryObject(COSName.TYPE); + COSBase objSubtype = srcObj.getDictionaryObject(COSName.SUBTYPE); + if (COSName.ANNOT.equals(objType) || COSName.LINK.equals(objSubtype)) + { + COSDictionary srcPageDict = srcDict.getCOSDictionary(COSName.PG); + if (srcPageDict == null) + { + // /Pg entry is not always on this level + srcPageDict = currentPageDict; + } + if (srcPageDict != null) + { + COSArray annotationArray = srcPageDict.getCOSArray(COSName.ANNOTS); + if (annotationArray == null || annotationArray.indexOfObject(srcObj) == -1) + { + // Ideally the entire OBJR entry should be removed. + // Removing the OBJ entry is done to avoid potential page orphans + // from the annotation destination. + Log.w("PdfBox-Android", "An annotation OBJ that isn't in the page has been removed from the structure tree"); + dstDict.removeItem(COSName.OBJ); + } + } + } + } + } + + // Look for /StructParent and /StructParents and add them to the destination tree + private void processResources(PDResources res, + Map srcNumberTreeAsMap, + Map dstNumberTreeAsMap, + Set visited) throws IOException + { + if (res == null) + { + return; + } + if (visited.contains(res.getCOSObject())) + { + // avoid endless recursion, e.g. with 002874.pdf + return; + } + visited.add(res.getCOSObject()); + + for (COSName name : res.getXObjectNames()) + { + PDXObject xObject = res.getXObject(name); + int sp2 = -1; + if (xObject instanceof PDFormXObject) + { + sp2 = ((PDFormXObject) xObject).getStructParents(); + processResources(((PDFormXObject) xObject).getResources(), srcNumberTreeAsMap, dstNumberTreeAsMap, visited); + } + else if (xObject instanceof PDImageXObject) + { + sp2 = ((PDImageXObject) xObject).getStructParent(); + } + if (sp2 != -1) + { + cloneTreeElement(srcNumberTreeAsMap, dstNumberTreeAsMap, sp2); + } + } + } + /** * This will tell the splitting algorithm where to split the pages. The default * is 1, so every page will become a new document. If it was two then each document would @@ -134,7 +661,7 @@ public void setStartPage(int start) * This will set the end page. * * @param end the 1-based end page - * @throws IllegalArgumentException if the end page is smaller than one. + * @throws IllegalArgumentException if the end page is smaller than one or than the start page. */ public void setEndPage(int end) { @@ -142,6 +669,10 @@ public void setEndPage(int end) { throw new IllegalArgumentException("End page is smaller than one"); } + if (end < startPage) + { + throw new IllegalArgumentException("End page is smaller than startPage"); + } endPage = end; } @@ -184,6 +715,10 @@ private void createNewDocumentIfNecessary() throws IOException { currentDestinationDocument = createNewDocument(); destinationDocuments.add(currentDestinationDocument); + pageDictMap = new HashMap<>(); + pageDictMaps.add(pageDictMap); + annotDictMap = new HashMap<>(); + annotDictMaps.add(annotDictMap); } } @@ -210,13 +745,12 @@ protected boolean splitAtPage(int pageNumber) /** * Create a new document to write the split contents to. * - * @return the newly created PDDocument. + * @return the newly created PDDocument. * @throws IOException If there is an problem creating the new document. */ protected PDDocument createNewDocument() throws IOException { - PDDocument document = memoryUsageSetting == null ? - new PDDocument() : new PDDocument(memoryUsageSetting.streamCache); + PDDocument document = streamCacheCreateFunction != null ? new PDDocument(streamCacheCreateFunction) : new PDDocument(); document.getDocument().setVersion(getSourceDocument().getVersion()); PDDocumentInformation sourceDocumentInformation = getSourceDocument().getDocumentInformation(); if (sourceDocumentInformation != null) @@ -233,7 +767,7 @@ protected PDDocument createNewDocument() throws IOException Log.w("PdfBox-Android", "Nested entry for key '" + key.getName() + "' skipped in document information dictionary"); if (sourceDocument.getDocumentCatalog().getCOSObject() == - sourceDocument.getDocumentInformation().getCOSObject()) + sourceDocument.getDocumentInformation().getCOSObject()) { Log.w("PdfBox-Android", "/Root and /Info share the same dictionary"); } @@ -247,8 +781,14 @@ protected PDDocument createNewDocument() throws IOException } document.setDocumentInformation(new PDDocumentInformation(destDocumentInformationDictionary)); } - document.getDocumentCatalog().setViewerPreferences( - getSourceDocument().getDocumentCatalog().getViewerPreferences()); + PDDocumentCatalog destCatalog = document.getDocumentCatalog(); + PDDocumentCatalog sourceCatalog = getSourceDocument().getDocumentCatalog(); + destCatalog.setViewerPreferences(sourceCatalog.getViewerPreferences()); + destCatalog.setLanguage(sourceCatalog.getLanguage()); + destCatalog.setMarkInfo(sourceCatalog.getMarkInfo()); + destCatalog.setMetadata(sourceCatalog.getMetadata()); + // reset reused object keys to avoid gaps in the xref table + destCatalog.getCOSObject().resetImportedObjectKeys(); return document; } @@ -269,34 +809,177 @@ protected void processPage(PDPage page) throws IOException imported.setResources(page.getResources()); Log.i("PdfBox-Android", "Resources imported in Splitter"); // follow-up to warning in importPage } - // remove page links to avoid copying not needed resources + if (imported.getCOSObject().containsKey(COSName.B)) + { + imported.getCOSObject().removeItem(COSName.B); + Log.i("PdfBox-Android", "/B entry (beads) removed by splitter"); + } + // remove page links to avoid copying not needed resources processAnnotations(imported); + + pageDictMap.put(page.getCOSObject(), imported.getCOSObject()); } + /** + * Clone all annotations because of changes possibly made, and because the structure tree is + * cloned. + * + * @param imported + * @throws IOException + */ private void processAnnotations(PDPage imported) throws IOException { List annotations = imported.getAnnotations(); + if (annotations.isEmpty()) + { + return; + } + List clonedAnnotations = new ArrayList<>(annotations.size()); for (PDAnnotation annotation : annotations) { - if (annotation instanceof PDAnnotationLink) + // create a shallow clone + COSDictionary clonedDict = new COSDictionary(annotation.getCOSObject()); + PDAnnotation annotationClone = PDAnnotation.createAnnotation(clonedDict); + annotDictMap.put(annotation.getCOSObject(), clonedDict); + clonedAnnotations.add(annotationClone); + if (annotationClone instanceof PDAnnotationLink) + { + PDAnnotationLink link = (PDAnnotationLink) annotationClone; + PDDestination srcDestination = null; + try + { + srcDestination = link.getDestination(); + } + catch (IOException ex) + { + Log.w("PdfBox-Android", + "Incorrect destination in link annotation on page " + + currentPageNumber + + 1 + " is removed", ex + ); + link.setDestination(null); + } + PDAction action = null; + if (srcDestination == null) + { + action = link.getAction(); + if (action instanceof PDActionGoTo) + { + PDActionGoTo goToAction = (PDActionGoTo) action; + try + { + srcDestination = goToAction.getDestination(); + } + catch (IOException ex) + { + Log.i("PdfBox-Android", + "GoToAction with incorrect destination in link annotation on page " + + currentPageNumber + + 1 + " is removed", ex + ); + link.setAction(null); + } + } + } + if (srcDestination instanceof PDNamedDestination) + { + srcDestination = sourceDocument.getDocumentCatalog(). + findNamedDestinationPage((PDNamedDestination) srcDestination); + // we do not use the named destination anymore because names get modified, e.g. + // 0xAD becomes 0, see file 410609.pdf where the name no longer matches with the + // entry in the new name tree; plus the original solution was 40 additional loc + } + if (srcDestination instanceof PDPageDestination) + { + // preserve links to pages within the split result: + // not fully possible here because we don't have the full target document yet. + // However we're cloning as needed and remember what to do later. + PDPage destinationPage = ((PDPageDestination) srcDestination).getPage(); + if (destinationPage != null) + { + // clone destination + COSArray clonedDestinationArray = + new COSArray(((PDPageDestination) srcDestination).getCOSObject().toList()); + PDPageDestination dstDestination = + (PDPageDestination) PDDestination.create(clonedDestinationArray); + + // remember the destination to adjust / remove page later + destToFixMap.put(dstDestination, imported); + + if (action != null) + { + // if action is not null, then the destination came from an action, + // thus clone action as well, then assign destination clone, then action + COSDictionary clonedActionDict = new COSDictionary(action.getCOSObject()); + PDActionGoTo dstAction = + (PDActionGoTo) PDActionFactory.createAction(clonedActionDict); + dstAction.setDestination(dstDestination); + link.setAction(dstAction); + } + else + { + // just assign destination clone + link.setDestination(dstDestination); + } + } + } + } + if (annotationClone instanceof PDAnnotationWidget && + annotationClone.getCOSObject().containsKey(COSName.PARENT)) + { + // remove non-terminal field /Parent reference, because this may lead to orphan pages + annotationClone.getCOSObject().removeItem(COSName.PARENT); + } + if (annotation.getPage() != null) + { + annotationClone.setPage(imported); + } + } + // Second loop for markup and popup annotations, which reference annotations themselves + for (PDAnnotation annotation : clonedAnnotations) + { + if (annotation instanceof PDAnnotationMarkup) { - PDAnnotationLink link = (PDAnnotationLink)annotation; - PDDestination destination = link.getDestination(); - PDAction action = link.getAction(); - if (destination == null && action instanceof PDActionGoTo) + PDAnnotationPopup annotationPopup = ((PDAnnotationMarkup) annotation).getPopup(); + if (annotationPopup == null) { - destination = ((PDActionGoTo) action).getDestination(); + continue; } - if (destination instanceof PDPageDestination) + COSDictionary clonedPopupDict = annotDictMap.get(annotationPopup.getCOSObject()); + if (clonedPopupDict != null) { - // TODO preserve links to pages within the split result - ((PDPageDestination) destination).setPage(null); + annotation.getCOSObject().setItem(COSName.POPUP, clonedPopupDict); + } + else + { + // orphan popup (not in annotation list); clone it and fix references + clonedPopupDict = new COSDictionary(annotationPopup.getCOSObject()); + annotDictMap.put(annotationPopup.getCOSObject(), clonedPopupDict); + PDAnnotationPopup annotationPopupClone = + (PDAnnotationPopup) PDAnnotation.createAnnotation(clonedPopupDict); + annotationPopupClone.setParent((PDAnnotationMarkup) annotation); + ((PDAnnotationMarkup) annotation).setPopup(annotationPopupClone); + if (annotationPopupClone.getPage() != null) + { + annotationPopupClone.setPage(imported); + } } } - // TODO preserve links to pages within the split result - annotation.setPage(null); + if (annotation instanceof PDAnnotationPopup) + { + PDAnnotationMarkup annotationMarkup = ((PDAnnotationPopup) annotation).getParent(); + if (annotationMarkup == null) + { + continue; + } + COSDictionary clonedMarkupDict = annotDictMap.get(annotationMarkup.getCOSObject()); + // clonedMarkupDict will be null if markup annotation is an orphan (not in annotation list) + annotation.getCOSObject().setItem(COSName.PARENT, clonedMarkupDict); + } } + imported.setAnnotations(clonedAnnotations); } + /** * The source PDF document. * diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdfwriter/COSWriter.java b/library/src/main/java/com/ainceborn/pdfbox/pdfwriter/COSWriter.java index 796695fd..26e99886 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdfwriter/COSWriter.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdfwriter/COSWriter.java @@ -744,13 +744,18 @@ private void doWriteXRefInc(COSDocument doc) throws IOException trailer.removeItem(COSName.PREV); } pdfxRefStream.addTrailerInfo(trailer); - // the size is the highest object number+1. we add one more - // for the xref stream object we are going to write - pdfxRefStream.setSize(number + 2); - - setStartxref(getStandardOutput().getPos()); - COSStream stream2 = pdfxRefStream.getStream(); - doWriteObject(stream2); + // Pre-assign the object key for the xref stream so it can be + // included in its own cross-reference data. Per PDF Reference + // §7.5.8, the /Size value must be one greater than the highest + // object number in the file, including the xref stream itself. + COSObjectKey xrefStreamKey = new COSObjectKey(++number, 0); + long xrefStreamOffset = getStandardOutput().getPos(); + setStartxref(xrefStreamOffset); + pdfxRefStream.addEntry(new NormalXReference(xrefStreamOffset, xrefStreamKey, null)); + pdfxRefStream.setSize(number + 1); + + COSStream xrefStream = pdfxRefStream.getStream(); + doWriteObject(xrefStreamKey, xrefStream); } } diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java new file mode 100644 index 00000000..bf927dc4 --- /dev/null +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java @@ -0,0 +1,49 @@ +/* + * 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.ainceborn.pdfbox.pdmodel; + +import java.io.IOException; + +public interface ContentStreamForGlyphLayoutInterface +{ + + /** + * Show the given glyphs at the specified positions + * + * @param glyphsAndPositions List of glyphs and positions + * @throws IOException if an IO error occurs + */ + void showGlyphsWithPositioning(GlyphsAndPositions glyphsAndPositions) throws IOException; + + /** + * Shows the glyphs for the given glyph codes + * + * @param glyphCodes Array of glyph codes of the content font + * @throws IOException if an I/O exception occurs + */ + void showGlyphCodes(int[] glyphCodes) throws IOException; + + /** + * Set the text rise value, i.e. move the baseline up or down. This is useful for drawing + * superscripts or subscripts. + * + * @param rise Specifies the distance, in unscaled text space units, to move the baseline up or + * down from its default location. 0 restores the default location. + * @throws IOException If the content stream could not be written. + */ + void setTextRise(float rise) throws IOException; +} diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java new file mode 100644 index 00000000..474370fb --- /dev/null +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java @@ -0,0 +1,28 @@ +package com.ainceborn.pdfbox.pdmodel; + +import com.ainceborn.pdfbox.pdmodel.font.PDFont; +import com.ainceborn.pdfbox.pdmodel.font.PDType0Font; + +import java.io.IOException; + +public interface GlyphLayoutProcessorInterface { + /** + * Checks if the font is supported + * + * @param font to be checked + * @return true if glyph layout is supported for this font and this font is a PDType0Font + */ + boolean supportsFont(PDFont font); + + /** + * Shows a text using glyph positioning (if needed) + * + * @param contentStream the content stream + * @param font to be used + * @param fontSize font size + * @param text text to show + * @throws IOException if an I/O exception occurs + * @throws IllegalArgumentException if glyphs are missing + */ + void showText(ContentStreamForGlyphLayoutInterface contentStream, PDType0Font font, float fontSize, String text) throws IOException; +} \ No newline at end of file diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/GlyphsAndPositions.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/GlyphsAndPositions.java new file mode 100644 index 00000000..c009a112 --- /dev/null +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/GlyphsAndPositions.java @@ -0,0 +1,98 @@ +/* + * 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.ainceborn.pdfbox.pdmodel; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Stores sublists of glyphs and positions in a list + * + * @author Volker Kunert + */ +public class GlyphsAndPositions +{ + + private final ArrayList list = new ArrayList<>(); + + /** + * Sublist to store adjacent glyphs + */ + public static class GlyphSubList extends ArrayList + { + } + + /** + * Adds a glyph + * + * @param glyph to be added + */ + public void add(Integer glyph) + { + Object last = !list.isEmpty() ? list.get(list.size() - 1) : null; + GlyphSubList glyphSubList; + if (!(last instanceof GlyphSubList)) + { + glyphSubList = new GlyphSubList(); + list.add(glyphSubList); + } + else + { + glyphSubList = (GlyphSubList) last; + } + glyphSubList.add(glyph); + } + + /** + * Add a position + * + * @param position to be added + */ + public void add(Float position) + { + list.add(position); + } + + /** + * Checks if the list is empty + * + * @return true if it is empty + */ + public boolean isEmpty() + { + return list.isEmpty(); + } + + /** + * Clears the list + */ + public void clear() + { + list.clear(); + } + + /** + * Converts GlyphsAndPositions to an array of objects (GlyphSubList and Float) + * + * @return the array + */ + public Object[] toArray() + { + return Collections.unmodifiableList(list).toArray(); + } +} diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDAbstractContentStream.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDAbstractContentStream.java index c865c35e..dc20e361 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDAbstractContentStream.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDAbstractContentStream.java @@ -18,6 +18,22 @@ import android.util.Log; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.text.NumberFormat; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + import com.ainceborn.fontbox.ttf.CmapLookup; import com.ainceborn.fontbox.ttf.gsub.GsubWorker; import com.ainceborn.fontbox.ttf.gsub.GsubWorkerFactory; @@ -54,29 +70,14 @@ import com.ainceborn.pdfbox.util.NumberFormatUtil; import com.ainceborn.pdfbox.util.StringUtil; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.text.NumberFormat; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - /** * Provides the ability to write to a content stream. * * @author Ben Litchfield */ -abstract class PDAbstractContentStream implements Closeable +abstract class PDAbstractContentStream implements ContentStreamForGlyphLayoutInterface, Closeable { + protected final PDDocument document; // may be null protected final OutputStream outputStream; @@ -84,6 +85,7 @@ abstract class PDAbstractContentStream implements Closeable protected boolean inTextMode = false; protected final Deque fontStack = new ArrayDeque<>(); + protected final Deque fontSizeStack = new ArrayDeque<>(); protected final Deque nonStrokingColorSpaceStack = new ArrayDeque<>(); protected final Deque strokingColorSpaceStack = new ArrayDeque<>(); @@ -94,6 +96,7 @@ abstract class PDAbstractContentStream implements Closeable private final Map gsubWorkers = new HashMap<>(); private final GsubWorkerFactory gsubWorkerFactory = new GsubWorkerFactory(); + private GlyphLayoutProcessorInterface glyphLayoutProcessor; /** * Create a new appearance stream. @@ -112,6 +115,16 @@ abstract class PDAbstractContentStream implements Closeable formatDecimal.setGroupingUsed(false); } + /** + * Sets the glyph layout processor + * + * @param glyphLayoutProcessor glyph layout processor + */ + public void setGlyphLayoutProcessor(GlyphLayoutProcessorInterface glyphLayoutProcessor) + { + this.glyphLayoutProcessor = glyphLayoutProcessor; + } + /** * Sets the maximum number of digits allowed for fractional numbers. * @@ -176,6 +189,16 @@ public void setFont(PDFont font, float fontSize) throws IOException fontStack.push(font); } + if (fontSizeStack.isEmpty()) + { + fontSizeStack.add(fontSize); + } + else + { + fontSizeStack.pop(); + fontSizeStack.push(fontSize); + } + // keep track of fonts which are configured for subsetting if (font.willBeSubset()) { @@ -230,6 +253,16 @@ else if (!font.isEmbedded() && !font.isStandard14()) */ public void showTextWithPositioning(Object[] textWithPositioningArray) throws IOException { + if (!inTextMode) + { + throw new IllegalStateException("Must call beginText() before showTextWithPositioning()"); + } + + if (fontStack.isEmpty()) + { + throw new IllegalStateException("Must call setFont() before showTextWithPositioning()"); + } + write("["); for (Object obj : textWithPositioningArray) { @@ -250,6 +283,49 @@ else if (obj instanceof Float) writeOperator(OperatorName.SHOW_TEXT_ADJUSTED); } + /** + * Show the given glyphs at the specified positions. This method is meant to be called from + * within a GlyphLayoutProcessorInterface implementation and only for PDType0Font. + * + * @param glyphsAndPositions List of glyphs and positions + * @throws IOException if an IO error occurs + * @throws IllegalStateException if the current font isn't a PDType0Font. + */ + @Override + public void showGlyphsWithPositioning(GlyphsAndPositions glyphsAndPositions) throws IOException + { + write("["); + + for (Object obj : glyphsAndPositions.toArray()) + { + if (obj instanceof GlyphsAndPositions.GlyphSubList) + { + GlyphsAndPositions.GlyphSubList glyphSubList = (GlyphsAndPositions.GlyphSubList) obj; + int[] intGlyphArray = new int[glyphSubList.size()]; + // Convert Type to int[] + for (int i = 0; i < intGlyphArray.length; i++) + { + intGlyphArray[i] = glyphSubList.get(i); + } + writeTextPDType0Font(intGlyphArray); + } + else if (obj instanceof Float) + { + writeOperand((Float) obj); + } + else + { + if (obj == null) + { + throw new NullPointerException("Argument contains null entry"); + } + throw new IllegalArgumentException("Argument must consist of array of Float and GlyphsAndPositions.GlyphSubList types, not " + obj.getClass().getName()); + } + } + write("] "); + writeOperator(OperatorName.SHOW_TEXT_ADJUSTED); + } + /** * Shows the given text at the location specified by the current text matrix. * @@ -259,30 +335,106 @@ else if (obj instanceof Float) */ public void showText(String text) throws IOException { - showTextInternal(text); + if (!inTextMode) + { + throw new IllegalStateException("Must call beginText() before showText()"); + } + if (fontStack.isEmpty()) + { + throw new IllegalStateException("Must call setFont() before showText()"); + } + if (fontSizeStack.isEmpty()) + { + throw new IllegalStateException("Font is set, but fontSize is not set"); + } + PDFont font = fontStack.peek(); + if (glyphLayoutProcessor != null && glyphLayoutProcessor.supportsFont(font)) + { + float fontSize = fontSizeStack.peek(); + glyphLayoutProcessor.showText(this, (PDType0Font) font, fontSize, text); + } + else + { + showTextInternal(text); + write(" "); + writeOperator(OperatorName.SHOW_TEXT); + } + } + + /** + * Shows the glyphs for the given glyph codes - only for PDType0Font + * + * @param glyphCodes Array of glyph codes of the content font + * @throws IOException if an I/O exception occurs + * @throws IllegalStateException if the current font isn't a PDType0Font. + */ + @Override + public void showGlyphCodes(int[] glyphCodes) throws IOException + { + writeTextPDType0Font(glyphCodes); write(" "); writeOperator(OperatorName.SHOW_TEXT); } /** - * Outputs a string using the correct encoding and subsetting as required. + * Outputs the given glyph codes - only for PDType0Font * - * @param text The Unicode text to show. + * @param glyphCodes The glyph codes to write * - * @throws IOException If an io exception occurs. + * @throws IOException in case of I/O error + * @throws IllegalStateException if the current font isn't a PDType0Font. */ - protected void showTextInternal(String text) throws IOException + protected void writeTextPDType0Font(int[] glyphCodes) throws IOException { if (!inTextMode) { - throw new IllegalStateException("Must call beginText() before showText()"); + throw new IllegalStateException("Must call beginText() before writeTextPDType0Font()"); } - if (fontStack.isEmpty()) { - throw new IllegalStateException("Must call setFont() before showText()"); + throw new IllegalStateException("Must call setFont() before writeTextPDType0Font()"); } + PDFont font = fontStack.peek(); + if (!(font instanceof PDType0Font)) + { + throw new IllegalStateException("Must be called with current font instance of PDType0Font"); + } + PDType0Font pdType0Font = (PDType0Font) font; + + // encode glyphs, update set of used glyphs + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Set glyphIds = new HashSet<>(); + for (int glyphCode : glyphCodes) + { + out.write(pdType0Font.encodeGlyphId(glyphCode)); + if (glyphCode < 0xFFFF) + { + glyphIds.add(glyphCode); + } + } + byte[] encodedText = out.toByteArray(); + + // add glyphs to subset + if (pdType0Font.willBeSubset()) + { + pdType0Font.addGlyphsToSubset(glyphIds); + } + // write encoded text and the PDF operator + COSWriter.writeString(encodedText, outputStream); + } + + /** + * Outputs a string using the correct encoding and subsetting as required. + *

+ * inTextMode and fontStack must already have already been checked before calling this method. + * + * @param text The Unicode text to show. + * + * @throws IOException If an io exception occurs. + */ + protected void showTextInternal(String text) throws IOException + { PDFont font = fontStack.peek(); // complex text layout @@ -1748,9 +1900,9 @@ else if (Character.isValidCodePoint(codePoint)) List glyphIdsAfterGsub = gsubWorker.applyTransforms(originalGlyphIds); for (Integer glyphId : glyphIdsAfterGsub) { - out.writeBytes(font.encodeGlyphId(glyphId)); + IOUtils.writeBytesCompat(out, font.encodeGlyphId(glyphId)); } return glyphIdsAfterGsub; } -} +} \ No newline at end of file diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDDocument.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDDocument.java index a25e4bec..43273fe1 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDDocument.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDDocument.java @@ -155,9 +155,6 @@ public class PDDocument implements Closeable // to make sure only one signature is added private boolean signatureAdded = false; - // cache for the key of all imported indirect objects - private final Collection indirectObjectKeys = new HashSet<>(); - /** * Creates an empty PDF document. * You need to add at least one page for the document to be valid. @@ -223,7 +220,7 @@ public PDDocument(COSDocument doc, RandomAccessRead source) * * @param doc The COSDocument that this document wraps. * @param source input representing the pdf - * @param permission he access permissions of the pdf + * @param permission the access permissions of the pdf * */ public PDDocument(COSDocument doc, RandomAccessRead source, AccessPermission permission) @@ -243,7 +240,6 @@ public PDDocument(COSDocument doc, RandomAccessRead source, AccessPermission per public void addPage(PDPage page) { getPages().add(page); - setHighestImportedObjectNumber(page); } /** @@ -706,6 +702,8 @@ public PDPage importPage(PDPage page) throws IOException importedPage.getCOSObject().removeItem(COSName.PARENT); PDStream dest = new PDStream(this, page.getContents(), COSName.FLATE_DECODE); importedPage.setContents(dest); + // reset imported object keys to avoid overlapping object numbers + importedPage.getCOSObject().resetImportedObjectKeys(); addPage(importedPage); importedPage.setCropBox(new PDRectangle(page.getCropBox().getCOSArray())); importedPage.setMediaBox(new PDRectangle(page.getMediaBox().getCOSArray())); @@ -718,21 +716,6 @@ public PDPage importPage(PDPage page) throws IOException return importedPage; } - /** - * Determine the highest object number from the imported page to avoid mixed up numbers when saving the new pdf. - * - * @param importedPage the imported page. - */ - private void setHighestImportedObjectNumber(PDPage importedPage) - { - importedPage.getCOSObject().getIndirectObjectKeys(indirectObjectKeys); - long highestImportedNumber = indirectObjectKeys.stream().map(COSObjectKey::getNumber) - .max(Long::compare).orElse(0L); - long highestXRefObjectNumber = getDocument().getHighestXRefObjectNumber(); - getDocument().setHighestXRefObjectNumber( - Math.max(highestXRefObjectNumber, highestImportedNumber)); - } - /** * This will get the low level document. * @@ -774,7 +757,7 @@ public PDDocumentInformation getDocumentInformation() *

* In PDF 2.0 this is deprecated except for two entries, /CreationDate and /ModDate. For any other * document level metadata, a metadata stream should be used instead, see - * {@link PDDocumentCatalog#setMetadata(PDMetadata) PDDocumentCatalog#setMetadata(PDMetadata)}. + * {@link PDDocumentCatalog#setMetadata(com.ainceborn.pdfbox.pdmodel.common.PDMetadata) PDDocumentCatalog#setMetadata(PDMetadata)}. * * @param info The updated document information. */ @@ -927,7 +910,7 @@ Set getFontsToSubset() *

* Don't use the input file as target as this will produce a corrupted file. *

- * If encryption has been activated (with {@link #protect(ProtectionPolicy) + * If encryption has been activated (with {@link #protect(com.ainceborn.pdfbox.pdmodel.encryption.ProtectionPolicy) * protect(ProtectionPolicy)}), do not use the document after saving because the contents are now encrypted. * The same applies if your file was created from parts of another file and that * one is to be used after saving. @@ -946,7 +929,7 @@ public void save(String fileName) throws IOException *

* Don't use the input file as target as this will produce a corrupted file. *

- * If encryption has been activated (with {@link #protect(ProtectionPolicy) + * If encryption has been activated (with {@link #protect(com.ainceborn.pdfbox.pdmodel.encryption.ProtectionPolicy) * protect(ProtectionPolicy)}), do not use the document after saving because the contents are now encrypted. * The same applies if your file was created from parts of another file and that * one is to be used after saving. @@ -965,7 +948,7 @@ public void save(File file) throws IOException *

* Don't use the input file as target as this will produce a corrupted file. *

- * If encryption has been activated (with {@link #protect(ProtectionPolicy) + * If encryption has been activated (with {@link #protect(com.ainceborn.pdfbox.pdmodel.encryption.ProtectionPolicy) * protect(ProtectionPolicy)}), do not use the document after saving because the contents are now encrypted. * The same applies if your file was created from parts of another file and that * one is to be used after saving. @@ -985,7 +968,7 @@ public void save(OutputStream output) throws IOException *

* Don't use the input file as target as this will produce a corrupted file. *

- * If encryption has been activated (with {@link #protect(ProtectionPolicy) + * If encryption has been activated (with {@link #protect(com.ainceborn.pdfbox.pdmodel.encryption.ProtectionPolicy) * protect(ProtectionPolicy)}), do not use the document after saving because the contents are now encrypted. * The same applies if your file was created from parts of another file and that * one is to be used after saving. @@ -1014,7 +997,7 @@ public void save(File file, CompressParameters compressParameters) throws IOExce *

* Don't use the input file as target as this will produce a corrupted file. *

- * If encryption has been activated (with {@link #protect(ProtectionPolicy) + * If encryption has been activated (with {@link #protect(com.ainceborn.pdfbox.pdmodel.encryption.ProtectionPolicy) * protect(ProtectionPolicy)}), do not use the document after saving because the contents are now encrypted. * The same applies if your file was created from parts of another file and that * one is to be used after saving. @@ -1034,7 +1017,7 @@ public void save(String fileName, CompressParameters compressParameters) throws *

* Don't use the input file as target as this will produce a corrupted file. *

- * If encryption has been activated (with {@link #protect(ProtectionPolicy) + * If encryption has been activated (with {@link #protect(com.ainceborn.pdfbox.pdmodel.encryption.ProtectionPolicy) * protect(ProtectionPolicy)}), do not use the document after saving because the contents are now encrypted. * The same applies if your file was created from parts of another file and that * one is to be used after saving. @@ -1089,7 +1072,7 @@ private void subsetDesignatedFonts() throws IOException * signed. (PDFBox already does this for signature widget annotations) *

* Another problem with page-based modifications can occur if the page tree isn't flat: there - * won't be an closed update path from the catalog to the page. To fix this, add code like this: + * won't be a closed update path from the catalog to the page. To fix this, add code like this: *

{@code
      * COSDictionary parent = page.getCOSObject().getCOSDictionary(COSName.PARENT);
      * while (parent != null)
@@ -1100,7 +1083,7 @@ private void subsetDesignatedFonts() throws IOException
      * }
* Don't use the input file as target as this will produce a corrupted file. * - * @param output stream to write to. It will be closed when done. It must never point to the source + * @param output stream to write to. It must never point to the source * file or that one will be harmed! * @throws IOException if the output could not be written * @throws IllegalStateException if the document was not loaded from a file or a stream. @@ -1136,7 +1119,7 @@ public void saveIncremental(OutputStream output) throws IOException *

* Don't use the input file as target as this will produce a corrupted file. * - * @param output stream to write to. It will be closed when done. It must never point to the source + * @param output stream to write to. It must never point to the source * file or that one will be harmed! * @param objectsToWrite objects that must be part of the incremental saving. * @throws IOException if the output could not be written @@ -1183,7 +1166,7 @@ public void saveIncremental(OutputStream output, Set objectsToWri *

* Don't use the input file as target as this will produce a corrupted file. * - * @param output stream to write the final PDF. It will be closed when the document is closed. It must + * @param output stream to write the final PDF. It must * never point to the source file or that one will be harmed! * @return instance to be used for external signing and setting CMS signature * @throws IOException if the output could not be written @@ -1321,8 +1304,8 @@ public void close() throws IOException * The same applies if your file was created from parts of another file and that * one is to be used after saving. * - * @see StandardProtectionPolicy - * @see PublicKeyProtectionPolicy + * @see com.ainceborn.pdfbox.pdmodel.encryption.StandardProtectionPolicy + * @see com.ainceborn.pdfbox.pdmodel.encryption.PublicKeyProtectionPolicy * * @param policy The protection policy. * @throws IOException if there isn't any suitable security handler. diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDDocumentCatalog.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDDocumentCatalog.java index 13efaa05..f7e7a530 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDDocumentCatalog.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDDocumentCatalog.java @@ -235,6 +235,12 @@ public List getThreads() */ public void setThreads(List threads) { + // PDFBOX-6186: avoid IllegalArgumentException when threads is null + if (threads == null) + { + root.removeItem(COSName.THREADS); + return; + } root.setItem(COSName.THREADS, COSArrayList.converterToCOSArray(threads)); } diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDPage.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDPage.java index ace59ca3..7eef1280 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDPage.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDPage.java @@ -658,6 +658,12 @@ public List getThreadBeads() */ public void setThreadBeads(List beads) { + // PDFBOX-6186: avoid NPE when beads is null + if (beads == null) + { + page.removeItem(COSName.B); + return; + } page.setItem(COSName.B, new COSArray(beads)); } @@ -747,7 +753,7 @@ public void setTransition(PDTransition transition, float duration) * * @throws IOException If there is an error while creating the annotation list. */ - public List getAnnotations() throws IOException + public List getAnnotations() { return getAnnotations(annotation -> true); } @@ -761,7 +767,7 @@ public List getAnnotations() throws IOException * * @throws IOException If there is an error while creating the annotation list. */ - public List getAnnotations(AnnotationFilter annotationFilter) throws IOException + public List getAnnotations(AnnotationFilter annotationFilter) { COSArray annots = page.getCOSArray(COSName.ANNOTS); if (annots == null) @@ -777,10 +783,18 @@ public List getAnnotations(AnnotationFilter annotationFilter) thro { continue; } - PDAnnotation createdAnnotation = PDAnnotation.createAnnotation(item); - if (annotationFilter.accept(createdAnnotation)) + try + { + // PDFBOX-6206: skip and log bad annotations when rendering + PDAnnotation createdAnnotation = PDAnnotation.createAnnotation(item); + if (annotationFilter.accept(createdAnnotation)) + { + actuals.add(createdAnnotation); + } + } + catch (IOException ex) { - actuals.add(createdAnnotation); + Log.e(TAG, ex.getMessage(), ex); } } return new COSArrayList<>(actuals, annots); diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDResources.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDResources.java index 22c3c37c..46cdb04a 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDResources.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/PDResources.java @@ -54,8 +54,7 @@ public final class PDResources implements COSObjectable // PDFBOX-3442 cache fonts that are not indirect objects, as these aren't cached in ResourceCache // and this would result in huge memory footprint in text extraction - private final Map > directFontCache = - new HashMap>(); + private final Map> directFontCache; /** * Constructor for embedding. @@ -64,6 +63,7 @@ public PDResources() { resources = new COSDictionary(); cache = null; + directFontCache = new HashMap<>(); } /** @@ -79,6 +79,7 @@ public PDResources(COSDictionary resourceDictionary) } resources = resourceDictionary; cache = null; + directFontCache = new HashMap<>(); } /** @@ -95,6 +96,30 @@ public PDResources(COSDictionary resourceDictionary, ResourceCache resourceCache } resources = resourceDictionary; cache = resourceCache; + directFontCache = new HashMap<>(); + } + + /** + * Constructor for reading. + * + * @param resourceDictionary The cos dictionary for this resource. + * @param resourceCache The document's resource cache, may be null. + * @param directFontCache The document's direct font cache. Must be mutable + */ + public PDResources(COSDictionary resourceDictionary, ResourceCache resourceCache, + Map> directFontCache) + { + if (resourceDictionary == null) + { + throw new IllegalArgumentException("resourceDictionary is null"); + } + if (directFontCache == null) + { + throw new IllegalArgumentException("directFontCache is null"); + } + resources = resourceDictionary; + cache = resourceCache; + this.directFontCache = directFontCache; } /** @@ -110,8 +135,7 @@ public COSDictionary getCOSObject() * Returns the font resource with the given name, or null if none exists. * * @param name Name of the font resource. - * - * @return the font resource with the given name. + * @return the font with the given name or null * * @throws IOException if something went wrong. */ @@ -152,7 +176,7 @@ else if (indirect == null) } else if (indirect == null) { - directFontCache.put(name, new SoftReference(font)); + directFontCache.put(name, new SoftReference<>(font)); } return font; } @@ -215,8 +239,7 @@ public PDColorSpace getColorSpace(COSName name, boolean wasDefault) throws IOExc * Returns true if the given color space name exists in these resources. * * @param name Name of the color space resource. - * - * @return true if the color space with the given name exists. + * @return true if the given color space name exists in these resources, otherwise false */ public boolean hasColorSpace(COSName name) { @@ -227,8 +250,7 @@ public boolean hasColorSpace(COSName name) * Returns the extended graphics state resource with the given name, or null if none exists. * * @param name Name of the graphics state resource. - * - * @return the extended graphics state resource with the given name. + * @return the extended graphics state with the given name or null */ public PDExtendedGraphicsState getExtGState(COSName name) { @@ -247,7 +269,7 @@ public PDExtendedGraphicsState getExtGState(COSName name) COSBase base = get(COSName.EXT_G_STATE, name); if (base instanceof COSDictionary) { - extGState = new PDExtendedGraphicsState((COSDictionary) base); + extGState = new PDExtendedGraphicsState((COSDictionary) base, getResourceCache()); } if (cache != null && indirect != null) @@ -261,8 +283,7 @@ public PDExtendedGraphicsState getExtGState(COSName name) * Returns the shading resource with the given name, or null if none exists. * * @param name Name of the shading resource. - * - * @return the shading resource of the given name. + * @return the shading state with the given name or null * * @throws IOException if something went wrong. */ @@ -297,8 +318,7 @@ public PDShading getShading(COSName name) throws IOException * Returns the pattern resource with the given name, or null if none exists. * * @param name Name of the pattern resource. - * - * @return the pattern resource of the given name. + * @return the pattern with the given name or null * * @throws IOException if something went wrong. */ @@ -333,8 +353,7 @@ public PDAbstractPattern getPattern(COSName name) throws IOException * Returns the property list resource with the given name, or null if none exists. * * @param name Name of the property list resource. - * - * @return the property list resource of the given name. + * @return the property list with the given name or null */ public PDPropertyList getProperties(COSName name) { @@ -393,8 +412,7 @@ else if (value instanceof COSObject) * Returns the XObject resource with the given name, or null if none exists. * * @param name Name of the XObject resource. - * - * @return the XObject resource of the given name. + * @return the XObject with the given name or null * * @throws IOException if something went wrong. */ @@ -436,11 +454,10 @@ private boolean isAllowedCache(PDXObject xobject) { if (xobject instanceof PDImageXObject) { - COSBase colorSpace = xobject.getCOSObject().getDictionaryObject(COSName.COLORSPACE); - if (colorSpace instanceof COSName) + COSName colorSpaceName = xobject.getCOSObject().getCOSName(COSName.COLORSPACE); + if (colorSpaceName != null) { // don't cache if it might use page resources, see PDFBOX-2370 and PDFBOX-3484 - COSName colorSpaceName = (COSName) colorSpace; if (colorSpaceName.equals(COSName.DEVICECMYK) && hasColorSpace(COSName.DEFAULT_CMYK)) { return false; @@ -487,17 +504,13 @@ private COSObject getIndirect(COSName kind, COSName name) private COSBase get(COSName kind, COSName name) { COSDictionary dict = resources.getCOSDictionary(kind); - if (dict == null) - { - return null; - } - return dict.getDictionaryObject(name); + return dict != null ? dict.getDictionaryObject(name) : null; } /** * Returns the names of the color space resources, if any. * - * @return the names of all color space resources. + * @return an iterable containing all names of available colorspaces */ public Iterable getColorSpaceNames() { @@ -507,7 +520,7 @@ public Iterable getColorSpaceNames() /** * Returns the names of the XObject resources, if any. * - * @return the names of all XObject resources. + * @return an iterable containing all names of available xobjects */ public Iterable getXObjectNames() { @@ -517,7 +530,7 @@ public Iterable getXObjectNames() /** * Returns the names of the font resources, if any. * - * @return the names of all font resources. + * @return an iterable containing all names of available fonts */ public Iterable getFontNames() { @@ -527,7 +540,7 @@ public Iterable getFontNames() /** * Returns the names of the property list resources, if any. * - * @return the names of all property list resources. + * @return an iterable containing all names of available property lists */ public Iterable getPropertiesNames() { @@ -537,7 +550,7 @@ public Iterable getPropertiesNames() /** * Returns the names of the shading resources, if any. * - * @return the names of all shading resources. + * @return an iterable containing all names of available shadings */ public Iterable getShadingNames() { @@ -547,7 +560,7 @@ public Iterable getShadingNames() /** * Returns the names of the pattern resources, if any. * - * @return the names of all pattern resources. + * @return an iterable containing all names of available patterns */ public Iterable getPatternNames() { @@ -557,7 +570,7 @@ public Iterable getPatternNames() /** * Returns the names of the extended graphics state resources, if any. * - * @return the names of all extended graphics state resources. + * @return an iterable containing all names of available extended graphics states */ public Iterable getExtGStateNames() { @@ -566,17 +579,11 @@ public Iterable getExtGStateNames() /** * Returns the resource names of the given kind. - * - * @return the names of all resources of the given kind. */ private Iterable getNames(COSName kind) { COSDictionary dict = resources.getCOSDictionary(kind); - if (dict == null) - { - return Collections.emptySet(); - } - return dict.keySet(); + return dict != null ? dict.keySet() : Collections.emptySet(); } /** @@ -707,14 +714,14 @@ private COSName add(COSName kind, String prefix, COSObjectable object) return dict.getKeyForValue(object.getCOSObject()); } - // PDFBOX-4509: It could exist as an indirect object, happens when a font is taken from the + // PDFBOX-4509: It could exist as an indirect object, happens when a font is taken from the // AcroForm default resources of a loaded PDF. if (dict != null && COSName.FONT.equals(kind)) { for (Map.Entry entry : dict.entrySet()) { if (entry.getValue() instanceof COSObject && - object.getCOSObject() == ((COSObject) entry.getValue()).getObject()) + object.getCOSObject() == ((COSObject) entry.getValue()).getObject()) { return entry.getKey(); } @@ -844,7 +851,7 @@ public void put(COSName name, PDXObject xobject) /** * Returns the resource cache associated with the Resources, or null if there is none. * - * @return the resource cache associated with the resources. + * @return the resource cache associated with the resources, or null */ public ResourceCache getResourceCache() { diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/fixup/processor/AcroFormOrphanWidgetsProcessor.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/fixup/processor/AcroFormOrphanWidgetsProcessor.java index 5d7eff80..440ae150 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/fixup/processor/AcroFormOrphanWidgetsProcessor.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/fixup/processor/AcroFormOrphanWidgetsProcessor.java @@ -85,18 +85,11 @@ private void resolveFieldsFromWidgets(PDAcroForm acroForm) return; } - List fields = new ArrayList(); - Map nonTerminalFieldsMap = new HashMap(); + List fields = new ArrayList<>(); + Map nonTerminalFieldsMap = new HashMap<>(); for (PDPage page : document.getPages()) { - try - { - handleAnnotations(acroForm, resources, fields, page.getAnnotations(), nonTerminalFieldsMap); - } - catch (IOException ioe) - { - Log.d("PdfBox-Android", "couldn't read annotations for page " + ioe.getMessage()); - } + handleAnnotations(acroForm, resources, fields, page.getAnnotations(), nonTerminalFieldsMap); } acroForm.setFields(fields); @@ -111,8 +104,8 @@ private void resolveFieldsFromWidgets(PDAcroForm acroForm) } private void handleAnnotations(PDAcroForm acroForm, PDResources acroFormResources, - List fields, List annotations, - Map nonTerminalFieldsMap) + List fields, List annotations, + Map nonTerminalFieldsMap) { for (PDAnnotation annot : annotations) { @@ -131,7 +124,11 @@ private void handleAnnotations(PDAcroForm acroForm, PDResources acroFormResource } else { - fields.add(PDFieldFactory.createField(acroForm, annot.getCOSObject(), null)); + PDField field = PDFieldFactory.createField(acroForm, annot.getCOSObject(), null); + if (field != null) + { + fields.add(field); + } } } } @@ -156,7 +153,7 @@ private void addFontFromWidget(PDResources acroFormResources, PDAnnotation annot { return; } - for (COSName fontName : widgetResources.getFontNames()) + widgetResources.getFontNames().forEach(fontName -> { if (!fontName.getName().startsWith("+")) { @@ -177,7 +174,7 @@ private void addFontFromWidget(PDResources acroFormResources, PDAnnotation annot { Log.d("PdfBox-Android", "font resource for widget was a subsetted font - ignored: " + fontName.getName()); } - } + }); } /* @@ -209,6 +206,7 @@ private PDField resolveNonRootField(PDAcroForm acroForm, COSDictionary parent, M return null; } + /* * Lookup the font used in the default appearance and if this is * not available try to find a suitable font and use that. @@ -223,7 +221,7 @@ private void ensureFontResources(PDResources defaultResources, PDVariableText fi String daString = field.getDefaultAppearance(); if (daString.startsWith("/") && daString.length() > 1) { - COSName fontName = COSName.getPDFName(daString.substring(1, daString.indexOf(" "))); + COSName fontName = COSName.getPDFName(daString.substring(1, daString.indexOf(' '))); try { if (defaultResources.getFont(fontName) == null) @@ -249,4 +247,4 @@ private void ensureFontResources(PDResources defaultResources, PDVariableText fi } } } -} \ No newline at end of file +} diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/font/PDType1FontEmbedder.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/font/PDType1FontEmbedder.java index a4c8adf7..7a487f64 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/font/PDType1FontEmbedder.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/font/PDType1FontEmbedder.java @@ -76,7 +76,6 @@ class PDType1FontEmbedder PDFontDescriptor fd = buildFontDescriptor(type1); PDStream fontStream = new PDStream(doc, pfbParser.getInputStream(), COSName.FLATE_DECODE); - fontStream.getCOSObject().setInt("Length", pfbParser.size()); for (int i = 0; i < pfbParser.getLengths().length; i++) { fontStream.getCOSObject().setInt("Length" + (i + 1), pfbParser.getLengths()[i]); diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/font/TrueTypeEmbedder.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/font/TrueTypeEmbedder.java index 77b4e138..ce0323b7 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/font/TrueTypeEmbedder.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/font/TrueTypeEmbedder.java @@ -377,8 +377,8 @@ protected abstract void buildSubset(InputStream ttfSubset, String tag, */ public String getTag(Map gidToCid) { - // deterministic - long num = gidToCid.hashCode(); + // PDFBOX-6193: hash might be negative due to an overflow if the map contains lots of values + long num = Math.abs(gidToCid.hashCode()); // base25 encode StringBuilder sb = new StringBuilder(); diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/BlendMode.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/BlendMode.java index 40d64067..a60c8c84 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/BlendMode.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/BlendMode.java @@ -28,191 +28,222 @@ * * @author Kühn & Weyh Software GmbH */ -public abstract class BlendMode +public class BlendMode { - public static final SeparableBlendMode NORMAL = new SeparableBlendMode() + @FunctionalInterface + public interface BlendChannelFunction { - @Override - public float blendChannel(float srcValue, float dstValue) - { - return srcValue; - } - }; - - public static final SeparableBlendMode COMPATIBLE = NORMAL; + /** + * BlendChannel function for separable blend modes. + * + * @param src the source value + * @param dest the destination value + * @return the function result + */ + float blendChannel(float src, float dest); + } - public static final SeparableBlendMode MULTIPLY = new SeparableBlendMode() + @FunctionalInterface + public interface BlendFunction { - @Override - public float blendChannel(float srcValue, float dstValue) - { - return srcValue * dstValue; - } - }; + /** + * Blend function for non separable blend modes. + * + * @param src the source values + * @param dest the destination values + * @param result the function result values + */ + void blend(float[] src, float[] dest, float[] result); + } - public static final SeparableBlendMode SCREEN = new SeparableBlendMode() - { - @Override - public float blendChannel(float srcValue, float dstValue) - { - return srcValue + dstValue - srcValue * dstValue; - } - }; + /** + * Functions for the blend operation of separable blend modes + */ + private static final BlendChannelFunction fNormal = (src, dest) -> src; - public static final SeparableBlendMode OVERLAY = new SeparableBlendMode() - { - @Override - public float blendChannel(float srcValue, float dstValue) - { - return (dstValue <= 0.5) ? 2 * dstValue * srcValue : 2 * (srcValue + dstValue - srcValue - * dstValue) - 1; - } - }; + private static final BlendChannelFunction fMultiply = (src, dest) -> src * dest; - public static final SeparableBlendMode DARKEN = new SeparableBlendMode() - { - @Override - public float blendChannel(float srcValue, float dstValue) - { - return Math.min(srcValue, dstValue); - } - }; + private static final BlendChannelFunction fScreen = (src, dest) -> src + dest - src * dest; - public static final SeparableBlendMode LIGHTEN = new SeparableBlendMode() - { - @Override - public float blendChannel(float srcValue, float dstValue) + private static final BlendChannelFunction fOverlay = (src, dest) -> (dest <= 0.5) ? 2 * dest * src + : 2 * (src + dest - src * dest) - 1; + + private static final BlendChannelFunction fDarken = Math::min; + + private static final BlendChannelFunction fLighten = Math::max; + + private static final BlendChannelFunction fColorDodge = (src, dest) -> { + // See PDF 2.0 specification + if (Float.compare(dest, 0) == 0) { - return Math.max(srcValue, dstValue); + return 0f; } - }; - - public static final SeparableBlendMode COLOR_DODGE = new SeparableBlendMode() - { - @Override - public float blendChannel(float srcValue, float dstValue) + if (dest >= 1 - src) { - // See PDF 2.0 specification - if (dstValue == 0) - { - return 0; - } - if (dstValue >= 1 - srcValue) - { - return 1; - } - return dstValue / (1 - srcValue); + return 1f; } + return dest / (1 - src); }; - public static final SeparableBlendMode COLOR_BURN = new SeparableBlendMode() - { - @Override - public float blendChannel(float srcValue, float dstValue) + private static final BlendChannelFunction fColorBurn = (src, dest) -> { + // See PDF 2.0 specification + if (Float.compare(dest, 1) == 0) { - // See PDF 2.0 specification - if (dstValue == 1) - { - return 1; - } - if (1 - dstValue >= srcValue) - { - return 0; - } - return 1 - (1 - dstValue) / srcValue; + return 1f; } - }; - - public static final SeparableBlendMode HARD_LIGHT = new SeparableBlendMode() - { - @Override - public float blendChannel(float srcValue, float dstValue) + if (1 - dest >= src) { - return (srcValue <= 0.5) ? 2 * dstValue * srcValue : - 2 * (srcValue + dstValue - srcValue * dstValue) - 1; + return 0f; } + return 1 - (1 - dest) / src; }; - public static final SeparableBlendMode SOFT_LIGHT = new SeparableBlendMode() - { - @Override - public float blendChannel(float srcValue, float dstValue) + private static final BlendChannelFunction fHardLight = (src, dest) -> (src <= 0.5) ? 2 * dest * src + : 2 * (src + dest - src * dest) - 1; + + private static final BlendChannelFunction fSoftLight = (src, dest) -> { + if (src <= 0.5) { - if (srcValue <= 0.5) - { - return dstValue - (1 - 2 * srcValue) * dstValue * (1 - dstValue); - } - else - { - float d = (dstValue <= 0.25) ? ((16 * dstValue - 12) * dstValue + 4) * dstValue - : (float) Math .sqrt(dstValue); - return dstValue + (2 * srcValue - 1) * (d - dstValue); - } + return dest - (1 - 2 * src) * dest * (1 - dest); } - }; - - public static final SeparableBlendMode DIFFERENCE = new SeparableBlendMode() - { - @Override - public float blendChannel(float srcValue, float dstValue) + else { - return Math.abs(dstValue - srcValue); + float d = (dest <= 0.25) ? ((16 * dest - 12) * dest + 4) * dest + : (float) Math.sqrt(dest); + return dest + (2 * src - 1) * (d - dest); } }; - public static final SeparableBlendMode EXCLUSION = new SeparableBlendMode() - { - @Override - public float blendChannel(float srcValue, float dstValue) - { - return dstValue + srcValue - 2 * dstValue * srcValue; - } + private static final BlendChannelFunction fDifference = (src, dest) -> Math.abs(dest - src); + + private static final BlendChannelFunction fExclusion = (src, dest) -> dest + src - 2 * dest * src; + + /** + * Functions for the blend operation of non-separable blend modes + */ + private static final BlendFunction fHue = (src, dest, result) -> { + float[] temp = new float[3]; + getSaturationRGB(dest, src, temp); + getLuminosityRGB(dest, temp, result); }; - public static final NonSeparableBlendMode HUE = new NonSeparableBlendMode() + private static final BlendFunction fSaturation = BlendMode::getSaturationRGB; + + private static final BlendFunction fColor = (src, dest, result) -> getLuminosityRGB(dest, src, + result); + + private static final BlendFunction fLuminosity = BlendMode::getLuminosityRGB; + + /** + * Separable blend modes as defined in the PDF specification + */ + public static final BlendMode NORMAL = new BlendMode(COSName.NORMAL, fNormal, null); + public static final BlendMode COMPATIBLE = BlendMode.NORMAL; + public static final BlendMode MULTIPLY = new BlendMode(COSName.MULTIPLY, fMultiply, null); + public static final BlendMode SCREEN = new BlendMode(COSName.SCREEN, fScreen, null); + public static final BlendMode OVERLAY = new BlendMode(COSName.OVERLAY, fOverlay, null); + public static final BlendMode DARKEN = new BlendMode(COSName.DARKEN, fDarken, null); + public static final BlendMode LIGHTEN = new BlendMode(COSName.LIGHTEN, fLighten, null); + public static final BlendMode COLOR_DODGE = new BlendMode(COSName.COLOR_DODGE, fColorDodge, + null); + public static final BlendMode COLOR_BURN = new BlendMode(COSName.COLOR_BURN, fColorBurn, null); + public static final BlendMode HARD_LIGHT = new BlendMode(COSName.HARD_LIGHT, fHardLight, null); + public static final BlendMode SOFT_LIGHT = new BlendMode(COSName.SOFT_LIGHT, fSoftLight, null); + public static final BlendMode DIFFERENCE = new BlendMode(COSName.DIFFERENCE, fDifference, null); + public static final BlendMode EXCLUSION = new BlendMode(COSName.EXCLUSION, fExclusion, null); + + /** + * Non-separable blend modes as defined in the PDF specification + */ + public static final BlendMode HUE = new BlendMode(COSName.HUE, null, fHue); + public static final BlendMode SATURATION = new BlendMode(COSName.SATURATION, null, fSaturation); + public static final BlendMode COLOR = new BlendMode(COSName.COLOR, null, fColor); + public static final BlendMode LUMINOSITY = new BlendMode(COSName.LUMINOSITY, null, fLuminosity); + + private static final Map BLEND_MODES = createBlendModeMap(); + + private static Map createBlendModeMap() { - @Override - public void blend(float[] srcValues, float[] dstValues, float[] result) - { - float[] temp = new float[3]; - getSaturationRGB(dstValues, srcValues, temp); - getLuminosityRGB(dstValues, temp, result); - } - }; + Map map = new HashMap<>(13); + map.put(COSName.NORMAL, NORMAL); + // BlendMode.COMPATIBLE should not be used + map.put(COSName.COMPATIBLE, NORMAL); + map.put(COSName.MULTIPLY, MULTIPLY); + map.put(COSName.SCREEN, SCREEN); + map.put(COSName.OVERLAY, OVERLAY); + map.put(COSName.DARKEN, DARKEN); + map.put(COSName.LIGHTEN, LIGHTEN); + map.put(COSName.COLOR_DODGE, COLOR_DODGE); + map.put(COSName.COLOR_BURN, COLOR_BURN); + map.put(COSName.HARD_LIGHT, HARD_LIGHT); + map.put(COSName.SOFT_LIGHT, SOFT_LIGHT); + map.put(COSName.DIFFERENCE, DIFFERENCE); + map.put(COSName.EXCLUSION, EXCLUSION); + map.put(COSName.HUE, HUE); + map.put(COSName.SATURATION, SATURATION); + map.put(COSName.LUMINOSITY, LUMINOSITY); + map.put(COSName.COLOR, COLOR); + return map; + } - public static final NonSeparableBlendMode SATURATION = new NonSeparableBlendMode() + private final COSName name; + private final BlendChannelFunction blendChannel; + private final BlendFunction blend; + private final boolean isSeparable; + + /** + * Private constructor due to the limited set of possible blend modes. + * + * @param name the corresponding COSName of the blend mode + * @param blendChannel the blend function for separable blend modes + * @param blend the blend function for non-separable blend modes + */ + private BlendMode(COSName name, BlendChannelFunction blendChannel, BlendFunction blend) { - @Override - public void blend(float[] srcValues, float[] dstValues, float[] result) - { - getSaturationRGB(srcValues, dstValues, result); - } - }; + this.name = name; + this.blendChannel = blendChannel; + this.blend = blend; + isSeparable = blendChannel != null; + } - public static final NonSeparableBlendMode COLOR = new NonSeparableBlendMode() + /** + * The blend mode name from the BM object. + * + * @return name of blend mode. + */ + public COSName getCOSName() { - @Override - public void blend(float[] srcValues, float[] dstValues, float[] result) - { - getLuminosityRGB(dstValues, srcValues, result); - } - }; + return name; + } - public static final NonSeparableBlendMode LUMINOSITY = new NonSeparableBlendMode() + /** + * Determines if the blend mode is a separable blend mode. + * + * @return true for separable blend modes + */ + public boolean isSeparableBlendMode() { - @Override - public void blend(float[] srcValues, float[] dstValues, float[] result) - { - getLuminosityRGB(srcValues, dstValues, result); - } - }; + return isSeparable; + } - // these maps *must* come after the declarations above, otherwise its values will be null - private static final Map BLEND_MODES = createBlendModeMap(); - private static final Map BLEND_MODE_NAMES = createBlendModeNamesMap(); + /** + * Returns the blend channel function, only available for separable blend modes. + * + * @return the blend channel function + */ + public BlendChannelFunction getBlendChannelFunction() + { + return blendChannel; + } - BlendMode() + /** + * Returns the blend function, only available for non separable blend modes. + * + * @return the blend function + */ + public BlendFunction getBlendFunction() { + return blend; } /** @@ -233,30 +264,18 @@ else if (cosBlendMode instanceof COSArray) COSArray cosBlendModeArray = (COSArray) cosBlendMode; for (int i = 0; i < cosBlendModeArray.size(); i++) { - result = BLEND_MODES.get(cosBlendModeArray.getObject(i)); - if (result != null) + COSBase cosBase = cosBlendModeArray.getObject(i); + if (cosBase instanceof COSName) { - break; + result = BLEND_MODES.get(cosBase); + if (result != null) + { + break; + } } } } - - if (result != null) - { - return result; - } - return BlendMode.NORMAL; - } - - /** - * Determines the blend mode name from the BM object. - * - * @param bm Blend mode. - * @return name of blend mode. - */ - public static COSName getCOSName(BlendMode bm) - { - return BLEND_MODE_NAMES.get(bm); + return result != null ? result : BlendMode.NORMAL; } private static int get255Value(float val) @@ -266,25 +285,12 @@ private static int get255Value(float val) private static void getSaturationRGB(float[] srcValues, float[] dstValues, float[] result) { - int minb; - int maxb; - int mins; - int maxs; - int y; - int scale; - int r; - int g; - int b; - int rd = get255Value(dstValues[0]); int gd = get255Value(dstValues[1]); int bd = get255Value(dstValues[2]); - int rs = get255Value(srcValues[0]); - int gs = get255Value(srcValues[1]); - int bs = get255Value(srcValues[2]); - minb = Math.min(rd, Math.min(gd, bd)); - maxb = Math.max(rd, Math.max(gd, bd)); + int minb = Math.min(rd, Math.min(gd, bd)); + int maxb = Math.max(rd, Math.max(gd, bd)); if (minb == maxb) { /* backdrop has zero saturation, avoid divide by 0 */ @@ -294,24 +300,26 @@ private static void getSaturationRGB(float[] srcValues, float[] dstValues, float return; } - mins = Math.min(rs, Math.min(gs, bs)); - maxs = Math.max(rs, Math.max(gs, bs)); + int rs = get255Value(srcValues[0]); + int gs = get255Value(srcValues[1]); + int bs = get255Value(srcValues[2]); + + int mins = Math.min(rs, Math.min(gs, bs)); + int maxs = Math.max(rs, Math.max(gs, bs)); - scale = ((maxs - mins) << 16) / (maxb - minb); - y = (rd * 77 + gd * 151 + bd * 28 + 0x80) >> 8; - r = y + ((((rd - y) * scale) + 0x8000) >> 16); - g = y + ((((gd - y) * scale) + 0x8000) >> 16); - b = y + ((((bd - y) * scale) + 0x8000) >> 16); + int scale = ((maxs - mins) << 16) / (maxb - minb); + int y = (rd * 77 + gd * 151 + bd * 28 + 0x80) >> 8; + int r = y + ((((rd - y) * scale) + 0x8000) >> 16); + int g = y + ((((gd - y) * scale) + 0x8000) >> 16); + int b = y + ((((bd - y) * scale) + 0x8000) >> 16); if (((r | g | b) & 0x100) == 0x100) { int scalemin; int scalemax; - int min; - int max; - min = Math.min(r, Math.min(g, b)); - max = Math.max(r, Math.max(g, b)); + int min = Math.min(r, Math.min(g, b)); + int max = Math.max(r, Math.max(g, b)); if (min < 0) { @@ -343,26 +351,21 @@ private static void getSaturationRGB(float[] srcValues, float[] dstValues, float private static void getLuminosityRGB(float[] srcValues, float[] dstValues, float[] result) { - int delta; - int scale; - int r; - int g; - int b; - int y; int rd = get255Value(dstValues[0]); int gd = get255Value(dstValues[1]); int bd = get255Value(dstValues[2]); int rs = get255Value(srcValues[0]); int gs = get255Value(srcValues[1]); int bs = get255Value(srcValues[2]); - delta = ((rs - rd) * 77 + (gs - gd) * 151 + (bs - bd) * 28 + 0x80) >> 8; - r = rd + delta; - g = gd + delta; - b = bd + delta; + int delta = ((rs - rd) * 77 + (gs - gd) * 151 + (bs - bd) * 28 + 0x80) >> 8; + int r = rd + delta; + int g = gd + delta; + int b = bd + delta; if (((r | g | b) & 0x100) == 0x100) { - y = (rs * 77 + gs * 151 + bs * 28 + 0x80) >> 8; + int scale; + int y = (rs * 77 + gs * 151 + bs * 28 + 0x80) >> 8; if (delta > 0) { int max; @@ -384,51 +387,9 @@ private static void getLuminosityRGB(float[] srcValues, float[] dstValues, float result[2] = b / 255.0f; } - private static Map createBlendModeMap() - { - Map map = new HashMap(13); - map.put(COSName.NORMAL, BlendMode.NORMAL); - // BlendMode.COMPATIBLE should not be used - map.put(COSName.COMPATIBLE, BlendMode.NORMAL); - map.put(COSName.MULTIPLY, BlendMode.MULTIPLY); - map.put(COSName.SCREEN, BlendMode.SCREEN); - map.put(COSName.OVERLAY, BlendMode.OVERLAY); - map.put(COSName.DARKEN, BlendMode.DARKEN); - map.put(COSName.LIGHTEN, BlendMode.LIGHTEN); - map.put(COSName.COLOR_DODGE, BlendMode.COLOR_DODGE); - map.put(COSName.COLOR_BURN, BlendMode.COLOR_BURN); - map.put(COSName.HARD_LIGHT, BlendMode.HARD_LIGHT); - map.put(COSName.SOFT_LIGHT, BlendMode.SOFT_LIGHT); - map.put(COSName.DIFFERENCE, BlendMode.DIFFERENCE); - map.put(COSName.EXCLUSION, BlendMode.EXCLUSION); - map.put(COSName.HUE, BlendMode.HUE); - map.put(COSName.SATURATION, BlendMode.SATURATION); - map.put(COSName.LUMINOSITY, BlendMode.LUMINOSITY); - map.put(COSName.COLOR, BlendMode.COLOR); - return map; - } - - private static Map createBlendModeNamesMap() + @Override + public String toString() { - Map map = new HashMap(13); - map.put(BlendMode.NORMAL, COSName.NORMAL); - // BlendMode.COMPATIBLE should not be used - map.put(BlendMode.COMPATIBLE, COSName.NORMAL); - map.put(BlendMode.MULTIPLY, COSName.MULTIPLY); - map.put(BlendMode.SCREEN, COSName.SCREEN); - map.put(BlendMode.OVERLAY, COSName.OVERLAY); - map.put(BlendMode.DARKEN, COSName.DARKEN); - map.put(BlendMode.LIGHTEN, COSName.LIGHTEN); - map.put(BlendMode.COLOR_DODGE, COSName.COLOR_DODGE); - map.put(BlendMode.COLOR_BURN, COSName.COLOR_BURN); - map.put(BlendMode.HARD_LIGHT, COSName.HARD_LIGHT); - map.put(BlendMode.SOFT_LIGHT, COSName.SOFT_LIGHT); - map.put(BlendMode.DIFFERENCE, COSName.DIFFERENCE); - map.put(BlendMode.EXCLUSION, COSName.EXCLUSION); - map.put(BlendMode.HUE, COSName.HUE); - map.put(BlendMode.SATURATION, COSName.SATURATION); - map.put(BlendMode.LUMINOSITY, COSName.LUMINOSITY); - map.put(BlendMode.COLOR, COSName.COLOR); - return map; + return "BlendMode{name=" + name.getName() + ", isSeparable=" + isSeparable + '}'; } } diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/NonSeparableBlendMode.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/NonSeparableBlendMode.java deleted file mode 100644 index e191f842..00000000 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/NonSeparableBlendMode.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.ainceborn.pdfbox.pdmodel.graphics.blend; - -/** - * Non-separable blend mode (supports blend function). - * - * @author Kühn & Weyh Software GmbH - */ -public abstract class NonSeparableBlendMode extends BlendMode -{ - NonSeparableBlendMode() - { - } - - public abstract void blend(float[] srcValues, float[] dstValues, float[] result); -} diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/SeparableBlendMode.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/SeparableBlendMode.java deleted file mode 100644 index 34d1c48c..00000000 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/SeparableBlendMode.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.ainceborn.pdfbox.pdmodel.graphics.blend; - -/** - * Separable blend mode (support blendChannel) - * - * @author Kühn & Weyh Software GmbH - */ -public abstract class SeparableBlendMode extends BlendMode -{ - SeparableBlendMode() - { - } - - public abstract float blendChannel(float srcValue, float dstValue); -} diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java index 5276c500..48ef4acb 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java @@ -26,6 +26,7 @@ import com.ainceborn.pdfbox.cos.COSFloat; import com.ainceborn.pdfbox.cos.COSName; import com.ainceborn.pdfbox.cos.COSNumber; +import com.ainceborn.pdfbox.pdmodel.ResourceCache; import com.ainceborn.pdfbox.pdmodel.common.COSObjectable; import com.ainceborn.pdfbox.pdmodel.graphics.PDFontSetting; import com.ainceborn.pdfbox.pdmodel.graphics.PDLineDashPattern; @@ -39,6 +40,7 @@ public class PDExtendedGraphicsState implements COSObjectable { private final COSDictionary dict; + private final ResourceCache cache; /** * Default constructor, creates blank graphics state. @@ -47,6 +49,7 @@ public PDExtendedGraphicsState() { dict = new COSDictionary(); dict.setItem(COSName.TYPE, COSName.EXT_G_STATE); + cache = null; } /** @@ -55,8 +58,20 @@ public PDExtendedGraphicsState() * @param dictionary The existing graphics state. */ public PDExtendedGraphicsState(COSDictionary dictionary) + { + this(dictionary, null); + } + + /** + * Create a graphics state from an existing dictionary. + * + * @param dictionary The existing graphics state. + * @param resourceCache Resource cache, may be null. + */ + public PDExtendedGraphicsState(COSDictionary dictionary, ResourceCache resourceCache) { dict = dictionary; + cache = resourceCache; } /** @@ -96,7 +111,8 @@ else if( key.equals( COSName.RI ) ) } else if( key.equals( COSName.OPM ) ) { - gs.setOverprintMode( defaultIfNull( getOverprintMode(), 0 ) ); + Integer overprintMode = getOverprintMode(); + gs.setOverprintMode(overprintMode != null ? overprintMode : 0); } else if( key.equals( COSName.OP ) ) { @@ -162,7 +178,7 @@ else if (key.equals(COSName.TR)) { if (dict.containsKey(COSName.TR2)) { - // "If both TR and TR2 are present in the same graphics state parameter dictionary, + // "If both TR and TR2 are present in the same graphics state parameter dictionary, // TR2 shall take precedence." continue; } @@ -309,11 +325,11 @@ public void setMiterLimit( Float miterLimit ) public PDLineDashPattern getLineDashPattern() { PDLineDashPattern retval = null; - COSBase dp = dict.getDictionaryObject( COSName.D ); - if( dp instanceof COSArray && ((COSArray)dp).size() == 2) + COSArray dp = dict.getCOSArray(COSName.D); + if (dp != null && dp.size() == 2) { - COSBase dashArray = ((COSArray)dp).getObject(0); - COSBase phase = ((COSArray)dp).getObject(1); + COSBase dashArray = dp.getObject(0); + COSBase phase = dp.getObject(1); if (dashArray instanceof COSArray && phase instanceof COSNumber) { retval = new PDLineDashPattern((COSArray) dashArray, ((COSNumber) phase).intValue()); @@ -406,18 +422,24 @@ public void setNonStrokingOverprintControl( boolean op ) * * @return The overprint control mode or null if one has not been set. */ - public Float getOverprintMode() + public Integer getOverprintMode() { - return getFloatItem(COSName.OPM); + Integer retval = null; + COSBase base = dict.getDictionaryObject(COSName.OPM); + if (base instanceof COSNumber) + { + COSNumber value = (COSNumber) base; + retval = value.intValue(); + } + return retval; } /** * This will set the overprint mode(OPM). * - * @param overprintMode The overprint mode. It will be truncated to an integer. This parameter - * will be an integer in version 3. + * @param overprintMode The overprint mode */ - public void setOverprintMode(Float overprintMode) + public void setOverprintMode(Integer overprintMode) { if (overprintMode == null) { @@ -425,7 +447,7 @@ public void setOverprintMode(Float overprintMode) } else { - dict.setInt(COSName.OPM, overprintMode.intValue()); + dict.setInt(COSName.OPM, overprintMode); } } @@ -436,14 +458,8 @@ public void setOverprintMode(Float overprintMode) */ public PDFontSetting getFontSetting() { - PDFontSetting setting = null; - COSBase base = dict.getDictionaryObject(COSName.FONT); - if (base instanceof COSArray) - { - COSArray font = (COSArray) base; - setting = new PDFontSetting(font); - } - return setting; + COSArray font = dict.getCOSArray(COSName.FONT); + return font != null ? new PDFontSetting(font) : null; } /** @@ -593,11 +609,11 @@ public BlendMode getBlendMode() /** * Set the blending mode. * - * @param bm + * @param bm blend mode */ public void setBlendMode(BlendMode bm) { - dict.setItem(COSName.BM, BlendMode.getCOSName(bm)); + dict.setItem(COSName.BM, bm.getCOSName()); } /** @@ -607,11 +623,8 @@ public void setBlendMode(BlendMode bm) */ public PDSoftMask getSoftMask() { - if (!dict.containsKey(COSName.SMASK)) - { - return null; - } - return PDSoftMask.create(dict.getDictionaryObject(COSName.SMASK)); + COSBase smask = dict.getDictionaryObject(COSName.SMASK); + return smask == null ? null : PDSoftMask.create(smask, cache); } /** @@ -646,6 +659,7 @@ public void setTextKnockoutFlag( boolean tk ) private Float getFloatItem( COSName key ) { Float retval = null; + COSBase base = dict.getDictionaryObject(key); if (base instanceof COSNumber) { diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDGraphicsState.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDGraphicsState.java index 9b0ce044..c29adf5b 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDGraphicsState.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDGraphicsState.java @@ -63,11 +63,13 @@ public class PDGraphicsState implements Cloneable private double alphaConstant = 1.0; private double nonStrokingAlphaConstant = 1.0; private boolean alphaSource = false; + private Matrix textMatrix = null; + private Matrix textLineMatrix = null; // DEVICE-DEPENDENT parameters private boolean overprint = false; private boolean nonStrokingOverprint = false; - private double overprintMode = 0; + private int overprintMode = 0; //black generation //undercolor removal private COSBase transfer = null; @@ -75,10 +77,6 @@ public class PDGraphicsState implements Cloneable private double flatness = 1.0; private double smoothness = 0; - - private Matrix textMatrix = null; - private Matrix textLineMatrix = null; - /** * Constructor with a given page size to initialize the clipping path. * @param page the size of the page @@ -228,30 +226,6 @@ public void setAlphaConstant(double value) alphaConstant = value; } - /** - * Get the value of the non-stroke alpha constant property. - * - * @return The value of the non-stroke alpha constant parameter. - * @deprecated use {@link #getNonStrokeAlphaConstant() } - */ - @Deprecated - public double getNonStrokeAlphaConstants() - { - return nonStrokingAlphaConstant; - } - - /** - * set the value of the non-stroke alpha constant property. - * - * @param value The value of the non-stroke alpha constant parameter. - * @deprecated use {@link #setNonStrokeAlphaConstant(double) } - */ - @Deprecated - public void setNonStrokeAlphaConstants(double value) - { - nonStrokingAlphaConstant = value; - } - /** * Get the value of the non-stroke alpha constant property. * @@ -306,7 +280,7 @@ public PDSoftMask getSoftMask() /** * Sets the current soft mask * - * @param softMask + * @param softMask soft mask */ public void setSoftMask(PDSoftMask softMask) { @@ -326,10 +300,15 @@ public BlendMode getBlendMode() /** * Sets the blend mode in the current graphics state * - * @param blendMode + * @param blendMode blend mode + * @throws IllegalArgumentException if blendMode is null. */ public void setBlendMode(BlendMode blendMode) { + if (blendMode == null) + { + throw new IllegalArgumentException("blendMode parameter cannot be null"); + } this.blendMode = blendMode; } @@ -378,7 +357,7 @@ public void setNonStrokingOverprint(boolean value) * * @return The value of the overprint mode parameter. */ - public double getOverprintMode() + public int getOverprintMode() { return overprintMode; } @@ -388,7 +367,7 @@ public double getOverprintMode() * * @param value The value of the overprint mode parameter. */ - public void setOverprintMode(double value) + public void setOverprintMode(int value) { overprintMode = value; } @@ -615,11 +594,9 @@ private void intersectClippingPath(Path path, boolean clonePath) if (!isClippingPathDirty) { // shallow copy - clippingPaths = new ArrayList(clippingPaths); - + clippingPaths = new ArrayList<>(clippingPaths); isClippingPathDirty = true; } - // add path to current clipping paths, combined later (see getCurrentClippingPath) clippingPaths.add(clonePath ? new Path(path) : path); } @@ -641,6 +618,7 @@ public void intersectClippingPath(Region area) */ public Region getCurrentClippingPath() { + // If there is just a single clipping path, no intersections are needed. if (clippingPaths.size() == 1) { // If there is just a single clipping path, no intersections are needed. @@ -668,8 +646,7 @@ public Region getCurrentClippingPath() } /** - * This will get the current clipping path, as one or more individual paths. Do not modify the - * list or the paths! + * This will get the current clipping path, as one or more individual paths. Do not modify the list or the paths! * * @return The current clipping paths. */ @@ -678,9 +655,17 @@ public List getCurrentClippingPaths() return clippingPaths; } -// public Composite getStrokingJavaComposite() TODO: PdfBox-Android + //TODO: implement getStrokingJavaComposite and getNonStrokingJavaComposite with BlendComposite -// public Composite getNonStrokingJavaComposite() TODO: PdfBox-Android + /*public Composite getStrokingJavaComposite() + { + return BlendComposite.getInstance(blendMode, (float) alphaConstant); + } + + public Composite getNonStrokingJavaComposite() + { + return BlendComposite.getInstance(blendMode, (float) nonStrokingAlphaConstant); + }*/ /** * This will get the transfer function. @@ -741,4 +726,4 @@ public void setTextMatrix(Matrix value) { textMatrix = value; } -} +} \ No newline at end of file diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDSoftMask.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDSoftMask.java index 1e734c69..48affc65 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDSoftMask.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/graphics/state/PDSoftMask.java @@ -24,6 +24,8 @@ import com.ainceborn.pdfbox.cos.COSBase; import com.ainceborn.pdfbox.cos.COSDictionary; import com.ainceborn.pdfbox.cos.COSName; +import com.ainceborn.pdfbox.pdmodel.PDResources; +import com.ainceborn.pdfbox.pdmodel.ResourceCache; import com.ainceborn.pdfbox.pdmodel.common.COSObjectable; import com.ainceborn.pdfbox.pdmodel.common.function.PDFunction; import com.ainceborn.pdfbox.pdmodel.graphics.PDXObject; @@ -41,8 +43,23 @@ public final class PDSoftMask implements COSObjectable * Creates a new soft mask. * * @param dictionary SMask + * + * @return the newly created instance of PDSoftMask */ public static PDSoftMask create(COSBase dictionary) + { + return create(dictionary, null); + } + + /** + * Creates a new soft mask. + * + * @param dictionary SMask + * @param resourceCache Resource cache, may be null. + * + * @return the newly created instance of PDSoftMask + */ + public static PDSoftMask create(COSBase dictionary, ResourceCache resourceCache) { if (dictionary instanceof COSName) { @@ -58,7 +75,7 @@ public static PDSoftMask create(COSBase dictionary) } else if (dictionary instanceof COSDictionary) { - return new PDSoftMask((COSDictionary) dictionary); + return new PDSoftMask((COSDictionary) dictionary, resourceCache); } else { @@ -68,6 +85,7 @@ else if (dictionary instanceof COSDictionary) } private final COSDictionary dictionary; + private final ResourceCache resourceCache; private COSName subType = null; private PDTransparencyGroup group = null; private COSArray backdropColor = null; @@ -84,8 +102,20 @@ else if (dictionary instanceof COSDictionary) * @param dictionary The soft mask dictionary. */ public PDSoftMask(COSDictionary dictionary) + { + this(dictionary, null); + } + + /** + * Creates a new soft mask. + * + * @param dictionary The soft mask dictionary. + * @param resourceCache Resource cache, may be null. + */ + public PDSoftMask(COSDictionary dictionary, ResourceCache resourceCache) { this.dictionary = dictionary; + this.resourceCache = resourceCache; } @Override @@ -96,12 +126,14 @@ public COSDictionary getCOSObject() /** * Returns the subtype of the soft mask (Alpha, Luminosity) - S entry + * + * @return the subtype of the soft mask */ public COSName getSubType() { if (subType == null) { - subType = (COSName) getCOSObject().getDictionaryObject(COSName.S); + subType = getCOSObject().getCOSName(COSName.S); } return subType; } @@ -110,7 +142,7 @@ public COSName getSubType() * Returns the G entry of the soft mask object * * @return form containing the transparency group - * @throws IOException + * @throws IOException if the group could not be read */ public PDTransparencyGroup getGroup() throws IOException { @@ -119,7 +151,8 @@ public PDTransparencyGroup getGroup() throws IOException COSBase cosGroup = getCOSObject().getDictionaryObject(COSName.G); if (cosGroup != null) { - PDXObject x = PDXObject.createXObject(cosGroup, null); + PDResources resources = new PDResources(new COSDictionary(), resourceCache); + PDXObject x = PDXObject.createXObject(cosGroup, resources); if (x instanceof PDTransparencyGroup) { group = (PDTransparencyGroup) x; @@ -131,18 +164,22 @@ public PDTransparencyGroup getGroup() throws IOException /** * Returns the backdrop color. + * + * @return the backdrop color */ public COSArray getBackdropColor() { if (backdropColor == null) { - backdropColor = (COSArray) getCOSObject().getDictionaryObject(COSName.BC); + backdropColor = getCOSObject().getCOSArray(COSName.BC); } return backdropColor; } /** * Returns the transfer function. + * + * @return the transfer function * @throws IOException If we are unable to create the PDFunction object. */ public PDFunction getTransferFunction() throws IOException @@ -161,7 +198,7 @@ public PDFunction getTransferFunction() throws IOException /** * Set the CTM that is valid at the time the ExtGState was activated. * - * @param ctm + * @param ctm the transformation matrix */ void setInitialTransformationMatrix(Matrix ctm) { @@ -171,7 +208,7 @@ void setInitialTransformationMatrix(Matrix ctm) /** * Returns the CTM at the time the ExtGState was activated. * - * @return the CTM at the time the ExtGState was activated. + * @return the transformation matrix */ public Matrix getInitialTransformationMatrix() { diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/TextAlign.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/TextAlign.java new file mode 100644 index 00000000..18b3f93d --- /dev/null +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/TextAlign.java @@ -0,0 +1,30 @@ +package com.ainceborn.pdfbox.pdmodel.interactive; + +public enum TextAlign +{ + LEFT(0), CENTER(1), RIGHT(2), JUSTIFY(4); + + private final int alignment; + + private TextAlign(int alignment) + { + this.alignment = alignment; + } + + int getTextAlign() + { + return alignment; + } + + public static TextAlign valueOf(int alignment) + { + for (TextAlign textAlignment : TextAlign.values()) + { + if (textAlignment.getTextAlign() == alignment) + { + return textAlignment; + } + } + return TextAlign.LEFT; + } +} diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java index dd264755..e4cfb639 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java @@ -306,6 +306,20 @@ public void setAppearanceState(String as) getCOSObject().setName(COSName.AS, as); } + /** + * This will set the annotations appearance state name. + * + *

PDFBOX-6178: Note that the PDF specification defines the AS entry as a name, but some + * PDFs use a string. This method preserves the exact COSName (including byte encoding) which + * is essential for form fields with non-ASCII appearance keys.

+ * + * @param as The COSName of the appearance stream. + */ + public void setAppearanceState(COSName as) + { + getCOSObject().setItem(COSName.AS, as); + } + /** * This will get the appearance dictionary associated with this annotation. This may return null. * diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAppearanceDictionary.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAppearanceDictionary.java index 4a6a6a5c..9d573890 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAppearanceDictionary.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAppearanceDictionary.java @@ -64,12 +64,8 @@ public COSDictionary getCOSObject() */ public PDAppearanceEntry getNormalAppearance() { - COSBase entry = dictionary.getDictionaryObject(COSName.N); - if (entry instanceof COSDictionary) - { - return new PDAppearanceEntry(entry); - } - return null; + COSDictionary entry = dictionary.getCOSDictionary(COSName.N); + return entry != null ? new PDAppearanceEntry(entry) : null; } /** @@ -102,15 +98,8 @@ public void setNormalAppearance(PDAppearanceStream ap) */ public PDAppearanceEntry getRolloverAppearance() { - COSBase entry = dictionary.getDictionaryObject(COSName.R); - if (entry instanceof COSDictionary) - { - return new PDAppearanceEntry(entry); - } - else - { - return getNormalAppearance(); - } + COSDictionary entry = dictionary.getCOSDictionary(COSName.R); + return entry != null ? new PDAppearanceEntry(entry) : getNormalAppearance(); } /** @@ -143,15 +132,8 @@ public void setRolloverAppearance(PDAppearanceStream ap) */ public PDAppearanceEntry getDownAppearance() { - COSBase entry = dictionary.getDictionaryObject(COSName.D); - if (entry instanceof COSDictionary) - { - return new PDAppearanceEntry(entry); - } - else - { - return getNormalAppearance(); - } + COSDictionary entry = dictionary.getCOSDictionary(COSName.D); + return entry != null ? new PDAppearanceEntry(entry) : getNormalAppearance(); } /** diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAppearanceEntry.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAppearanceEntry.java index 4a4d8ffb..508ec5d9 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAppearanceEntry.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/annotation/PDAppearanceEntry.java @@ -33,7 +33,7 @@ */ public class PDAppearanceEntry implements COSObjectable { - private COSBase entry; + private COSDictionary entry; private PDAppearanceEntry() { @@ -42,21 +42,23 @@ private PDAppearanceEntry() /** * Constructor for reading. * - * @param entry + * @param entry the dictionary of the appearance entry */ - public PDAppearanceEntry(COSBase entry) + public PDAppearanceEntry(COSDictionary entry) { this.entry = entry; } @Override - public COSBase getCOSObject() + public COSDictionary getCOSObject() { return entry; } /** * Returns true if this entry is an appearance subdictionary. + * + * @return true if this entry is an appearance subdictionary */ public boolean isSubDictionary() { @@ -65,6 +67,8 @@ public boolean isSubDictionary() /** * Returns true if this entry is an appearance stream. + * + * @return true if this entry is an appearance stream */ public boolean isStream() { @@ -74,6 +78,8 @@ public boolean isStream() /** * Returns the entry as an appearance stream. * + * @return the entry as an appearance stream + * * @throws IllegalStateException if this entry is not an appearance stream */ public PDAppearanceStream getAppearanceStream() @@ -88,6 +94,8 @@ public PDAppearanceStream getAppearanceStream() /** * Returns the entry as an appearance subdictionary. * + * @return the entry as an appearance subdictionary + * * @throws IllegalStateException if this entry is not an appearance subdictionary */ public Map getSubDictionary() @@ -97,20 +105,18 @@ public Map getSubDictionary() throw new IllegalStateException("This entry is not an appearance subdictionary"); } - COSDictionary dict = (COSDictionary) entry; - Map map = new HashMap(); + COSDictionary dict = entry; + Map map = new HashMap<>(); for (COSName name : dict.keySet()) { - COSBase value = dict.getDictionaryObject(name); - + COSStream stream = dict.getCOSStream(name); // the file from PDFBOX-1599 contains /null as its entry, so we skip non-stream entries - if (value instanceof COSStream) + if (stream != null) { - map.put(name, new PDAppearanceStream((COSStream) value)); + map.put(name, new PDAppearanceStream(stream)); } } - - return new COSDictionaryMap(map, dict); + return new COSDictionaryMap<>(map, dict); } } diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java index cbbae365..1ed75d4a 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java @@ -26,6 +26,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; import com.ainceborn.fontbox.util.BoundingBox; import com.ainceborn.harmony.awt.geom.AffineTransform; @@ -35,6 +36,8 @@ import com.ainceborn.pdfbox.cos.COSString; import com.ainceborn.pdfbox.pdfparser.PDFStreamParser; import com.ainceborn.pdfbox.pdfwriter.ContentStreamWriter; +import com.ainceborn.pdfbox.pdmodel.GlyphLayoutProcessorInterface; +import com.ainceborn.pdfbox.pdmodel.PDAppearanceContentStream; import com.ainceborn.pdfbox.pdmodel.PDPageContentStream; import com.ainceborn.pdfbox.pdmodel.PDResources; import com.ainceborn.pdfbox.pdmodel.common.PDRectangle; @@ -65,6 +68,8 @@ class AppearanceGeneratorHelper { private static final Operator BMC = Operator.getOperator("BMC"); private static final Operator EMC = Operator.getOperator("EMC"); + private static final Pattern PATTERN = Pattern.compile("\\u000D\\u000A|[\\u000A\\u000B\\u000C\\u000D\\u0085\\u2028\\u2029]"); + private final PDVariableText field; private PDDefaultAppearanceString defaultAppearance; @@ -73,13 +78,11 @@ class AppearanceGeneratorHelper { /** * The highlight color * - * The color setting is used by Adobe to display the highlight box for selected - * entries in a list box. + * The color setting is used by Adobe to display the highlight box for selected entries in a list box. * - * Regardless of other settings in an existing appearance stream Adobe will - * always use this value. + * Regardless of other settings in an existing appearance stream Adobe will always use this value. */ - private static final float[] HIGHLIGHT_COLOR = { 153 / 255f, 193 / 255f, 215 / 255f }; + private static final float[] HIGHLIGHT_COLOR = {153/255f, 193/255f, 215/255f}; /** * The scaling factor for font units to PDF units @@ -95,7 +98,6 @@ class AppearanceGeneratorHelper { * The minimum/maximum font sizes used for multiline text auto sizing */ private static final float MINIMUM_FONT_SIZE = 4; - private static final float MAXIMUM_FONT_SIZE = 300; /** * The default padding applied by Acrobat to the fields bbox. @@ -108,15 +110,20 @@ class AppearanceGeneratorHelper { * @param field the field which you wish to control the appearance of * @throws IOException */ - AppearanceGeneratorHelper(PDVariableText field) throws IOException { + AppearanceGeneratorHelper(PDVariableText field) throws IOException + { this.field = field; validateAndEnsureAcroFormResources(); - try { + try + { this.defaultAppearance = field.getDefaultAppearanceString(); - } catch (IOException ex) { - throw new IOException("Could not process default appearance string '" + field.getDefaultAppearance() - + "' for field '" + field.getFullyQualifiedName() + "'", ex); + } + catch (IOException ex) + { + throw new IOException("Could not process default appearance string '" + + field.getDefaultAppearance() + "' for field '" + + field.getFullyQualifiedName() + "': " + ex.getMessage(), ex); } } @@ -124,16 +131,17 @@ class AppearanceGeneratorHelper { * Adobe Reader/Acrobat are adding resources which are at the field/widget level * to the AcroForm level. */ - private void validateAndEnsureAcroFormResources() { + private void validateAndEnsureAcroFormResources() + { // add font resources which might be available at the field // level but are not at the AcroForm level to the AcroForm // to match Adobe Reader/Acrobat behavior - if (field.getAcroForm().getDefaultResources() == null) { + PDResources acroFormResources = field.getAcroForm().getDefaultResources(); + if (acroFormResources == null) + { return; } - PDResources acroFormResources = field.getAcroForm().getDefaultResources(); - for (PDAnnotationWidget widget : field.getWidgets()) { PDAppearanceStream stream = widget.getNormalAppearanceStream(); @@ -147,9 +155,9 @@ private void validateAndEnsureAcroFormResources() { continue; } COSDictionary widgetFontDict = widgetResources.getCOSObject() - .getCOSDictionary(COSName.FONT); + .getCOSDictionary(COSName.FONT); COSDictionary acroFormFontDict = acroFormResources.getCOSObject() - .getCOSDictionary(COSName.FONT); + .getCOSDictionary(COSName.FONT); for (COSName fontResourceName : widgetResources.getFontNames()) { try @@ -159,7 +167,7 @@ private void validateAndEnsureAcroFormResources() { Log.d("PdfBox-Android", "Adding font resource " + fontResourceName + " from widget to AcroForm"); // use the COS-object to preserve a possible indirect object reference acroFormFontDict.setItem(fontResourceName, - widgetFontDict.getItem(fontResourceName)); + widgetFontDict.getItem(fontResourceName)); } } catch (IOException e) @@ -175,8 +183,11 @@ private void validateAndEnsureAcroFormResources() { * * @param apValue the String value which the appearance should represent * @throws IOException If there is an error creating the stream. + * @throws IllegalArgumentException if the string contains a character that is not in the field + * font, see {@link #setDefaultAppearance(java.lang.String)}. */ - public void setAppearanceValue(String apValue) throws IOException { + public void setAppearanceValue(String apValue) throws IOException + { value = getFormattedValue(apValue); // Treat multiline field values in single lines as single lime values. @@ -185,12 +196,13 @@ public void setAppearanceValue(String apValue) throws IOException { // set programmatically and Reader is forced to generate the appearance // using PDAcroForm.setNeedAppearances // see PDFBOX-3911 - if (field instanceof PDTextField && !((PDTextField) field).isMultiline()) { - value = value.replaceAll("\\u000D\\u000A|[\\u000A\\u000B\\u000C\\u000D\\u0085\\u2028\\u2029]", " "); + if (field instanceof PDTextField && !((PDTextField) field).isMultiline()) + { + value = PATTERN.matcher(value).replaceAll(" "); } - for (PDAnnotationWidget widget : field.getWidgets()) { - + for (PDAnnotationWidget widget : field.getWidgets()) + { if (widget.getCOSObject().containsKey("PMD")) { Log.w("PdfBox-Android", "widget of field " + field.getFullyQualifiedName() + " is a PaperMetaData widget, no appearance stream created"); @@ -201,12 +213,14 @@ public void setAppearanceValue(String apValue) throws IOException { // widgets differ in layout. PDDefaultAppearanceString acroFormAppearance = defaultAppearance; - if (widget.getCOSObject().getDictionaryObject(COSName.DA) != null) { + if (widget.getCOSObject().getDictionaryObject(COSName.DA) != null) + { defaultAppearance = getWidgetDefaultAppearanceString(widget); } PDRectangle rect = widget.getRectangle(); - if (rect == null) { + if (rect == null) + { widget.getCOSObject().removeItem(COSName.AP); Log.w("PdfBox-Android", "widget of field " + field.getFullyQualifiedName() + " has no rectangle, no appearance stream created"); @@ -214,7 +228,8 @@ public void setAppearanceValue(String apValue) throws IOException { } PDAppearanceDictionary appearanceDict = widget.getAppearance(); - if (appearanceDict == null) { + if (appearanceDict == null) + { appearanceDict = new PDAppearanceDictionary(); widget.setAppearance(appearanceDict); } @@ -223,21 +238,23 @@ public void setAppearanceValue(String apValue) throws IOException { // TODO support appearances other than "normal" PDAppearanceStream appearanceStream; - if (isValidAppearanceStream(appearance)) { + if (isValidAppearanceStream(appearance)) + { appearanceStream = appearance.getAppearanceStream(); - } else { + } + else + { appearanceStream = prepareNormalAppearanceStream(widget); appearanceDict.setNormalAppearance(appearanceStream); // TODO support appearances other than "normal" } PDAppearanceCharacteristicsDictionary appearanceCharacteristics = - widget.getAppearanceCharacteristics(); + widget.getAppearanceCharacteristics(); /* - * Adobe Acrobat always recreates the complete appearance stream if there is an - * appearance characteristics entry (the widget dictionaries MK entry). In - * addition if there is no content yet also create the appearance stream from - * the entries. + * Adobe Acrobat always recreates the complete appearance stream if there is an appearance characteristics + * entry (the widget dictionaries MK entry). In addition if there is no content yet also create the appearance + * stream from the entries. * */ if (appearanceCharacteristics != null || appearanceStream.getContentStream().getLength() == 0) @@ -247,15 +264,15 @@ public void setAppearanceValue(String apValue) throws IOException { setAppearanceContent(widget, appearanceStream); + // restore the field level appearance - defaultAppearance = acroFormAppearance; + defaultAppearance = acroFormAppearance; } } private String getFormattedValue(String apValue) { - // format the field value for the appearance if there is scripting support and - // the field + // format the field value for the appearance if there is scripting support and the field // has a format event PDFormFieldAdditionalActions actions = field.getActions(); if (actions == null) @@ -265,9 +282,9 @@ private String getFormattedValue(String apValue) PDAction actionF = actions.getF(); if (actionF != null) { - if (field.getAcroForm().getScriptingHandler() != null) + ScriptingHandler scriptingHandler = field.getAcroForm().getScriptingHandler(); + if (scriptingHandler != null) { - ScriptingHandler scriptingHandler = field.getAcroForm().getScriptingHandler(); return scriptingHandler.format((PDActionJavaScript) actionF, apValue); } Log.i("PdfBox-Android", "Field contains a formatting action but no ScriptingHandler " + @@ -276,21 +293,26 @@ private String getFormattedValue(String apValue) return apValue; } - private static boolean isValidAppearanceStream(PDAppearanceEntry appearance) { - if (appearance == null) { + private static boolean isValidAppearanceStream(PDAppearanceEntry appearance) + { + if (appearance == null) + { return false; } - if (!appearance.isStream()) { + if (!appearance.isStream()) + { return false; } PDRectangle bbox = appearance.getAppearanceStream().getBBox(); - if (bbox == null) { + if (bbox == null) + { return false; } return Math.abs(bbox.getWidth()) > 0 && Math.abs(bbox.getHeight()) > 0; } - private PDAppearanceStream prepareNormalAppearanceStream(PDAnnotationWidget widget) { + private PDAppearanceStream prepareNormalAppearanceStream(PDAnnotationWidget widget) + { PDAppearanceStream appearanceStream = new PDAppearanceStream(field.getAcroForm().getDocument()); // Calculate the entries for the bounding box and the transformation matrix @@ -304,7 +326,8 @@ private PDAppearanceStream prepareNormalAppearanceStream(PDAnnotationWidget widg appearanceStream.setBBox(bbox); AffineTransform at = calculateMatrix(bbox, rotation); - if (!at.isIdentity()) { + if (!at.isIdentity()) + { appearanceStream.setMatrix(at); } appearanceStream.setFormType(1); @@ -312,15 +335,18 @@ private PDAppearanceStream prepareNormalAppearanceStream(PDAnnotationWidget widg return appearanceStream; } - private PDDefaultAppearanceString getWidgetDefaultAppearanceString(PDAnnotationWidget widget) throws IOException { + private PDDefaultAppearanceString getWidgetDefaultAppearanceString(PDAnnotationWidget widget) throws IOException + { COSString da = (COSString) widget.getCOSObject().getDictionaryObject(COSName.DA); PDResources dr = field.getAcroForm().getDefaultResources(); return new PDDefaultAppearanceString(da, dr); } - private int resolveRotation(PDAnnotationWidget widget) { - PDAppearanceCharacteristicsDictionary characteristicsDictionary = widget.getAppearanceCharacteristics(); - if (characteristicsDictionary != null) { + private int resolveRotation(PDAnnotationWidget widget) + { + PDAppearanceCharacteristicsDictionary characteristicsDictionary = widget.getAppearanceCharacteristics(); + if (characteristicsDictionary != null) + { // 0 is the default value if the R key doesn't exist return characteristicsDictionary.getRotation(); } @@ -330,62 +356,81 @@ private int resolveRotation(PDAnnotationWidget widget) { /** * Initialize the content of the appearance stream. * - * Get settings like border style, border width and colors to be used to draw a - * rectangle and background color around the widget + * Get settings like border style, border width and colors to be used to draw a rectangle and background color + * around the widget * - * @param widget the field widget - * @param appearanceStream the appearance stream to be used + * @param widget the field widget * @param appearanceCharacteristics the appearance characteristics dictionary from the widget or * null + * @param appearanceStream the appearance stream to be used * @throws IOException in case we can't write to the appearance stream */ private void initializeAppearanceContent(PDAnnotationWidget widget, - PDAppearanceCharacteristicsDictionary appearanceCharacteristics, - PDAppearanceStream appearanceStream) - throws IOException + PDAppearanceCharacteristicsDictionary appearanceCharacteristics, + PDAppearanceStream appearanceStream) throws IOException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - PDPageContentStream contents = new PDPageContentStream(field.getAcroForm().getDocument(), appearanceStream, - output); - - // TODO: support more entries like patterns, etc. - if (appearanceCharacteristics != null) { - PDColor backgroundColour = appearanceCharacteristics.getBackground(); - if (backgroundColour != null) { - contents.setNonStrokingColor(backgroundColour); - PDRectangle bbox = resolveBoundingBox(widget, appearanceStream); - contents.addRect(bbox.getLowerLeftX(), bbox.getLowerLeftY(), bbox.getWidth(), bbox.getHeight()); - contents.fill(); - } - - float lineWidth = 0f; - PDColor borderColour = appearanceCharacteristics.getBorderColour(); - if (borderColour != null) { - contents.setStrokingColor(borderColour); - lineWidth = 1f; - } - PDBorderStyleDictionary borderStyle = widget.getBorderStyle(); - if (borderStyle != null && borderStyle.getWidth() > 0) { - lineWidth = borderStyle.getWidth(); - } - - if (lineWidth > 0 && borderColour != null) { - if (lineWidth != 1) { - contents.setLineWidth(lineWidth); + try (ByteArrayOutputStream output = new ByteArrayOutputStream(); + PDAppearanceContentStream contents = new PDAppearanceContentStream(appearanceStream, output)) + { + // TODO: support more entries like patterns, etc. + if (appearanceCharacteristics != null) + { + PDColor backgroundColour = appearanceCharacteristics.getBackground(); + if (backgroundColour != null) + { + contents.setNonStrokingColor(backgroundColour); + PDRectangle bbox = resolveBoundingBox(widget, appearanceStream); + contents.addRect(bbox.getLowerLeftX(),bbox.getLowerLeftY(),bbox.getWidth(), bbox.getHeight()); + contents.fill(); + } + + float lineWidth = 0f; + PDColor borderColour = appearanceCharacteristics.getBorderColour(); + if (borderColour != null) + { + contents.setStrokingColor(borderColour); + lineWidth = 1f; + } + PDBorderStyleDictionary borderStyle = widget.getBorderStyle(); + if (borderStyle != null && borderStyle.getWidth() > 0) + { + lineWidth = borderStyle.getWidth(); + } + + if (lineWidth > 0 && borderColour != null) + { + if (Float.compare(lineWidth, 1) != 0) + { + contents.setLineWidth(lineWidth); + } + PDRectangle bbox = resolveBoundingBox(widget, appearanceStream); + PDRectangle clipRect = applyPadding(bbox, Math.max(DEFAULT_PADDING, lineWidth/2)); + contents.addRect(clipRect.getLowerLeftX(),clipRect.getLowerLeftY(),clipRect.getWidth(), clipRect.getHeight()); + contents.closeAndStroke(); + } + + // draw the dividers for a comb field + if (borderColour != null && shallComb()) { + int maxLen = ((PDTextField) field).getMaxLen(); + PDRectangle bbox = resolveBoundingBox(widget, appearanceStream); + PDRectangle clipRect = applyPadding(bbox, Math.max(DEFAULT_PADDING, lineWidth/2)); + float lowerLeft = clipRect.getLowerLeftX(); + float height = clipRect.getHeight(); + + float combWidth = bbox.getWidth() / maxLen; + + for (int i= 0; i < maxLen - 1; i++) { + contents.moveTo(combWidth + combWidth * i, height); + contents.lineTo(combWidth + combWidth * i, lowerLeft); + } + contents.closeAndStroke(); } - PDRectangle bbox = resolveBoundingBox(widget, appearanceStream); - PDRectangle clipRect = applyPadding(bbox, Math.max(DEFAULT_PADDING, lineWidth / 2)); - contents.addRect(clipRect.getLowerLeftX(), clipRect.getLowerLeftY(), clipRect.getWidth(), - clipRect.getHeight()); - contents.closeAndStroke(); } - } - contents.close(); - output.close(); - writeToStream(output.toByteArray(), appearanceStream); - } + writeToStream(output.toByteArray(), appearanceStream); + } + } /** * Constructs and sets new contents for given appearance stream. @@ -438,30 +483,40 @@ private void setAppearanceContent(PDAnnotationWidget widget, /** * Generate and insert text content and clipping around it. */ - private void insertGeneratedAppearance(PDAnnotationWidget widget, PDAppearanceStream appearanceStream, - OutputStream output) throws IOException { - PDPageContentStream contents = new PDPageContentStream(field.getAcroForm().getDocument(), appearanceStream, - output); + private void insertGeneratedAppearance(PDAnnotationWidget widget, + PDAppearanceStream appearanceStream, + OutputStream output) throws IOException + { + try (PDAppearanceContentStream contents = new PDAppearanceContentStream(appearanceStream, output)) + { + GlyphLayoutProcessorInterface glyphLayoutProcessor = field.getAcroForm().getGlyphLayoutProcessor(); + if (glyphLayoutProcessor != null) + { + contents.setGlyphLayoutProcessor(glyphLayoutProcessor); + } + PDRectangle bbox = resolveBoundingBox(widget, appearanceStream); - PDRectangle bbox = resolveBoundingBox(widget, appearanceStream); + // Acrobat calculates the left and right padding dependent on the offset of the border edge + // This calculation works for forms having been generated by Acrobat. + // The minimum distance is always 1f even if there is no rectangle being drawn around. + float borderWidth = 0; + if (widget.getBorderStyle() != null) + { + borderWidth = widget.getBorderStyle().getWidth(); + } + float padding = Math.max(1f, borderWidth); + PDRectangle clipRect = applyPadding(bbox, padding); + float clipRectLowerLeftY = clipRect.getLowerLeftY(); + float clipRectHeight = clipRect.getHeight(); - // Acrobat calculates the left and right padding dependent on the offset of the - // border edge - // This calculation works for forms having been generated by Acrobat. - // The minimum distance is always 1f even if there is no rectangle being drawn - // around. - float borderWidth = 0; - if (widget.getBorderStyle() != null) { - borderWidth = widget.getBorderStyle().getWidth(); - } - PDRectangle clipRect = applyPadding(bbox, Math.max(1f, borderWidth)); - PDRectangle contentRect = applyPadding(clipRect, Math.max(1f, borderWidth)); + PDRectangle contentRect = applyPadding(clipRect, padding); - contents.saveGraphicsState(); + contents.saveGraphicsState(); - // Acrobat always adds a clipping path - contents.addRect(clipRect.getLowerLeftX(), clipRect.getLowerLeftY(), clipRect.getWidth(), clipRect.getHeight()); - contents.clip(); + // Acrobat always adds a clipping path + contents.addRect(clipRect.getLowerLeftX(), clipRectLowerLeftY, + clipRect.getWidth(), clipRectHeight); + contents.clip(); // get the font PDFont font = defaultAppearance.getFont(); @@ -482,116 +537,135 @@ private void insertGeneratedAppearance(PDAnnotationWidget widget, PDAppearanceSt // calculate the fontSize (because 0 = autosize) float fontSize = defaultAppearance.getFontSize(); - if (fontSize == 0) { - fontSize = calculateFontSize(font, contentRect); - } - - // for a listbox generate the highlight rectangle for the selected - // options - if (field instanceof PDListBox) { - insertGeneratedListboxSelectionHighlight(contents, appearanceStream, font, fontSize); - } + if (Float.compare(fontSize, 0) == 0) + { + fontSize = calculateFontSize(font, contentRect); + } - // start the text output - contents.beginText(); + // for a listbox generate the highlight rectangle for the selected + // options + if (field instanceof PDListBox) + { + insertGeneratedListboxSelectionHighlight(contents, appearanceStream, font, fontSize); + } - // write font and color from the /DA string, with the calculated font size - defaultAppearance.writeTo(contents, fontSize); + // start the text output + contents.beginText(); - // calculate the y-position of the baseline - float y; + // write font and color from the /DA string, with the calculated font size + defaultAppearance.writeTo(contents, fontSize); - // calculate font metrics at font size - float fontScaleY = fontSize / FONTSCALE; - float fontBoundingBoxAtSize = font.getBoundingBox().getHeight() * fontScaleY; + // calculate the y-position of the baseline + float y; - float fontCapAtSize; - float fontDescentAtSize; + // calculate font metrics at font size + float fontScaleY = fontSize / FONTSCALE; + float fontBoundingBoxAtSize = font.getBoundingBox().getHeight() * fontScaleY; - if (font.getFontDescriptor() != null) { - fontCapAtSize = font.getFontDescriptor().getCapHeight() * fontScaleY; - fontDescentAtSize = font.getFontDescriptor().getDescent() * fontScaleY; - } else { - float fontCapHeight = resolveCapHeight(font); - float fontDescent = resolveDescent(font); - Log.d("PdfBox-Android", "missing font descriptor - resolved Cap/Descent to " + fontCapHeight + "/" + fontDescent); - fontCapAtSize = fontCapHeight * fontScaleY; - fontDescentAtSize = fontDescent * fontScaleY; - } + float fontCapAtSize; + float fontDescentAtSize; - if (field instanceof PDTextField && ((PDTextField) field).isMultiline()) { - y = contentRect.getUpperRightY() - fontBoundingBoxAtSize; - } else { - // Adobe shows the text 'shifted up' in case the caps don't fit into the - // clipping area - if (fontCapAtSize > clipRect.getHeight()) { - y = clipRect.getLowerLeftY() + -fontDescentAtSize; + if (font.getFontDescriptor() != null) { + fontCapAtSize = font.getFontDescriptor().getCapHeight() * fontScaleY; + fontDescentAtSize = font.getFontDescriptor().getDescent() * fontScaleY; } else { - // calculate the position based on the content rectangle - y = clipRect.getLowerLeftY() + (clipRect.getHeight() - fontCapAtSize) / 2; + float fontCapHeight = resolveCapHeight(font); + float fontDescent = resolveDescent(font); + Log.d("PdfBox-Android", "missing font descriptor - resolved Cap/Descent to " + fontCapHeight + "/" + fontDescent); + fontCapAtSize = fontCapHeight * fontScaleY; + fontDescentAtSize = fontDescent * fontScaleY; + } - // check to ensure that ascents and descents fit - if (y - clipRect.getLowerLeftY() < -fontDescentAtSize) { + if (field instanceof PDTextField && ((PDTextField) field).isMultiline()) + { + y = contentRect.getUpperRightY() - fontBoundingBoxAtSize; + } + else + { + // Adobe shows the text 'shifted up' in case the caps don't fit into the clipping area + if (fontCapAtSize > clipRectHeight) + { + y = clipRectLowerLeftY + -fontDescentAtSize; + } + else + { + // calculate the position based on the content rectangle + y = clipRectLowerLeftY + (clipRectHeight - fontCapAtSize) / 2; - float fontDescentBased = -fontDescentAtSize + contentRect.getLowerLeftY(); - float fontCapBased = contentRect.getHeight() - contentRect.getLowerLeftY() - fontCapAtSize; + // check to ensure that ascents and descents fit + if (y - clipRectLowerLeftY < -fontDescentAtSize) + { + float contentRectLowerLeftY = contentRect.getLowerLeftY(); + float fontDescentBased = -fontDescentAtSize + contentRectLowerLeftY; + float fontCapBased = contentRect.getHeight() - contentRectLowerLeftY - fontCapAtSize; - y = Math.min(fontDescentBased, Math.max(y, fontCapBased)); + y = Math.min(fontDescentBased, Math.max(y, fontCapBased)); + } } } - } - // show the text - float x = contentRect.getLowerLeftX(); + // show the text + float x = contentRect.getLowerLeftX(); - // special handling for comb boxes as these are like table cells with individual - // chars - if (shallComb()) { - insertGeneratedCombAppearance(contents, appearanceStream, font, fontSize); - } else if (field instanceof PDListBox) { - insertGeneratedListboxAppearance(contents, appearanceStream, contentRect, font, fontSize); - } else { - PlainText textContent = new PlainText(value); - AppearanceStyle appearanceStyle = new AppearanceStyle(); - appearanceStyle.setFont(font); - appearanceStyle.setFontSize(fontSize); - - // Adobe Acrobat uses the font's bounding box for the leading between the lines - appearanceStyle.setLeading(font.getBoundingBox().getHeight() * fontScaleY); - - PlainTextFormatter formatter = new PlainTextFormatter.Builder(contents) - .style(appearanceStyle) - .text(textContent) - .width(contentRect.getWidth()) - .wrapLines(isMultiLine()) - .initialOffset(x, y) - .textAlign(getTextAlign(widget)) - .build(); - formatter.format(); - } - - contents.endText(); - contents.restoreGraphicsState(); - contents.close(); + // special handling for comb boxes as these are like table cells with individual + // chars + if (shallComb()) + { + insertGeneratedCombAppearance(contents, appearanceStream, font, fontSize); + } + else if (field instanceof PDListBox) + { + insertGeneratedListboxAppearance(contents, appearanceStream, contentRect, font, fontSize); + } + else + { + PlainText textContent = new PlainText(value); + AppearanceStyle appearanceStyle = new AppearanceStyle(); + appearanceStyle.setFont(font); + appearanceStyle.setFontSize(fontSize); + + // Adobe Acrobat uses the font's bounding box for the leading between the lines + appearanceStyle.setLeading(font.getBoundingBox().getHeight() * fontScaleY); + + PlainTextFormatter formatter = new PlainTextFormatter + .Builder(contents) + .style(appearanceStyle) + .text(textContent) + .width(contentRect.getWidth()) + .wrapLines(isMultiLine()) + .initialOffset(x, y) + .textAlign(getTextAlign(widget)) + .build(); + formatter.format(); + } + + contents.endText(); + contents.restoreGraphicsState(); + } } /* - * PDFBox handles a widget with a joined in field dictionary and without an - * individual name as a widget only. As a result - as a widget can't have a + * PDFBox handles a widget with a joined in field dictionary and without + * an individual name as a widget only. As a result - as a widget can't have a * quadding /Q entry we need to do a low level access to the dictionary and * otherwise get the quadding from the field. */ - private int getTextAlign(PDAnnotationWidget widget) { + private int getTextAlign(PDAnnotationWidget widget) + { // Use quadding value from joined field/widget if set, else use from field. return widget.getCOSObject().getInt(COSName.Q, field.getQ()); } - private AffineTransform calculateMatrix(PDRectangle bbox, int rotation) { - if (rotation == 0) { + + private AffineTransform calculateMatrix(PDRectangle bbox, int rotation) + { + if (rotation == 0) + { return new AffineTransform(); } float tx = 0, ty = 0; - switch (rotation) { + switch (rotation) + { case 90: tx = bbox.getUpperRightY(); break; @@ -607,10 +681,10 @@ private AffineTransform calculateMatrix(PDRectangle bbox, int rotation) { } Matrix matrix = Matrix.getRotateInstance(Math.toRadians(rotation), tx, ty); return matrix.createAffineTransform(); - } - private boolean isMultiLine() { + private boolean isMultiLine() + { return field instanceof PDTextField && ((PDTextField) field).isMultiline(); } @@ -619,62 +693,79 @@ private boolean isMultiLine() { * *

* May be set only if the MaxLen entry is present in the text field dictionary - * and if the Multiline, Password, and FileSelect flags are clear. If set, the - * field shall be automatically divided into as many equally spaced positions, + * and if the Multiline, Password, and FileSelect flags are clear. + * If set, the field shall be automatically divided into as many equally spaced positions, * or combs, as the value of MaxLen, and the text is laid out into those combs. *

* * @return the comb state */ - private boolean shallComb() { - return field instanceof PDTextField && ((PDTextField) field).isComb() && !((PDTextField) field).isMultiline() - && !((PDTextField) field).isPassword() && !((PDTextField) field).isFileSelect(); + private boolean shallComb() + { + return field instanceof PDTextField && + ((PDTextField) field).isComb() && + ((PDTextField) field).getMaxLen() != -1 && + !((PDTextField) field).isMultiline() && + !((PDTextField) field).isPassword() && + !((PDTextField) field).isFileSelect(); } /** * Generate the appearance for comb fields. * - * @param contents the content stream to write to + * @param contents the content stream to write to * @param appearanceStream the appearance stream used - * @param font the font to be used - * @param fontSize the font size to be used + * @param font the font to be used + * @param fontSize the font size to be used * @throws IOException */ - private void insertGeneratedCombAppearance(PDPageContentStream contents, PDAppearanceStream appearanceStream, - PDFont font, float fontSize) throws IOException { + private void insertGeneratedCombAppearance(PDAppearanceContentStream contents, PDAppearanceStream appearanceStream, + PDFont font, float fontSize) throws IOException + { + if (value == null || value.isEmpty()) + { + return; + } int maxLen = ((PDTextField) field).getMaxLen(); int quadding = field.getQ(); int numChars = Math.min(value.length(), maxLen); - PDRectangle paddingEdge = applyPadding(appearanceStream.getBBox(), 1); - - float combWidth = appearanceStream.getBBox().getWidth() / maxLen; + PDRectangle bBox = appearanceStream.getBBox(); + float combWidth = bBox.getWidth() / maxLen; float ascentAtFontSize = font.getFontDescriptor().getAscent() / FONTSCALE * fontSize; - float baselineOffset = paddingEdge.getLowerLeftY() - + (appearanceStream.getBBox().getHeight() - ascentAtFontSize) / 2; + + float baselineOffset = bBox.getLowerLeftY() + (bBox.getHeight() - ascentAtFontSize) / 2; float prevCharWidth = 0f; - float xOffset = combWidth / 2; + // set initial offset based on width of first char. + float firstCharWidth = font.getStringWidth(value.substring(0, 1)) / FONTSCALE * fontSize; + float initialOffset = (combWidth - firstCharWidth)/2; // add to initial offset if right aligned or centered if (quadding == 2) { - xOffset = xOffset + (maxLen - numChars) * combWidth; + initialOffset = initialOffset + (maxLen - numChars) * combWidth; } else if (quadding == 1) { - xOffset = xOffset + (maxLen - numChars) / 2 * combWidth; + initialOffset = initialOffset + Math.floorDiv(maxLen - numChars, 2) * combWidth; } + float xOffset = initialOffset; + for (int i = 0; i < numChars; i++) { - String combString = value.substring(i, i + 1); - float currCharWidth = font.getStringWidth(combString) / FONTSCALE * fontSize / 2; + String combString = value.substring(i, i+1); + float currCharWidth = font.getStringWidth(combString) / FONTSCALE * fontSize/2; - xOffset = xOffset + prevCharWidth / 2 - currCharWidth / 2; + xOffset = xOffset + prevCharWidth/2 - currCharWidth/2; - contents.newLineAtOffset(xOffset, baselineOffset); + if (i == 0) { + contents.newLineAtOffset(initialOffset, baselineOffset); + } else { + contents.newLineAtOffset(xOffset, baselineOffset); + } contents.showText(combString); baselineOffset = 0; @@ -683,8 +774,8 @@ else if (quadding == 1) } } - private void insertGeneratedListboxSelectionHighlight(PDPageContentStream contents, - PDAppearanceStream appearanceStream, PDFont font, float fontSize) throws IOException + private void insertGeneratedListboxSelectionHighlight(PDAppearanceContentStream contents, PDAppearanceStream appearanceStream, + PDFont font, float fontSize) throws IOException { PDListBox listBox = (PDListBox) field; List indexEntries = listBox.getSelectedOptionsIndex(); @@ -694,7 +785,7 @@ private void insertGeneratedListboxSelectionHighlight(PDPageContentStream conten if (!values.isEmpty() && !options.isEmpty() && indexEntries.isEmpty()) { // create indexEntries from options - indexEntries = new ArrayList(values.size()); + indexEntries = new ArrayList<>(values.size()); for (String v : values) { indexEntries.add(options.indexOf(v)); @@ -711,34 +802,42 @@ private void insertGeneratedListboxSelectionHighlight(PDPageContentStream conten // the padding area PDRectangle paddingEdge = applyPadding(appearanceStream.getBBox(), 1); - for (int selectedIndex : indexEntries) { + for (int selectedIndex : indexEntries) + { contents.setNonStrokingColor(HIGHLIGHT_COLOR[0], HIGHLIGHT_COLOR[1], HIGHLIGHT_COLOR[2]); contents.addRect(paddingEdge.getLowerLeftX(), - paddingEdge.getUpperRightY() - highlightBoxHeight * (selectedIndex - topIndex + 1) + 2, - paddingEdge.getWidth(), highlightBoxHeight); + paddingEdge.getUpperRightY() - highlightBoxHeight * (selectedIndex - topIndex + 1) + 2, + paddingEdge.getWidth(), + highlightBoxHeight); contents.fill(); } contents.setNonStrokingColor(0f); } - private void insertGeneratedListboxAppearance(PDPageContentStream contents, PDAppearanceStream appearanceStream, - PDRectangle contentRect, PDFont font, float fontSize) throws IOException { + + private void insertGeneratedListboxAppearance(PDAppearanceContentStream contents, PDAppearanceStream appearanceStream, + PDRectangle contentRect, PDFont font, float fontSize) throws IOException + { contents.setNonStrokingColor(0f); int q = field.getQ(); - if (q == PDVariableText.QUADDING_CENTERED || q == PDVariableText.QUADDING_RIGHT) { + if (q == PDVariableText.QUADDING_CENTERED || q == PDVariableText.QUADDING_RIGHT) + { float fieldWidth = appearanceStream.getBBox().getWidth(); float stringWidth = (font.getStringWidth(value) / FONTSCALE) * fontSize; float adjustAmount = fieldWidth - stringWidth - 4; - if (q == PDVariableText.QUADDING_CENTERED) { + if (q == PDVariableText.QUADDING_CENTERED) + { adjustAmount = adjustAmount / 2.0f; } contents.newLineAtOffset(adjustAmount, 0); - } else if (q != PDVariableText.QUADDING_LEFT) { + } + else if (q != PDVariableText.QUADDING_LEFT) + { throw new IOException("Error: Unknown justification value:" + q); } @@ -751,11 +850,14 @@ private void insertGeneratedListboxAppearance(PDPageContentStream contents, PDAp float ascent = font.getFontDescriptor().getAscent(); float height = font.getBoundingBox().getHeight(); - for (int i = topIndex; i < numOptions; i++) { - - if (i == topIndex) { + for (int i = topIndex; i < numOptions; i++) + { + if (i == topIndex) + { yTextPos = yTextPos - ascent / FONTSCALE * fontSize; - } else { + } + else + { yTextPos = yTextPos - height / FONTSCALE * fontSize; contents.beginText(); } @@ -763,7 +865,8 @@ private void insertGeneratedListboxAppearance(PDPageContentStream contents, PDAp contents.newLineAtOffset(contentRect.getLowerLeftX(), yTextPos); contents.showText(options.get(i)); - if (i != (numOptions - 1)) { + if (i != (numOptions - 1)) + { contents.endText(); } } @@ -774,33 +877,41 @@ private void insertGeneratedListboxAppearance(PDPageContentStream contents, PDAp * * @throws IOException If there is an error writing to the stream */ - private void writeToStream(byte[] data, PDAppearanceStream appearanceStream) throws IOException { - OutputStream out = appearanceStream.getCOSObject().createOutputStream(); - out.write(data); - out.close(); + private void writeToStream(byte[] data, PDAppearanceStream appearanceStream) throws IOException + { + try (OutputStream out = appearanceStream.getCOSObject().createOutputStream()) + { + out.write(data); + } } /** - * My "not so great" method for calculating the fontsize. It does not work - * superb, but it handles ok. + * My "not so great" method for calculating the fontsize. It does not work superb, but it + * handles ok. * * @return the calculated font-size * @throws IOException If there is an error getting the font information. */ - private float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOException { + private float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOException + { float fontSize = defaultAppearance.getFontSize(); // zero is special, it means the text is auto-sized - if (fontSize == 0) { - if (isMultiLine()) { + if (Float.compare(fontSize, 0) == 0) + { + if (isMultiLine()) + { PlainText textContent = new PlainText(value); - if (textContent.getParagraphs() != null) { + if (textContent.getParagraphs() != null) + { float width = contentRect.getWidth() - contentRect.getLowerLeftX(); float fs = MINIMUM_FONT_SIZE; - while (fs <= DEFAULT_FONT_SIZE) { + while (fs <= DEFAULT_FONT_SIZE) + { // determine the number of lines needed for this font and contentRect int numLines = 0; - for (PlainText.Paragraph paragraph : textContent.getParagraphs()) { + for (PlainText.Paragraph paragraph : textContent.getParagraphs()) + { numLines += paragraph.getLines(font, fs, width).size(); } // calculate the height required for this font size @@ -809,32 +920,42 @@ private float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOE float height = leading * numLines; // if this font size didn't fit, use the prior size that did fit - if (height > contentRect.getHeight()) { + if (height > contentRect.getHeight()) + { return Math.max(fs - 1, MINIMUM_FONT_SIZE); } - fs++; + fs += 1.0; } return Math.min(fs, DEFAULT_FONT_SIZE); } // Acrobat defaults to 12 for multiline text with size 0 return DEFAULT_FONT_SIZE; - } else { - float yScalingFactor = FONTSCALE * font.getFontMatrix().getScaleY(); - float xScalingFactor = FONTSCALE * font.getFontMatrix().getScaleX(); + } + else + { + Matrix fontMatrix = font.getFontMatrix(); + float yScalingFactor = FONTSCALE * fontMatrix.getScaleY(); + float xScalingFactor = FONTSCALE * fontMatrix.getScaleX(); // fit width - float width = font.getStringWidth(value) * font.getFontMatrix().getScaleX(); + float width = font.getStringWidth(value) * fontMatrix.getScaleX(); float widthBasedFontSize = contentRect.getWidth() / width * xScalingFactor; // fit height - float height = (font.getFontDescriptor().getCapHeight() + -font.getFontDescriptor().getDescent()) - * font.getFontMatrix().getScaleY(); - if (height <= 0) { - height = font.getBoundingBox().getHeight() * font.getFontMatrix().getScaleY(); + float height = (font.getFontDescriptor().getCapHeight() + + -font.getFontDescriptor().getDescent()) * fontMatrix.getScaleY(); + if (height <= 0) + { + height = font.getBoundingBox().getHeight() * fontMatrix.getScaleY(); } float heightBasedFontSize = contentRect.getHeight() / height * yScalingFactor; + if (Float.isInfinite(widthBasedFontSize)) + { + // PDFBOX-5763: avoids -Infinity if empty value and tiny rectangle + return heightBasedFontSize; + } return Math.min(heightBasedFontSize, widthBasedFontSize); } @@ -908,13 +1029,16 @@ private float resolveGlyphHeight(PDFont font, int code) throws IOException { /** * Resolve the bounding box. * - * @param fieldWidget the annotation widget. + * @param fieldWidget the annotation widget. * @param appearanceStream the annotations appearance stream. * @return the resolved boundingBox. */ - private PDRectangle resolveBoundingBox(PDAnnotationWidget fieldWidget, PDAppearanceStream appearanceStream) { + private PDRectangle resolveBoundingBox(PDAnnotationWidget fieldWidget, + PDAppearanceStream appearanceStream) + { PDRectangle boundingBox = appearanceStream.getBBox(); - if (boundingBox == null) { + if (boundingBox == null) + { boundingBox = fieldWidget.getRectangle().createRetranslatedRectangle(); } return boundingBox; @@ -926,8 +1050,11 @@ private PDRectangle resolveBoundingBox(PDAnnotationWidget fieldWidget, PDAppeara * @param box box * @return the padded box. */ - private PDRectangle applyPadding(PDRectangle box, float padding) { - return new PDRectangle(box.getLowerLeftX() + padding, box.getLowerLeftY() + padding, - box.getWidth() - 2 * padding, box.getHeight() - 2 * padding); + private PDRectangle applyPadding(PDRectangle box, float padding) + { + return new PDRectangle(box.getLowerLeftX() + padding, + box.getLowerLeftY() + padding, + box.getWidth() - 2 * padding, + box.getHeight() - 2 * padding); } } \ No newline at end of file diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDAcroForm.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDAcroForm.java index aac2b711..0a3cd966 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDAcroForm.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDAcroForm.java @@ -21,6 +21,7 @@ import android.util.Log; import java.io.IOException; +import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -28,6 +29,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import com.ainceborn.pdfbox.cos.COSArray; @@ -35,6 +37,7 @@ import com.ainceborn.pdfbox.cos.COSDictionary; import com.ainceborn.pdfbox.cos.COSName; import com.ainceborn.pdfbox.cos.COSNumber; +import com.ainceborn.pdfbox.pdmodel.GlyphLayoutProcessorInterface; import com.ainceborn.pdfbox.pdmodel.PDDocument; import com.ainceborn.pdfbox.pdmodel.PDPage; import com.ainceborn.pdfbox.pdmodel.PDPageContentStream; @@ -48,6 +51,7 @@ import com.ainceborn.pdfbox.pdmodel.fdf.FDFDictionary; import com.ainceborn.pdfbox.pdmodel.fdf.FDFDocument; import com.ainceborn.pdfbox.pdmodel.fdf.FDFField; +import com.ainceborn.pdfbox.pdmodel.font.PDFont; import com.ainceborn.pdfbox.pdmodel.graphics.form.PDFormXObject; import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; @@ -71,6 +75,10 @@ public final class PDAcroForm implements COSObjectable private ScriptingHandler scriptingHandler; + private final Map> directFontCache = new HashMap<>(); + + private GlyphLayoutProcessorInterface glyphLayoutProcessor; + /** * Constructor. * @@ -84,7 +92,7 @@ public PDAcroForm(PDDocument doc) } /** - * Constructor. Side effect: /Helv and /ZaDb fonts added with update mark. + * Constructor. * * @param doc The document that this form is part of. * @param form The existing acroForm. @@ -95,6 +103,26 @@ public PDAcroForm(PDDocument doc, COSDictionary form) dictionary = form; } + /** + * Sets the glyph layout processor + * + * @param glyphLayoutProcessor glyph layout processor + */ + public void setGlyphLayoutProcessor(GlyphLayoutProcessorInterface glyphLayoutProcessor) + { + this.glyphLayoutProcessor = glyphLayoutProcessor; + } + + /** + * Returns the glyph layout processor or null + * + * @return the glyph layout processor or null + */ + public GlyphLayoutProcessorInterface getGlyphLayoutProcessor() + { + return glyphLayoutProcessor; + } + /** * This will get the document associated with this form. * @@ -145,38 +173,51 @@ public void importFDF(FDFDocument fdf) throws IOException public FDFDocument exportFDF() throws IOException { FDFDocument fdf = new FDFDocument(); - FDFCatalog catalog = fdf.getCatalog(); - FDFDictionary fdfDict = new FDFDictionary(); - catalog.setFDF(fdfDict); - - List fields = getFields(); - List fdfFields = new ArrayList(fields.size()); - for (PDField field : fields) + try { - fdfFields.add(field.exportFDF()); - } + FDFCatalog catalog = fdf.getCatalog(); + FDFDictionary fdfDict = new FDFDictionary(); + catalog.setFDF(fdfDict); - fdfDict.setID(document.getDocument().getDocumentID()); + List fields = getFields(); + List fdfFields = new ArrayList<>(fields.size()); + for (PDField field : fields) + { + fdfFields.add(field.exportFDF()); + } + + fdfDict.setID(document.getDocument().getDocumentID()); - if (!fdfFields.isEmpty()) + if (!fdfFields.isEmpty()) + { + fdfDict.setFields(fdfFields); + } + return fdf; + } + catch (Exception e) { - fdfDict.setFields(fdfFields); + fdf.close(); + throw e; } - return fdf; } /** * This will flatten all form fields. * - *

Flattening a form field will take the current appearance and make that part - * of the pages content stream. All form fields and annotations associated are removed.

+ *

+ * Flattening a form field will take the current appearance and make that part of the pages content stream. All form + * fields and annotations associated are removed. + *

* - *

Invisible and hidden fields will be skipped and will not become part of the - * page content stream

+ *

+ * Invisible and hidden fields will be skipped and will not become part of the page content stream + *

* - *

The appearances for the form fields widgets will not be generated

+ *

+ * The appearances for the form fields widgets will not be generated + *

* - * @throws IOException + * @throws IOException if something went wrong flattening the fields */ public void flatten() throws IOException { @@ -188,7 +229,7 @@ public void flatten() throws IOException return; } - List fields = new ArrayList(); + List fields = new ArrayList<>(); for (PDField field: getFieldTree()) { fields.add(field); @@ -200,15 +241,18 @@ public void flatten() throws IOException /** * This will flatten the specified form fields. * - *

Flattening a form field will take the current appearance and make that part - * of the pages content stream. All form fields and annotations associated are removed.

+ *

+ * Flattening a form field will take the current appearance and make that part of the pages content stream. All form + * fields and annotations associated are removed. + *

* - *

Invisible and hidden fields will be skipped and will not become part of the - * page content stream

+ *

+ * Invisible and hidden fields will be skipped and will not become part of the page content stream + *

* - * @param fields + * @param fields a list of fields to be flattened * @param refreshAppearances if set to true the appearances for the form field widgets will be updated - * @throws IOException + * @throws IOException if something went wrong flattening the fields */ public void flatten(List fields, boolean refreshAppearances) throws IOException { @@ -240,20 +284,20 @@ public void flatten(List fields, boolean refreshAppearances) throws IOE refreshAppearances(fields); } - // get the widgets per page PDPageTree pages = document.getPages(); Map> pagesWidgetsMap = buildPagesWidgetsMap(fields, pages); // preserve all non widget annotations for (PDPage page : pages) { + // get the widgets that are to be flattened for this page Set widgetsForPageMap = pagesWidgetsMap.get(page.getCOSObject()); // indicates if the original content stream // has been wrapped in a q...Q pair. boolean isContentStreamWrapped = false; - List annotations = new ArrayList(); + List annotations = new ArrayList<>(); for (PDAnnotation annotation: page.getAnnotations()) { @@ -263,10 +307,13 @@ public void flatten(List fields, boolean refreshAppearances) throws IOE } else if (isVisibleAnnotation(annotation)) { - PDPageContentStream contentStream = new PDPageContentStream( - document, page, AppendMode.APPEND, true, !isContentStreamWrapped); - try + try (PDPageContentStream contentStream = new PDPageContentStream( + document, page, AppendMode.APPEND, true, !isContentStreamWrapped)) { + if (glyphLayoutProcessor != null) + { + contentStream.setGlyphLayoutProcessor(glyphLayoutProcessor); + } isContentStreamWrapped = true; PDAppearanceStream appearanceStream = annotation.getNormalAppearanceStream(); @@ -280,14 +327,11 @@ else if (isVisibleAnnotation(annotation)) // this will transform the appearance stream form object into the rectangle of the // annotation bbox and map the coordinate systems Matrix transformationMatrix = resolveTransformationMatrix(annotation, appearanceStream); + contentStream.transform(transformationMatrix); contentStream.drawForm(fieldObject); contentStream.restoreGraphicsState(); } - finally - { - contentStream.close(); - } } } page.setAnnotations(annotations); @@ -322,10 +366,9 @@ private boolean isVisibleAnnotation(PDAnnotation annotation) } /** - * Refreshes the appearance streams and appearance dictionaries for - * the widget annotations of all fields. + * Refreshes the appearance streams and appearance dictionaries for the widget annotations of all fields. * - * @throws IOException + * @throws IOException if the appearance streams could not be refreshed */ public void refreshAppearances() throws IOException { @@ -339,11 +382,10 @@ public void refreshAppearances() throws IOException } /** - * Refreshes the appearance streams and appearance dictionaries for - * the widget annotations of the specified fields. + * Refreshes the appearance streams and appearance dictionaries for the widget annotations of the specified fields. * - * @param fields - * @throws IOException + * @param fields a list of fields to be refreshed + * @throws IOException if the appearance streams could not be refreshed */ public void refreshAppearances(List fields) throws IOException { @@ -363,7 +405,7 @@ public void refreshAppearances(List fields) throws IOException * A field might have children that are fields (non-terminal field) or does not * have children which are fields (terminal fields). * - * The fields within an AcroForm are organized in a tree structure. The documents root fields + * The fields within an AcroForm are organized in a tree structure. The documents root fields * might either be terminal fields, non-terminal fields or a mixture of both. Non-terminal fields * mark branches which contents can be retrieved using {@link PDNonTerminalField#getChildren()}. * @@ -377,7 +419,7 @@ public List getFields() { return Collections.emptyList(); } - List pdFields = new ArrayList(); + List pdFields = new ArrayList<>(); for (int i = 0; i < cosFields.size(); i++) { COSBase element = cosFields.getObject(i); @@ -390,7 +432,7 @@ public List getFields() } } } - return new COSArrayList(pdFields, cosFields); + return new COSArrayList<>(pdFields, cosFields); } /** @@ -400,11 +442,13 @@ public List getFields() */ public void setFields(List fields) { - dictionary.setItem(COSName.FIELDS, COSArrayList.converterToCOSArray(fields)); + dictionary.setItem(COSName.FIELDS, new COSArray(fields)); } /** * Returns an iterator which walks all fields in the field tree, in order. + * + * @return an iterator which walks all fields in the field tree */ public Iterator getFieldIterator() { @@ -413,6 +457,8 @@ public Iterator getFieldIterator() /** * Return the field tree representing all form fields + * + * @return the field tree representing all form fields */ public PDFieldTree getFieldTree() { @@ -431,7 +477,7 @@ public void setCacheFields(boolean cache) { if (cache) { - fieldCache = new HashMap(); + fieldCache = new HashMap<>(); for (PDField field : getFieldTree()) { @@ -458,7 +504,7 @@ public boolean isCachingFields() * This will get a field by name, possibly using the cache if setCache is true. * * @param fullyQualifiedName The name of the field to get. - * @return The field with that name of null if one was not found. + * @return The first field with that name of null if one was not found. */ public PDField getField(String fullyQualifiedName) { @@ -471,7 +517,7 @@ public PDField getField(String fullyQualifiedName) // get the field from the field tree for (PDField field : getFieldTree()) { - if (field.getFullyQualifiedName().equals(fullyQualifiedName)) + if (Objects.equals(field.getFullyQualifiedName(), fullyQualifiedName)) { return field; } @@ -529,13 +575,9 @@ public void setNeedAppearances(Boolean value) */ public PDResources getDefaultResources() { - PDResources retval = null; - COSBase base = dictionary.getDictionaryObject(COSName.DR); - if (base instanceof COSDictionary) - { - retval = new PDResources((COSDictionary) base, document.getResourceCache()); - } - return retval; + COSDictionary dr = dictionary.getCOSDictionary(COSName.DR); + return dr != null ? new PDResources(dr, document.getResourceCache(), directFontCache) + : null; } /** @@ -575,13 +617,8 @@ public boolean xfaIsDynamic() */ public PDXFAResource getXFA() { - PDXFAResource xfa = null; COSBase base = dictionary.getDictionaryObject(COSName.XFA); - if (base != null) - { - xfa = new PDXFAResource(base); - } - return xfa; + return base != null ? new PDXFAResource(base) : null; } /** @@ -596,7 +633,7 @@ public void setXFA(PDXFAResource xfa) /** * This will get the document-wide default value for the quadding/justification of variable text - * fields. + * fields. *

* 0 - Left(default)
* 1 - Centered
@@ -607,13 +644,7 @@ public void setXFA(PDXFAResource xfa) */ public int getQ() { - int retval = 0; - COSNumber number = (COSNumber)dictionary.getDictionaryObject(COSName.Q); - if (number != null) - { - retval = number.intValue(); - } - return retval; + return dictionary.getInt(COSName.Q, 0); } /** @@ -657,6 +688,26 @@ public boolean isAppendOnly() return dictionary.getFlag(COSName.SIG_FLAGS, FLAG_APPEND_ONLY); } + /** + * Set a handler to support JavaScript actions in the form. + * + * @return scriptingHandler the handler to support JavaScript actions in the form + */ + public ScriptingHandler getScriptingHandler() + { + return scriptingHandler; + } + + /** + * Set a handler to support JavaScript actions in the form. + * + * @param scriptingHandler a handler to support JavaScript actions in the form + */ + public void setScriptingHandler(ScriptingHandler scriptingHandler) + { + this.scriptingHandler = scriptingHandler; + } + /** * Set the AppendOnly bit. * @@ -668,23 +719,49 @@ public void setAppendOnly(boolean appendOnly) } /** - * Set a handler to support JavaScript actions in the form. + * Return the calculation order in which field values should be recalculated when the value of + * any field changes. (Read about "Trigger Events" in the PDF specification) * - * @return scriptingHandler + * @return field list. Note these objects may not be identical to PDField objects retrieved from + * other methods (depending on cache setting). The best strategy is to call + * {@link #getCOSObject()} to check for identity. The list is not backed by the /CO COSArray in + * the document. */ - public ScriptingHandler getScriptingHandler() + public List getCalcOrder() { - return scriptingHandler; + COSArray co = dictionary.getCOSArray(COSName.CO); + if (co == null) + { + return Collections.emptyList(); + } + + Iterable fields = isCachingFields() ? fieldCache.values() : getFieldTree(); + + List actuals = new ArrayList<>(); + for (int i = 0; i < co.size(); i++) + { + COSBase item = co.getObject(i); + for (PDField field : fields) + { + if (field.getCOSObject() == item) + { + actuals.add(field); + break; + } + } + } + return actuals; } /** - * Set a handler to support JavaScript actions in the form. + * Set the calculation order in which field values should be recalculated when the value of any + * field changes. (Read about "Trigger Events" in the PDF specification) * - * @param scriptingHandler + * @param fields The field list. */ - public void setScriptingHandler(ScriptingHandler scriptingHandler) + public void setCalcOrder(List fields) { - this.scriptingHandler = scriptingHandler; + dictionary.setItem(COSName.CO, new COSArray(fields)); } private Matrix resolveTransformationMatrix(PDAnnotation annotation, PDAppearanceStream appearanceStream) @@ -719,11 +796,17 @@ private RectF getTransformedAppearanceBBox(PDAppearanceStream appearanceStream) return bounds; } + /** + * Build a map of page => set of widgets to be flattened + * + * @param fields a list of fields to be flattened + * @param pages the page tree + * @return + */ private Map> buildPagesWidgetsMap( - List fields, PDPageTree pages) throws IOException + List fields, PDPageTree pages) { - Map> pagesAnnotationsMap = - new HashMap>(); + Map> pagesAnnotationsMap = new HashMap<>(); boolean hasMissingPageRef = false; for (PDField field : fields) @@ -738,6 +821,7 @@ private Map> buildPagesWidgetsMap( } else { + Log.w("PdfBox-Android", "missing /P entry (page reference) in a widget for field: " + field); hasMissingPageRef = true; } } @@ -748,14 +832,15 @@ private Map> buildPagesWidgetsMap( return pagesAnnotationsMap; } - // If there is a widget with a missing page reference we need to build the map reverse i.e. + // If there is a widget with a missing page reference we need to build the map reverse i.e. // from the annotations to the widget. Log.w("PdfBox-Android", "There has been a widget with a missing page reference, will check all page annotations"); + Set widgetDictionarySet = createWidgetDictionarySet(fields); for (PDPage page : pages) { for (PDAnnotation annotation : page.getAnnotations()) { - if (annotation instanceof PDAnnotationWidget) + if (widgetDictionarySet.contains(annotation.getCOSObject())) { fillPagesAnnotationMap(pagesAnnotationsMap, page, (PDAnnotationWidget) annotation); } @@ -765,13 +850,33 @@ private Map> buildPagesWidgetsMap( return pagesAnnotationsMap; } + /** + * Return a set of all annotation widget dictionaries related to the fields to be flattened. + * + * @param fields + * @return + */ + private Set createWidgetDictionarySet(List fields) + { + Set widgetDictionarySet = new HashSet<>(); + for (PDField field : fields) + { + List widgets = field.getWidgets(); + for (PDAnnotationWidget widget : widgets) + { + widgetDictionarySet.add(widget.getCOSObject()); + } + } + return widgetDictionarySet; + } + private void fillPagesAnnotationMap(Map> pagesAnnotationsMap, - PDPage page, PDAnnotationWidget widget) + PDPage page, PDAnnotationWidget widget) { Set widgetsForPage = pagesAnnotationsMap.get(page.getCOSObject()); if (widgetsForPage == null) { - widgetsForPage = new HashSet(); + widgetsForPage = new HashSet<>(); widgetsForPage.add(widget.getCOSObject()); pagesAnnotationsMap.put(page.getCOSObject(), widgetsForPage); } @@ -789,12 +894,12 @@ private void removeFields(List fields) if (field.getParent() == null) { // if the field has no parent, assume it is at root level list, remove it from there - array = (COSArray) dictionary.getDictionaryObject(COSName.FIELDS); + array = dictionary.getCOSArray(COSName.FIELDS); } else { // if the field has a parent, then remove from the list there - array = (COSArray) field.getParent().getCOSObject().getDictionaryObject(COSName.KIDS); + array = field.getParent().getCOSObject().getCOSArray(COSName.KIDS); } array.removeObject(field.getCOSObject()); } diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDButton.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDButton.java index d76c209e..8a8c3a98 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDButton.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDButton.java @@ -16,19 +16,18 @@ */ package com.ainceborn.pdfbox.pdmodel.interactive.form; +import com.ainceborn.pdfbox.cos.COSArray; +import com.ainceborn.pdfbox.cos.COSBase; +import com.ainceborn.pdfbox.cos.COSDictionary; +import com.ainceborn.pdfbox.cos.COSName; +import com.ainceborn.pdfbox.cos.COSString; + import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import com.ainceborn.pdfbox.cos.COSArray; -import com.ainceborn.pdfbox.cos.COSBase; -import com.ainceborn.pdfbox.cos.COSDictionary; -import com.ainceborn.pdfbox.cos.COSName; -import com.ainceborn.pdfbox.cos.COSString; -import com.ainceborn.pdfbox.pdmodel.common.COSArrayList; import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry; @@ -62,7 +61,7 @@ public abstract class PDButton extends PDTerminalField * * @param acroForm The acroform. */ - public PDButton(PDAcroForm acroForm) + PDButton(PDAcroForm acroForm) { super(acroForm); getCOSObject().setItem(COSName.FT, COSName.BTN); @@ -90,22 +89,6 @@ public boolean isPushButton() return getCOSObject().getFlag(COSName.FF, FLAG_PUSHBUTTON); } - /** - * Set the push button bit. - * - * @deprecated use {@link com.ainceborn.pdfbox.pdmodel.interactive.form.PDPushButton} instead - * @param pushbutton if true the button field is treated as a push button field. - */ - @Deprecated - public void setPushButton(boolean pushbutton) - { - getCOSObject().setFlag(COSName.FF, FLAG_PUSHBUTTON, pushbutton); - if (pushbutton) - { - setRadioButton(false); - } - } - /** * Determines if radio button bit is set. * @@ -116,22 +99,6 @@ public boolean isRadioButton() return getCOSObject().getFlag(COSName.FF, FLAG_RADIO); } - /** - * Set the radio button bit. - * - * @deprecated use {@link com.ainceborn.pdfbox.pdmodel.interactive.form.PDRadioButton} instead - * @param radiobutton if true the button field is treated as a radio button field. - */ - @Deprecated - public void setRadioButton(boolean radiobutton) - { - getCOSObject().setFlag(COSName.FF, FLAG_RADIO, radiobutton); - if (radiobutton) - { - setPushButton(false); - } - } - /** * Returns the selected value. * @@ -173,8 +140,7 @@ public String getValue() } /** - * Sets the selected option given its name. It also tries to update the visual appearance, - * unless {@link PDAcroForm#getNeedAppearances()} is true. + * Set the selected option given its name, and try to update the visual appearance. * * @param value Name of option to select * @throws IOException if the value could not be set @@ -185,11 +151,10 @@ public void setValue(String value) throws IOException { checkValue(value); - // if there are export values/an Opt entry there is a different + // if there are export values/an Opt entry there is a different // approach to setting the value - boolean hasExportValues = getExportValues().size() > 0; - - if (hasExportValues) { + if (!getExportValues().isEmpty()) + { updateByOption(value); } else @@ -203,7 +168,7 @@ public void setValue(String value) throws IOException /** * Set the selected option given its index, and try to update the visual appearance. * - * NOTE: this method is only usable if there are export values and used for + * NOTE: this method is only usable if there are export values and used for * radio buttons with FLAG_RADIOS_IN_UNISON not set. * * @param index index of option to be selected @@ -212,11 +177,12 @@ public void setValue(String value) throws IOException */ public void setValue(int index) throws IOException { - if (getExportValues().isEmpty() || index < 0 || index >= getExportValues().size()) + List exportValues = getExportValues(); + if (exportValues.isEmpty() || index < 0 || index >= exportValues.size()) { throw new IllegalArgumentException("index '" + index - + "' is not a valid index for the field " + getFullyQualifiedName() - + ", valid indices are from 0 to " + (getExportValues().size() - 1)); + + "' is not a valid index for the field " + getFullyQualifiedName() + + ", valid indices are from 0 to " + (exportValues.size() - 1)); } updateByValue(String.valueOf(index)); @@ -225,7 +191,6 @@ public void setValue(int index) throws IOException } - /** * Returns the default value, if any. * @@ -269,7 +234,7 @@ public String getValueAsString() *

The export values are defined in the field dictionaries /Opt key.

* *

The option values are used to define the export values - * for the field to + * for the field to *

    *
  • hold values in non-Latin writing systems as name objects, which represent the field value, are limited * to PDFDocEncoding @@ -288,13 +253,16 @@ public List getExportValues() if (value instanceof COSString) { - List array = new ArrayList(); - array.add(((COSString) value).getString()); - return array; + String stringValue = ((COSString) value).getString(); + if (stringValue.isEmpty()) + { + return Collections.emptyList(); + } + return Collections.singletonList(stringValue); } else if (value instanceof COSArray) { - return COSArrayList.convertCOSStringCOSArrayToList((COSArray)value); + return ((COSArray) value).toCOSStringStringList(); } return Collections.emptyList(); } @@ -310,7 +278,7 @@ public void setExportValues(List values) COSArray cosValues; if (values != null && !values.isEmpty()) { - cosValues = COSArrayList.convertStringListToCOSStringCOSArray(values); + cosValues = COSArray.ofCOSStrings(values); getCOSObject().setItem(COSName.OPT, cosValues); } else @@ -322,27 +290,23 @@ public void setExportValues(List values) @Override void constructAppearances() throws IOException { - List exportValues = getExportValues(); - if (exportValues.size() > 0) + for (PDAnnotationWidget widget : getWidgets()) { - // the value is the index value of the option. So we need to get that - // and use it to set the value - try + PDAppearanceDictionary appearance = widget.getAppearance(); + if (appearance == null) { - int optionsIndex = Integer.parseInt(getValue()); - if (optionsIndex < exportValues.size()) - { - updateByOption(exportValues.get(optionsIndex)); - } - } catch (NumberFormatException e) + continue; + } + PDAppearanceEntry appearanceEntry = appearance.getNormalAppearance(); + COSName value = getCOSObject().getCOSName(COSName.V); + if (appearanceEntry.getCOSObject().containsKey(value)) { - // silently ignore that - // and don't update the appearance + widget.setAppearanceState(value); + } + else + { + widget.setAppearanceState(COSName.Off); } - } - else - { - updateByValue(getValue()); } } @@ -353,17 +317,17 @@ void constructAppearances() throws IOException * a PDF name object. The Off value shall always be 'Off'. If not set or not part of the normal * appearance keys 'Off' is the default

    * - * @return the potential values setting the check box to the On state. + * @return the potential values setting the check box to the On state. * If an empty Set is returned there is no appearance definition. */ public Set getOnValues() { // we need a set as the field can appear multiple times - Set onValues = new LinkedHashSet(); - - if (getExportValues().size() > 0) + Set onValues = new LinkedHashSet<>(); + List exportValues = getExportValues(); + if (!exportValues.isEmpty()) { - onValues.addAll(getExportValues()); + onValues.addAll(exportValues); return onValues; } @@ -412,7 +376,6 @@ private String getOnValueForWidget(PDAnnotationWidget widget) return ""; } - /** * Checks value. * @@ -425,34 +388,87 @@ void checkValue(String value) if (COSName.Off.getName().compareTo(value) != 0 && !onValues.contains(value)) { throw new IllegalArgumentException("value '" + value - + "' is not a valid option for the field " + getFullyQualifiedName() - + ", valid values are: " + onValues + " and " + COSName.Off.getName()); + + "' is not a valid option for the field " + getFullyQualifiedName() + + ", valid values are: " + onValues + " and " + COSName.Off.getName()); } } - private void updateByValue(String value) throws IOException + private void updateByValue(String value) { - getCOSObject().setName(COSName.V, value); - // update the appearance state (AS) + // Find the matching appearance key from the first widget that has it + COSName matchingKey = null; + + // update the appearance state (AS) for each widget for (PDAnnotationWidget widget : getWidgets()) { - if (widget.getAppearance() == null) + PDAppearanceDictionary appearance = widget.getAppearance(); + if (appearance == null) { continue; } - PDAppearanceEntry appearanceEntry = widget.getAppearance().getNormalAppearance(); - if (((COSDictionary) appearanceEntry.getCOSObject()).containsKey(value)) + PDAppearanceEntry appearanceEntry = appearance.getNormalAppearance(); + COSDictionary appearanceDict = appearanceEntry.getCOSObject(); + + // Find the matching appearance key by searching through the actual keys + // and comparing their decoded names. This handles encoding differences: + // the appearance key might be ISO-8859-1 encoded (e.g. /m#e4nnlich for "männlich") + // while the value String is UTF-8. + COSName widgetMatchingKey = findMatchingAppearanceKey(appearanceDict, value); + + // Save the first matching key to use for the V entry + if (widgetMatchingKey != null && matchingKey == null) { - widget.setAppearanceState(value); + matchingKey = widgetMatchingKey; + } + + if (widgetMatchingKey != null) + { + // Use the exact COSName from the appearance dictionary to preserve encoding + widget.setAppearanceState(widgetMatchingKey); } else { - widget.setAppearanceState(COSName.Off.getName()); + // Fall back to Off if no match found for this widget + widget.setAppearanceState(COSName.Off); + } + } + + // Set the V entry once using the first matching key found + if (matchingKey != null) + { + getCOSObject().setItem(COSName.V, matchingKey); + } + else + { + // Fall back to UTF-8 encoding if no match found in any widget + getCOSObject().setName(COSName.V, value); + } + } + + /** + * Find the appearance dictionary key that matches the given value String. + * This method handles encoding differences - the value might be UTF-8 while + * appearance keys in the PDF could be ISO-8859-1 or other encodings. + * + * @param appearanceDict the appearance dictionary with keys to search + * @param value the value String to match against (typically UTF-8) + * @return the matching COSName key, or null if no match found + */ + private COSName findMatchingAppearanceKey(COSDictionary appearanceDict, String value) + { + // Search all keys in the appearance dictionary and compare their decoded names + // COSName.getName() uses UTF-8 decoding with ISO-8859-1 fallback for non-UTF-8 bytes + for (COSName key : appearanceDict.keySet()) + { + if (value.equals(key.getName())) + { + return key; } } + return null; } - private void updateByOption(String value) throws IOException + private void updateByOption(String value) { List widgets = getWidgets(); List options = getExportValues(); @@ -480,5 +496,4 @@ private void updateByOption(String value) throws IOException } } } - } diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDChoice.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDChoice.java index 70333e0c..6ef875ef 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDChoice.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDChoice.java @@ -50,7 +50,7 @@ public abstract class PDChoice extends PDVariableText * * @param acroForm The acroform. */ - public PDChoice(PDAcroForm acroForm) + protected PDChoice(PDAcroForm acroForm) { super(acroForm); getCOSObject().setItem(COSName.FT, COSName.CH); @@ -75,9 +75,9 @@ public PDChoice(PDAcroForm acroForm) * For a choice field the options array can either be an array * of text strings or an array of a two-element arrays.
    * The method always only returns either the text strings or, - * in case of two-element arrays, an array of the first element of + * in case of two-element arrays, an array of the first element of * the two-element arrays - *

    + *

    *

    * Use {@link #getOptionsExportValues()} and {@link #getOptionsDisplayValues()} * to get the entries of two-element arrays. @@ -96,13 +96,13 @@ public List getOptions() * *

    * The Opt array specifies the list of options in the choice field either - * as an array of text strings representing the display value + * as an array of text strings representing the display value * or as an array of a two-element array where the * first element is the export value and the second the display value. *

    *

    * To set both the export and the display value use {@link #setOptions(List, List)} - *

    + *

    * * @param displayValues List containing all possible options. */ @@ -114,7 +114,8 @@ public void setOptions(List displayValues) { Collections.sort(displayValues); } - getCOSObject().setItem(COSName.OPT, COSArrayList.convertStringListToCOSStringCOSArray(displayValues)); + getCOSObject().setItem(COSName.OPT, + COSArray.ofCOSStrings(displayValues)); } else { @@ -127,7 +128,7 @@ public void setOptions(List displayValues) * *

    * This will set both, the export value and the display value - * of the choice field. If either one of the parameters is null or an + * of the choice field. If either one of the parameters is null or an * empty list is supplied the options will * be removed. *

    @@ -147,7 +148,7 @@ public void setOptions(List exportValues, List displayValues) if (exportValues.size() != displayValues.size()) { throw new IllegalArgumentException( - "The number of entries for exportValue and displayValue shall be the same."); + "The number of entries for exportValue and displayValue shall be the same."); } else { @@ -162,8 +163,9 @@ public void setOptions(List exportValues, List displayValues) for (int i = 0; i exportValues, List displayValues) *

    * For options with an array of text strings the display value and export value * are the same.
    - * For options with an array of two-element arrays the display value is the + * For options with an array of two-element arrays the display value is the * second entry in the two-element array. *

    * @@ -199,7 +201,7 @@ public List getOptionsDisplayValues() *

    * For options with an array of text strings the display value and export value * are the same.
    - * For options with an array of two-element arrays the export value is the + * For options with an array of two-element arrays the export value is the * first entry in the two-element array. *

    * @@ -210,6 +212,18 @@ public List getOptionsExportValues() return getOptions(); } + /** + * This will check if the field has dedicated display and export values. + * + * @return true if export and display values are different + */ + public boolean hasSeparateExportAndDisplayValues() + { + List exportValues = getOptionsExportValues(); + List displayValues = getOptionsDisplayValues(); + return !exportValues.equals(displayValues); + } + /** * This will get the indices of the selected options - the 'I' key. *

    @@ -223,12 +237,8 @@ public List getOptionsExportValues() */ public List getSelectedOptionsIndex() { - COSBase value = getCOSObject().getDictionaryObject(COSName.I); - if (value != null) - { - return COSArrayList.convertIntegerCOSArrayToList((COSArray) value); - } - return Collections.emptyList(); + COSArray value = getCOSObject().getCOSArray(COSName.I); + return value != null ? value.toCOSNumberIntegerList() : Collections.emptyList(); } /** @@ -251,9 +261,9 @@ public void setSelectedOptionsIndex(List values) if (!isMultiSelect()) { throw new IllegalArgumentException( - "Setting the indices is not allowed for choice fields not allowing multiple selections."); + "Setting the indices is not allowed for choice fields not allowing multiple selections."); } - getCOSObject().setItem(COSName.I, COSArrayList.converterToCOSArray(values)); + getCOSObject().setItem(COSName.I, COSArray.ofCOSIntegers(values)); } else { @@ -267,7 +277,7 @@ public void setSelectedOptionsIndex(List values) *

    * If set, the field’s option items shall be sorted alphabetically. * The sorting has to be done when writing the PDF. PDF Readers are supposed to - * display the options in the order in which they occur in the Opt array. + * display the options in the order in which they occur in the Opt array. *

    * * @return true if the options are sorted. @@ -369,8 +379,7 @@ public void setCombo(boolean combo) } /** - * Sets the selected value of this field. It also tries to update the visual appearance, unless - * {@link PDAcroForm#getNeedAppearances()} is true. + * Set the selected value of this field, and try to update the visual appearance. * * @param value The name of the selected item. * @throws IOException if the value could not be set @@ -390,9 +399,8 @@ public void setValue(String value) throws IOException * Sets the default value of this field. * * @param value The name of the selected item. - * @throws IOException if the value could not be set */ - public void setDefaultValue(String value) throws IOException + public void setDefaultValue(String value) { getCOSObject().setString(COSName.DV, value); } @@ -411,12 +419,14 @@ public void setValue(List values) throws IOException { throw new IllegalArgumentException("The list box does not allow multiple selections."); } - if (!getOptions().containsAll(values)) + List options = getOptions(); + if (!options.containsAll(values)) { throw new IllegalArgumentException("The values are not contained in the selectable options."); } - getCOSObject().setItem(COSName.V, COSArrayList.convertStringListToCOSStringCOSArray(values)); - updateSelectedOptionsIndex(values); + getCOSObject().setItem(COSName.V, + COSArray.ofCOSStrings(values)); + updateSelectedOptionsIndex(values, options); } else { @@ -456,13 +466,11 @@ private List getValueFor(COSName name) COSBase value = getCOSObject().getDictionaryObject(name); if (value instanceof COSString) { - List array = new ArrayList(); - array.add(((COSString) value).getString()); - return array; + return Collections.singletonList(((COSString) value).getString()); } else if (value instanceof COSArray) { - return COSArrayList.convertCOSStringCOSArrayToList((COSArray)value); + return ((COSArray) value).toCOSStringStringList(); } return Collections.emptyList(); } @@ -476,10 +484,9 @@ public String getValueAsString() /** * Update the 'I' key based on values set. */ - private void updateSelectedOptionsIndex(List values) + private void updateSelectedOptionsIndex(List values, List options) { - List options = getOptions(); - List indices = new ArrayList(); + List indices = new ArrayList<>(values.size()); for (String value : values) { @@ -493,4 +500,4 @@ private void updateSelectedOptionsIndex(List values) @Override abstract void constructAppearances() throws IOException; -} +} \ No newline at end of file diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDComboBox.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDComboBox.java index 7639e2fe..1755e42d 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDComboBox.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDComboBox.java @@ -45,7 +45,7 @@ public PDComboBox(PDAcroForm acroForm) /** * Constructor. - * + * * @param acroForm The form that this field is part of. * @param field the PDF object to represent as a field. * @param parent the parent node of the node @@ -57,7 +57,7 @@ public PDComboBox(PDAcroForm acroForm) /** * Determines if Edit is set. - * + * * @return true if the combo box shall include an editable text box as well as a drop-down list. */ public boolean isEdit() @@ -74,7 +74,7 @@ public void setEdit(boolean edit) { getCOSObject().setFlag(COSName.FF, FLAG_EDIT, edit); } - + @Override void constructAppearances() throws IOException { @@ -84,6 +84,16 @@ void constructAppearances() throws IOException if (!values.isEmpty()) { + if (hasSeparateExportAndDisplayValues()) + { + List displayValues = getOptionsDisplayValues(); + int index = getOptions().indexOf(values.get(0)); + if (index != -1 && index < displayValues.size()) + { + apHelper.setAppearanceValue(displayValues.get(index)); + return; + } + } apHelper.setAppearanceValue(values.get(0)); } else diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDDefaultAppearanceString.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDDefaultAppearanceString.java index 95318a42..b19cddb9 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDDefaultAppearanceString.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDDefaultAppearanceString.java @@ -29,6 +29,7 @@ import com.ainceborn.pdfbox.cos.COSNumber; import com.ainceborn.pdfbox.cos.COSString; import com.ainceborn.pdfbox.pdfparser.PDFStreamParser; +import com.ainceborn.pdfbox.pdmodel.PDAppearanceContentStream; import com.ainceborn.pdfbox.pdmodel.PDPageContentStream; import com.ainceborn.pdfbox.pdmodel.PDResources; import com.ainceborn.pdfbox.pdmodel.font.PDFont; @@ -75,7 +76,7 @@ class PDDefaultAppearanceString if (defaultAppearance == null) { throw new IllegalArgumentException("/DA is a required entry. " - + "Please set a default appearance first."); + + "Please set a default appearance first."); } if (defaultResources == null) @@ -95,7 +96,7 @@ class PDDefaultAppearanceString */ private void processAppearanceStringOperators(byte[] content) throws IOException { - List arguments = new ArrayList(); + List arguments = new ArrayList<>(); PDFStreamParser parser = new PDFStreamParser(content); Object token = parser.parseNextToken(); while (token != null) @@ -103,7 +104,7 @@ private void processAppearanceStringOperators(byte[] content) throws IOException if (token instanceof Operator) { processOperator((Operator) token, arguments); - arguments = new ArrayList(); + arguments = new ArrayList<>(); } else { @@ -122,23 +123,18 @@ private void processAppearanceStringOperators(byte[] content) throws IOException */ private void processOperator(Operator operator, List operands) throws IOException { - String name = operator.getName(); - - if (OperatorName.SET_FONT_AND_SIZE.equals(name)) - { - processSetFont(operands); - } - else if (OperatorName.NON_STROKING_GRAY.equals(name)) - { - processSetFontColor(operands); - } - else if (OperatorName.NON_STROKING_RGB.equals(name)) - { - processSetFontColor(operands); - } - else if (OperatorName.NON_STROKING_CMYK.equals(name)) + switch (operator.getName()) { - processSetFontColor(operands); + case OperatorName.SET_FONT_AND_SIZE: + processSetFont(operands); + break; + case OperatorName.NON_STROKING_GRAY: + case OperatorName.NON_STROKING_RGB: + case OperatorName.NON_STROKING_CMYK: + processSetFontColor(operands); + break; + default: + break; } } @@ -286,12 +282,16 @@ void setFontColor(PDColor fontColor) } /** - * Writes the DA string to the given content stream. + * Write font name, font size and color from the /DA string to the given content stream. + * + * @param contents The content stream. + * @param zeroFontSize The calculated font size to use if the /DA string has a size 0 + * (autosize). Otherwise the size from the /DA string is used. */ - void writeTo(PDPageContentStream contents, float zeroFontSize) throws IOException + void writeTo(PDAppearanceContentStream contents, float zeroFontSize) throws IOException { float fontSize = getFontSize(); - if (fontSize == 0) + if (Float.compare(fontSize, 0) == 0) { fontSize = zeroFontSize; } diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDField.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDField.java index 0655899f..05eab86f 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDField.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDField.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.List; +import java.util.Objects; import com.ainceborn.pdfbox.cos.COSArray; import com.ainceborn.pdfbox.cos.COSBase; @@ -224,12 +225,8 @@ public void setFieldFlags(int flags) */ public PDFormFieldAdditionalActions getActions() { - COSDictionary aa = (COSDictionary) dictionary.getDictionaryObject(COSName.AA); - if (aa != null) - { - return new PDFormFieldAdditionalActions(aa); - } - return null; + COSDictionary aa = dictionary.getCOSDictionary(COSName.AA); + return aa != null ? new PDFormFieldAdditionalActions(aa) : null; } /** @@ -260,7 +257,7 @@ else if (fieldValue instanceof COSStream) } else if (fieldValue instanceof COSArray && this instanceof PDChoice) { - ((PDChoice) this).setValue(COSArrayList.convertCOSStringCOSArrayToList((COSArray) fieldValue)); + ((PDChoice) this).setValue(((COSArray) fieldValue).toCOSStringStringList()); } else { @@ -336,7 +333,7 @@ public PDNonTerminalField getParent() PDField findKid(String[] name, int nameIndex) { PDField retval = null; - COSArray kids = (COSArray) dictionary.getDictionaryObject(COSName.KIDS); + COSArray kids = dictionary.getCOSArray(COSName.KIDS); if (kids != null) { for (int i = 0; retval == null && i < kids.size(); i++) @@ -345,7 +342,7 @@ PDField findKid(String[] name, int nameIndex) if (name[nameIndex].equals(kidDictionary.getString(COSName.T))) { retval = PDField.fromDictionary(acroForm, kidDictionary, - (PDNonTerminalField)this); + (PDNonTerminalField)this); if (retval != null && name.length > nameIndex + 1) { retval = retval.findKid(name, nameIndex + 1); @@ -398,7 +395,7 @@ public void setPartialName(String name) if (name.contains(".")) { throw new IllegalArgumentException( - "A field partial name shall not contain a period character: " + name); + "A field partial name shall not contain a period character: " + name); } dictionary.setString(COSName.T, name); } @@ -478,6 +475,35 @@ public void setMappingName(String mappingName) public String toString() { return getFullyQualifiedName() + "{type: " + getClass().getSimpleName() + " value: " + - getInheritableAttribute(COSName.V) + "}"; + getInheritableAttribute(COSName.V) + "}"; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals (Object o) + { + if (o == this) + { + return true; + } + + if (!(o instanceof PDField)) + { + return false; + } + + COSDictionary toBeCompared = ((PDField) o).getCOSObject(); + return toBeCompared.equals(getCOSObject()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() + { + return Objects.hash(dictionary); } } diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDTerminalField.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDTerminalField.java index abecad43..b469ed09 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDTerminalField.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDTerminalField.java @@ -164,14 +164,14 @@ FDFField exportFDF() throws IOException @Override public List getWidgets() { - List widgets = new ArrayList(); - COSArray kids = (COSArray)getCOSObject().getDictionaryObject(COSName.KIDS); + List widgets = new ArrayList<>(); + COSArray kids = getCOSObject().getCOSArray(COSName.KIDS); if (kids == null) { // the field itself is a widget widgets.add(new PDAnnotationWidget(getCOSObject())); } - else if (kids.size() > 0) + else if (!kids.isEmpty()) { // there are multiple widgets for (int i = 0; i < kids.size(); i++) @@ -193,7 +193,7 @@ else if (kids.size() > 0) */ public void setWidgets(List children) { - COSArray kidsArray = COSArrayList.converterToCOSArray(children); + COSArray kidsArray = new COSArray(children); getCOSObject().setItem(COSName.KIDS, kidsArray); for (PDAnnotationWidget widget : children) { @@ -201,21 +201,6 @@ public void setWidgets(List children) } } - /** - * This will get the single associated widget that is part of this field. This occurs when the - * Widget is embedded in the fields dictionary. Sometimes there are multiple sub widgets - * associated with this field, in which case you want to use getWidgets(). If the kids entry is - * specified, then the first entry in that list will be returned. - * - * @return The widget that is associated with this field. - * @deprecated Fields may have more than one widget, call {@link #getWidgets()} instead. - */ - @Deprecated - public PDAnnotationWidget getWidget() - { - return getWidgets().get(0); - } - /** * Applies a value change to the field. Generates appearances if required and raises events. * @@ -223,10 +208,7 @@ public PDAnnotationWidget getWidget() */ protected final void applyChange() throws IOException { - if (!getAcroForm().getNeedAppearances()) - { - constructAppearances(); - } + constructAppearances(); // if we supported JavaScript we would raise a field changed event here } diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDVariableText.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDVariableText.java index c2f8a3b8..e9beb806 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDVariableText.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDVariableText.java @@ -25,6 +25,7 @@ import com.ainceborn.pdfbox.cos.COSStream; import com.ainceborn.pdfbox.cos.COSString; import com.ainceborn.pdfbox.pdmodel.PDResources; +import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; /** * Base class for fields which use "Variable Text". @@ -107,7 +108,7 @@ PDDefaultAppearanceString getDefaultAppearanceString() throws IOException /** * Set the default appearance. * - * This will set the local default appearance for the variable text field only, not + * This will set the local default appearance for the variable text field only, not * affecting a default appearance in the parent hierarchy. * * Providing null as the value will remove the local default appearance. @@ -127,6 +128,19 @@ PDDefaultAppearanceString getDefaultAppearanceString() throws IOException public void setDefaultAppearance(String daValue) { getCOSObject().setString(COSName.DA, daValue); + + // PDFBOX-5797: Sejda files have a /DA entry in kid widgets + if (getCOSObject().containsKey(COSName.KIDS)) + { + for (PDAnnotationWidget widget : getWidgets()) + { + COSDictionary widgetDict = widget.getCOSObject(); + if (widgetDict.containsKey(COSName.DA)) + { + widgetDict.setString(COSName.DA, daValue); + } + } + } } /** @@ -139,8 +153,7 @@ public void setDefaultAppearance(String daValue) */ public String getDefaultStyleString() { - COSString defaultStyleString = (COSString) getCOSObject().getDictionaryObject(COSName.DS); - return defaultStyleString.getString(); + return getCOSObject().getString(COSName.DS); } /** @@ -201,9 +214,8 @@ public void setQ(int q) * Get the fields rich text value. * * @return the rich text value string - * @throws IOException if the field dictionary entry is not a text type */ - public String getRichTextValue() throws IOException + public String getRichTextValue() { return getStringOrStream(getInheritableAttribute(COSName.RV)); } @@ -245,11 +257,7 @@ public void setRichTextValue(String richTextValue) */ protected final String getStringOrStream(COSBase base) { - if (base == null) - { - return ""; - } - else if (base instanceof COSString) + if (base instanceof COSString) { return ((COSString)base).getString(); } @@ -257,9 +265,6 @@ else if (base instanceof COSStream) { return ((COSStream)base).toTextString(); } - else - { - return ""; - } + return ""; } } diff --git a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PlainTextFormatter.java b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PlainTextFormatter.java index a0ad4376..794a7cf2 100644 --- a/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PlainTextFormatter.java +++ b/library/src/main/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PlainTextFormatter.java @@ -19,51 +19,24 @@ import java.io.IOException; import java.util.List; +import com.ainceborn.pdfbox.pdmodel.PDAppearanceContentStream; import com.ainceborn.pdfbox.pdmodel.PDPageContentStream; +import com.ainceborn.pdfbox.pdmodel.interactive.TextAlign; import com.ainceborn.pdfbox.pdmodel.interactive.form.PlainText.Line; import com.ainceborn.pdfbox.pdmodel.interactive.form.PlainText.Paragraph; import com.ainceborn.pdfbox.pdmodel.interactive.form.PlainText.TextAttribute; import com.ainceborn.pdfbox.pdmodel.interactive.form.PlainText.Word; /** - * TextFormatter to handle plain text formatting. + * TextFormatter to handle plain text formatting for annotation rectangles. * * The text formatter will take a single value or an array of values which * are treated as paragraphs. */ -class PlainTextFormatter +public class PlainTextFormatter { - enum TextAlign - { - LEFT(0), CENTER(1), RIGHT(2), JUSTIFY(4); - - private final int alignment; - - private TextAlign(int alignment) - { - this.alignment = alignment; - } - - int getTextAlign() - { - return alignment; - } - - public static TextAlign valueOf(int alignment) - { - for (TextAlign textAlignment : TextAlign.values()) - { - if (textAlignment.getTextAlign() == alignment) - { - return textAlignment; - } - } - return TextAlign.LEFT; - } - } - /** * The scaling factor for font units to PDF units */ @@ -73,18 +46,18 @@ public static TextAlign valueOf(int alignment) private final boolean wrapLines; private final float width; - private final PDPageContentStream contents; + private final PDAppearanceContentStream contents; private final PlainText textContent; private final TextAlign textAlignment; private float horizontalOffset; private float verticalOffset; - static class Builder + public static class Builder { // required parameters - private PDPageContentStream contents; + private final PDAppearanceContentStream contents; // optional parameters private AppearanceStyle appearanceStyle; @@ -98,56 +71,56 @@ static class Builder private float horizontalOffset = 0f; private float verticalOffset = 0f; - Builder(PDPageContentStream contents) + public Builder(PDAppearanceContentStream contents) { this.contents = contents; } - Builder style(AppearanceStyle appearanceStyle) + public Builder style(AppearanceStyle appearanceStyle) { this.appearanceStyle = appearanceStyle; return this; } - Builder wrapLines(boolean wrapLines) + public Builder wrapLines(boolean wrapLines) { this.wrapLines = wrapLines; return this; } - Builder width(float width) + public Builder width(float width) { this.width = width; return this; } - Builder textAlign(int alignment) + public Builder textAlign(int alignment) { this.textAlignment = TextAlign.valueOf(alignment); return this; } - Builder textAlign(TextAlign alignment) + public Builder textAlign(TextAlign alignment) { this.textAlignment = alignment; return this; } - Builder text(PlainText textContent) + public Builder text(PlainText textContent) { this.textContent = textContent; return this; } - Builder initialOffset(float horizontalOffset, float verticalOffset) + public Builder initialOffset(float horizontalOffset, float verticalOffset) { this.horizontalOffset = horizontalOffset; this.verticalOffset = verticalOffset; return this; } - PlainTextFormatter build() + public PlainTextFormatter build() { return new PlainTextFormatter(this); } @@ -180,9 +153,9 @@ public void format() throws IOException if (wrapLines) { List lines = paragraph.getLines( - appearanceStyle.getFont(), - appearanceStyle.getFontSize(), - width + appearanceStyle.getFont(), + appearanceStyle.getFontSize(), + width ); processLines(lines, isFirstParagraph); isFirstParagraph = false; @@ -193,7 +166,7 @@ public void format() throws IOException float lineWidth = appearanceStyle.getFont().getStringWidth(paragraph.getText()) * - appearanceStyle.getFontSize() / FONTSCALE; + appearanceStyle.getFontSize() / FONTSCALE; if (lineWidth < width) { @@ -219,9 +192,9 @@ public void format() throws IOException } /** - * Process lines for output. + * Process lines for output. * - * Process lines for an individual paragraph and generate the + * Process lines for an individual paragraph and generate the * commands for the content stream to show the text. * * @param lines the lines to process. @@ -229,7 +202,7 @@ public void format() throws IOException */ private void processLines(List lines, boolean isFirstParagraph) throws IOException { - float wordWidth = 0f; + float wordWidth; float lastPos = 0f; float startOffset = 0f; From ca742d3d107f9d95b160e4c5dd8aea636b0700d6 Mon Sep 17 00:00:00 2001 From: Kanstantsin Valeitsenak Date: Tue, 14 Jul 2026 20:19:15 +0200 Subject: [PATCH 4/6] fix tests --- .../pdmodel/graphics/blend/BlendModeTest.java | 209 ++++++++-- .../interactive/form/PDAcroFormTest.java | 13 +- .../interactive/form/PDButtonTest.java | 32 -- .../pdmodel/interactive/form/TestListBox.java | 393 ++++++++++-------- 4 files changed, 396 insertions(+), 251 deletions(-) diff --git a/library/src/test/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/BlendModeTest.java b/library/src/test/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/BlendModeTest.java index 2f777ecc..9d8511d4 100644 --- a/library/src/test/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/BlendModeTest.java +++ b/library/src/test/java/com/ainceborn/pdfbox/pdmodel/graphics/blend/BlendModeTest.java @@ -16,10 +16,16 @@ */ package com.ainceborn.pdfbox.pdmodel.graphics.blend; +import com.ainceborn.pdfbox.cos.COSArray; +import com.ainceborn.pdfbox.cos.COSInteger; import com.ainceborn.pdfbox.cos.COSName; import org.junit.Test; import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * @@ -27,13 +33,9 @@ */ public class BlendModeTest { - public BlendModeTest() - { - } /** - * Check that BlendMode.* constant instances are not null. This could happen if the declaration - * sequence is changed. + * Check that BlendMode.* constant instances are not null. */ @Test public void testInstances() @@ -55,31 +57,182 @@ public void testInstances() assertEquals(BlendMode.SATURATION, BlendMode.getInstance(COSName.SATURATION)); assertEquals(BlendMode.LUMINOSITY, BlendMode.getInstance(COSName.LUMINOSITY)); assertEquals(BlendMode.COLOR, BlendMode.getInstance(COSName.COLOR)); + + COSArray cosArrayOverlay = new COSArray(); + cosArrayOverlay.add(COSName.OVERLAY); + assertEquals(BlendMode.OVERLAY, BlendMode.getInstance(cosArrayOverlay)); + + COSArray cosArrayInteger = new COSArray(); + cosArrayInteger.add(COSInteger.get(0)); + assertEquals(BlendMode.NORMAL, BlendMode.getInstance(cosArrayInteger)); + + } + + @Test + public void testBlendModeNormal() + { + assertTrue(BlendMode.NORMAL.isSeparableBlendMode()); + assertNull(BlendMode.NORMAL.getBlendFunction()); + assertNotNull(BlendMode.NORMAL.getBlendChannelFunction()); + assertEquals(COSName.NORMAL, BlendMode.NORMAL.getCOSName()); + assertEquals(3f, BlendMode.NORMAL.getBlendChannelFunction().blendChannel(3f, 5f)); + + assertEquals(COSName.NORMAL, BlendMode.COMPATIBLE.getCOSName()); + } + + @Test + public void testBlendModeMultiply() + { + assertTrue(BlendMode.MULTIPLY.isSeparableBlendMode()); + assertNull(BlendMode.MULTIPLY.getBlendFunction()); + assertNotNull(BlendMode.MULTIPLY.getBlendChannelFunction()); + assertEquals(COSName.MULTIPLY, BlendMode.MULTIPLY.getCOSName()); + assertEquals(15f, BlendMode.MULTIPLY.getBlendChannelFunction().blendChannel(3f, 5f)); + } + + @Test + public void testBlendModeScreen() + { + assertTrue(BlendMode.SCREEN.isSeparableBlendMode()); + assertNull(BlendMode.SCREEN.getBlendFunction()); + assertNotNull(BlendMode.SCREEN.getBlendChannelFunction()); + assertEquals(COSName.SCREEN, BlendMode.SCREEN.getCOSName()); + assertEquals(-7f, BlendMode.SCREEN.getBlendChannelFunction().blendChannel(3f, 5f)); + } + + @Test + public void testBlendModeOverlay() + { + assertTrue(BlendMode.OVERLAY.isSeparableBlendMode()); + assertNull(BlendMode.OVERLAY.getBlendFunction()); + assertNotNull(BlendMode.OVERLAY.getBlendChannelFunction()); + assertEquals(COSName.OVERLAY, BlendMode.OVERLAY.getCOSName()); + assertEquals(0f, BlendMode.OVERLAY.getBlendChannelFunction().blendChannel(1f, 0f)); + assertEquals(0.3f, BlendMode.OVERLAY.getBlendChannelFunction().blendChannel(0.5f, 0.3f)); + } + + @Test + public void testBlendModeDarken() + { + assertTrue(BlendMode.DARKEN.isSeparableBlendMode()); + assertNull(BlendMode.DARKEN.getBlendFunction()); + assertNotNull(BlendMode.DARKEN.getBlendChannelFunction()); + assertEquals(COSName.DARKEN, BlendMode.DARKEN.getCOSName()); + assertEquals(3f, BlendMode.DARKEN.getBlendChannelFunction().blendChannel(3f, 5f)); + } + + @Test + public void testBlendModeLighten() + { + assertTrue(BlendMode.LIGHTEN.isSeparableBlendMode()); + assertNull(BlendMode.LIGHTEN.getBlendFunction()); + assertNotNull(BlendMode.LIGHTEN.getBlendChannelFunction()); + assertEquals(COSName.LIGHTEN, BlendMode.LIGHTEN.getCOSName()); + assertEquals(5f, BlendMode.LIGHTEN.getBlendChannelFunction().blendChannel(3f, 5f)); + } + + @Test + public void testBlendModeColorDodge() + { + assertTrue(BlendMode.COLOR_DODGE.isSeparableBlendMode()); + assertNull(BlendMode.COLOR_DODGE.getBlendFunction()); + assertNotNull(BlendMode.COLOR_DODGE.getBlendChannelFunction()); + assertEquals(COSName.COLOR_DODGE, BlendMode.COLOR_DODGE.getCOSName()); + assertEquals(0f, BlendMode.COLOR_DODGE.getBlendChannelFunction().blendChannel(1f, 0f)); + assertEquals(1f, BlendMode.COLOR_DODGE.getBlendChannelFunction().blendChannel(0.3f, 0.7f)); + } + + @Test + public void testBlendModeColorBurn() + { + assertTrue(BlendMode.COLOR_BURN.isSeparableBlendMode()); + assertNull(BlendMode.COLOR_BURN.getBlendFunction()); + assertNotNull(BlendMode.COLOR_BURN.getBlendChannelFunction()); + assertEquals(COSName.COLOR_BURN, BlendMode.COLOR_BURN.getCOSName()); + assertEquals(1f, BlendMode.COLOR_BURN.getBlendChannelFunction().blendChannel(0f, 1f)); + assertEquals(0f, BlendMode.COLOR_BURN.getBlendChannelFunction().blendChannel(0.7f, 0.3f)); + } + + @Test + public void testBlendModeHardLight() + { + assertTrue(BlendMode.HARD_LIGHT.isSeparableBlendMode()); + assertNull(BlendMode.HARD_LIGHT.getBlendFunction()); + assertNotNull(BlendMode.HARD_LIGHT.getBlendChannelFunction()); + assertEquals(COSName.HARD_LIGHT, BlendMode.HARD_LIGHT.getCOSName()); + assertEquals(0f, BlendMode.HARD_LIGHT.getBlendChannelFunction().blendChannel(0f, 0.5f)); + assertEquals(0.2f, BlendMode.HARD_LIGHT.getBlendChannelFunction().blendChannel(0.2f, 0.5f)); + assertEquals(0.52f, + BlendMode.HARD_LIGHT.getBlendChannelFunction().blendChannel(0.6f, 0.4f)); } - /** - * Check that COSName constants returned for BlendMode.* instances are not null. This could - * happen if the declaration sequence is changed. - */ @Test - public void testCOSNames() - { - assertEquals(COSName.NORMAL, BlendMode.getCOSName(BlendMode.NORMAL)); - assertEquals(COSName.NORMAL, BlendMode.getCOSName(BlendMode.COMPATIBLE)); - assertEquals(COSName.MULTIPLY, BlendMode.getCOSName(BlendMode.MULTIPLY)); - assertEquals(COSName.SCREEN, BlendMode.getCOSName(BlendMode.SCREEN)); - assertEquals(COSName.OVERLAY, BlendMode.getCOSName(BlendMode.OVERLAY)); - assertEquals(COSName.DARKEN, BlendMode.getCOSName(BlendMode.DARKEN)); - assertEquals(COSName.LIGHTEN, BlendMode.getCOSName(BlendMode.LIGHTEN)); - assertEquals(COSName.COLOR_DODGE, BlendMode.getCOSName(BlendMode.COLOR_DODGE)); - assertEquals(COSName.COLOR_BURN, BlendMode.getCOSName(BlendMode.COLOR_BURN)); - assertEquals(COSName.HARD_LIGHT, BlendMode.getCOSName(BlendMode.HARD_LIGHT)); - assertEquals(COSName.SOFT_LIGHT, BlendMode.getCOSName(BlendMode.SOFT_LIGHT)); - assertEquals(COSName.DIFFERENCE, BlendMode.getCOSName(BlendMode.DIFFERENCE)); - assertEquals(COSName.EXCLUSION, BlendMode.getCOSName(BlendMode.EXCLUSION)); - assertEquals(COSName.HUE, BlendMode.getCOSName(BlendMode.HUE)); - assertEquals(COSName.SATURATION, BlendMode.getCOSName(BlendMode.SATURATION)); - assertEquals(COSName.LUMINOSITY, BlendMode.getCOSName(BlendMode.LUMINOSITY)); - assertEquals(COSName.COLOR, BlendMode.getCOSName(BlendMode.COLOR)); + public void testBlendModeSoftLight() + { + assertTrue(BlendMode.SOFT_LIGHT.isSeparableBlendMode()); + assertNull(BlendMode.SOFT_LIGHT.getBlendFunction()); + assertNotNull(BlendMode.SOFT_LIGHT.getBlendChannelFunction()); + assertEquals(COSName.SOFT_LIGHT, BlendMode.SOFT_LIGHT.getCOSName()); + assertEquals(0.25f, BlendMode.SOFT_LIGHT.getBlendChannelFunction().blendChannel(0f, 0.5f)); + assertEquals(0.35f, + BlendMode.SOFT_LIGHT.getBlendChannelFunction().blendChannel(0.2f, 0.5f)); + assertEquals(0.2f, + BlendMode.SOFT_LIGHT.getBlendChannelFunction().blendChannel(0.5f, 0.2f)); + } + + @Test + public void testBlendModeDifference() + { + assertTrue(BlendMode.DIFFERENCE.isSeparableBlendMode()); + assertNull(BlendMode.DIFFERENCE.getBlendFunction()); + assertNotNull(BlendMode.DIFFERENCE.getBlendChannelFunction()); + assertEquals(COSName.DIFFERENCE, BlendMode.DIFFERENCE.getCOSName()); + assertEquals(2f, BlendMode.DIFFERENCE.getBlendChannelFunction().blendChannel(3f, 5f)); + } + + @Test + public void testBlendModeExclusion() + { + assertTrue(BlendMode.EXCLUSION.isSeparableBlendMode()); + assertNull(BlendMode.EXCLUSION.getBlendFunction()); + assertNotNull(BlendMode.EXCLUSION.getBlendChannelFunction()); + assertEquals(COSName.EXCLUSION, BlendMode.EXCLUSION.getCOSName()); } + + @Test + public void testBlendModeHue() + { + assertFalse(BlendMode.HUE.isSeparableBlendMode()); + assertNotNull(BlendMode.HUE.getBlendFunction()); + assertNull(BlendMode.HUE.getBlendChannelFunction()); + assertEquals(COSName.HUE, BlendMode.HUE.getCOSName()); + } + + @Test + public void testBlendModeSaturation() + { + assertFalse(BlendMode.SATURATION.isSeparableBlendMode()); + assertNotNull(BlendMode.SATURATION.getBlendFunction()); + assertNull(BlendMode.SATURATION.getBlendChannelFunction()); + assertEquals(COSName.SATURATION, BlendMode.SATURATION.getCOSName()); + } + + @Test + public void testBlendModeLuminosity() + { + assertFalse(BlendMode.LUMINOSITY.isSeparableBlendMode()); + assertNotNull(BlendMode.LUMINOSITY.getBlendFunction()); + assertNull(BlendMode.LUMINOSITY.getBlendChannelFunction()); + assertEquals(COSName.LUMINOSITY, BlendMode.LUMINOSITY.getCOSName()); + } + + @Test + public void testBlendModeColor() + { + assertFalse(BlendMode.COLOR.isSeparableBlendMode()); + assertNotNull(BlendMode.COLOR.getBlendFunction()); + assertNull(BlendMode.COLOR.getBlendChannelFunction()); + assertEquals(COSName.COLOR, BlendMode.COLOR.getCOSName()); + } + } diff --git a/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDAcroFormTest.java b/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDAcroFormTest.java index 3b79d8f2..da2e10fe 100644 --- a/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDAcroFormTest.java +++ b/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDAcroFormTest.java @@ -382,20 +382,13 @@ private int countWidgets(PDDocument documentToTest) int count = 0; for (PDPage page : documentToTest.getPages()) { - try + for (PDAnnotation annotation : page.getAnnotations()) { - for (PDAnnotation annotation : page.getAnnotations()) + if (annotation instanceof PDAnnotationWidget) { - if (annotation instanceof PDAnnotationWidget) - { - count ++; - } + count ++; } } - catch (IOException e) - { - // ignoring - } } return count; } diff --git a/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDButtonTest.java b/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDButtonTest.java index ec2dc9f0..193842ee 100644 --- a/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDButtonTest.java +++ b/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/PDButtonTest.java @@ -97,38 +97,6 @@ public void createRadioButton() assertFalse(buttonField.isPushButton()); } - @Test - public void changeRadioButtonToPushButton() - { - PDButton buttonField = new PDRadioButton(acroForm); - - assertEquals(buttonField.getFieldType(), buttonField.getCOSObject().getNameAsString(COSName.FT)); - assertEquals("Btn", buttonField.getFieldType()); - assertTrue(buttonField.isRadioButton()); - assertFalse(buttonField.isPushButton()); - - // change to push button - buttonField.setPushButton(true); - assertFalse(buttonField.isRadioButton()); - assertTrue(buttonField.isPushButton()); - } - - @Test - public void changePushButtonToRadioButton() - { - PDButton buttonField = new PDPushButton(acroForm); - - assertEquals(buttonField.getFieldType(), buttonField.getCOSObject().getNameAsString(COSName.FT)); - assertEquals("Btn", buttonField.getFieldType()); - assertTrue(buttonField.isPushButton()); - assertFalse(buttonField.isRadioButton()); - - // change to push button - buttonField.setRadioButton(true); - assertFalse(buttonField.isPushButton()); - assertTrue(buttonField.isRadioButton()); - } - @Test /** * PDFBOX-3656 diff --git a/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/TestListBox.java b/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/TestListBox.java index bee30296..994a13e8 100644 --- a/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/TestListBox.java +++ b/library/src/test/java/com/ainceborn/pdfbox/pdmodel/interactive/form/TestListBox.java @@ -16,6 +16,11 @@ */ package com.ainceborn.pdfbox.pdmodel.interactive.form; +import static com.ainceborn.pdfbox.pdmodel.font.Standard14Fonts.FontName.*; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -23,215 +28,241 @@ import com.ainceborn.pdfbox.cos.COSArray; import com.ainceborn.pdfbox.cos.COSName; +import com.ainceborn.pdfbox.io.IOUtils; import com.ainceborn.pdfbox.pdmodel.PDDocument; +import com.ainceborn.pdfbox.pdmodel.PDPage; +import com.ainceborn.pdfbox.pdmodel.PDResources; +import com.ainceborn.pdfbox.pdmodel.common.PDRectangle; +import com.ainceborn.pdfbox.pdmodel.font.PDFont; +import com.ainceborn.pdfbox.pdmodel.font.PDType1Font; +import com.ainceborn.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; -import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + /** * This will test the functionality of choice fields in PDFBox. */ public class TestListBox extends TestCase { - /** - * Constructor. - * - * @param name The name of the test to run. - */ - public TestListBox( String name ) + private List exportValues; + private List displayValues; + private PDDocument doc; + PDListBox choice; + + @Before + public void setUp() { - super( name ); + // export values + exportValues = new ArrayList<>(); + exportValues.add("export01"); + exportValues.add("export02"); + exportValues.add("export03"); + + // display values, not sorted on purpose as this + // will be used to test the sort option of the list box + displayValues = new ArrayList<>(); + displayValues.add("display02"); + displayValues.add("display01"); + displayValues.add("display03"); + + doc = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + doc.addPage(page); + PDAcroForm form = new PDAcroForm( doc ); + + // Adobe Acrobat uses Helvetica as a default font and + // stores that under the name '/Helv' in the resources dictionary + PDFont font = new PDType1Font(HELVETICA); + PDResources resources = new PDResources(); + resources.put(COSName.HELV, font); + + // Add and set the resources and default appearance at the form level + form.setDefaultResources(resources); + + // Acrobat sets the font size on the form level to be + // auto sized as default. This is done by setting the font size to '0' + String defaultAppearanceString = "/Helv 0 Tf 0 g"; + form.setDefaultAppearance(defaultAppearanceString); + + // the choice field for testing + choice = new PDListBox(form); + + choice.setDefaultAppearance("/Helv 12 Tf 0g"); + + + // Specify the annotation associated with the field + PDAnnotationWidget widget = choice.getWidgets().get(0); + PDRectangle rect = new PDRectangle(50, 750, 200, 50); + widget.setRectangle(rect); + widget.setPage(page); + + // Add the annotation to the page + page.getAnnotations().add(widget); } - /** - * This will get the suite of test that this class holds. - * - * @return All of the tests that this class holds. - */ - public static Test suite() + @Test + public void testNoNullsReturned() { - return new TestSuite( TestListBox.class ); + // test that there are no nulls returned for an empty field + // only specific methods are tested here + assertNotNull(choice.getOptions()); + assertNotNull(choice.getValue()); } - /** - * infamous main method. - * - * @param args The command line arguments. + /* + * Tests for setting the export values */ - public static void main( String[] args ) + @Test + public void testExportValuesGetterSetter() throws IOException { - String[] arg = {TestListBox.class.getName() }; - junit.textui.TestRunner.main( arg ); + // setting/getting option values - the dictionaries Opt entry + choice.setOptions(exportValues); + assertEquals(exportValues,choice.getOptionsDisplayValues()); + assertEquals(exportValues,choice.getOptionsExportValues()); + + // Test bug 1 of PDFBOX-4252 when top index is not null + choice.setTopIndex(1); + choice.setValue(exportValues.get(2)); + assertEquals(exportValues.get(2), choice.getValue().get(0)); + choice.setTopIndex(null); // reset + + // assert that the option values have been correctly set + COSArray optItem = (COSArray) choice.getCOSObject().getItem(COSName.OPT); + assertNotNull(choice.getCOSObject().getItem(COSName.OPT)); + assertEquals(optItem.size(),exportValues.size()); + assertEquals(exportValues.get(0), optItem.getString(0)); + + // assert that the option values can be retrieved correctly + List retrievedOptions = choice.getOptions(); + assertEquals(retrievedOptions.size(),exportValues.size()); + assertEquals(retrievedOptions, exportValues); + // assert that the field value can be set } - /** - * This will test the list box PDModel. - * - * @throws IOException If there is an error creating the field. + /* + * Test for setting the field value */ - public void testListboxPDModel() throws IOException + @Test + public void testFieldValueSetterGetter() throws IOException { + // add test data + choice.setOptions(exportValues); + choice.setMultiSelect(true); + choice.setValue(exportValues); + + // assert that the option values have been correctly set + COSArray valueItems = (COSArray) choice.getCOSObject().getItem(COSName.V); + assertNotNull(valueItems); + assertEquals(valueItems.size(),exportValues.size()); + assertEquals(exportValues.get(0), valueItems.getString(0)); + + // assert that the index values have been correctly set + COSArray indexItems = (COSArray) choice.getCOSObject().getItem(COSName.I); + assertNotNull(indexItems); + assertEquals(indexItems.size(),exportValues.size()); + + // setting a single value shall remove the indices + choice.setValue("export01"); + indexItems = (COSArray) choice.getCOSObject().getItem(COSName.I); + assertNull(indexItems); + } - /* - * Set up two data lists which will be used for the tests - */ - - // export values - List exportValues = new ArrayList(); - exportValues.add("export01"); - exportValues.add("export02"); - exportValues.add("export03"); + @Test + public void testMultiselect() throws IOException + { + // add test data + choice.setOptions(exportValues); - // display values, not sorted on purpose as this - // will be used to test the sort option of the list box - List displayValues = new ArrayList(); - displayValues.add("display02"); - displayValues.add("display01"); - displayValues.add("display03"); + // ensure that the choice field doesn't allow multiple selections + choice.setMultiSelect(false); - PDDocument doc = null; - try - { - doc = new PDDocument(); - PDAcroForm form = new PDAcroForm( doc ); - PDListBox choice = new PDListBox(form); - - // appearance construction is not implemented, so turn on NeedAppearances - form.setNeedAppearances(true); - - // test that there are no nulls returned for an empty field - // only specific methods are tested here - assertNotNull(choice.getOptions()); - assertNotNull(choice.getValue()); - - /* - * Tests for setting the export values - */ - - // setting/getting option values - the dictionaries Opt entry - choice.setOptions(exportValues); - assertEquals(exportValues,choice.getOptionsDisplayValues()); - assertEquals(exportValues,choice.getOptionsExportValues()); - - // Test bug 1 of PDFBOX-4252 when top index is not null - choice.setTopIndex(1); - choice.setValue(exportValues.get(2)); - assertEquals(exportValues.get(2), choice.getValue().get(0)); - choice.setTopIndex(null); // reset - - // assert that the option values have been correctly set - COSArray optItem = (COSArray) choice.getCOSObject().getItem(COSName.OPT); - assertNotNull(choice.getCOSObject().getItem(COSName.OPT)); - assertEquals(optItem.size(),exportValues.size()); - assertEquals(exportValues.get(0), optItem.getString(0)); - - // assert that the option values can be retrieved correctly - List retrievedOptions = choice.getOptions(); - assertEquals(retrievedOptions.size(),exportValues.size()); - assertEquals(retrievedOptions, exportValues); - - /* - * Tests for setting the field values - */ - - // assert that the field value can be set - choice.setValue("export01"); - assertEquals(choice.getValue().get(0),"export01"); - - // ensure that the choice field doesn't allow multiple selections - choice.setMultiSelect(false); - - // without multiselect setting multiple items shall fail - try - { - choice.setValue(exportValues); - fail( "Missing IllegalArgumentException" ); - } - catch( IllegalArgumentException e ) - { - assertEquals("The list box does not allow multiple selections.",e.getMessage() ); - } - - // ensure that the choice field does allow multiple selections - choice.setMultiSelect(true); - // now this call must succeed + // without multiselect setting multiple items shall fail + Exception exception = assertThrows(IllegalArgumentException.class, () -> { choice.setValue(exportValues); + }); - // assert that the option values have been correctly set - COSArray valueItems = (COSArray) choice.getCOSObject().getItem(COSName.V); - assertNotNull(valueItems); - assertEquals(valueItems.size(),exportValues.size()); - assertEquals(exportValues.get(0), valueItems.getString(0)); - - // assert that the index values have been correctly set - COSArray indexItems = (COSArray) choice.getCOSObject().getItem(COSName.I); - assertNotNull(indexItems); - assertEquals(indexItems.size(),exportValues.size()); - - // setting a single value shall remove the indices - choice.setValue("export01"); - indexItems = (COSArray) choice.getCOSObject().getItem(COSName.I); - assertNull(indexItems); - - // assert that the Opt entry is removed - choice.setOptions(null); - assertNull(choice.getCOSObject().getItem(COSName.OPT)); - // if there is no Opt entry an empty List shall be returned - assertEquals(choice.getOptions(), Collections.emptyList()); - - /* - * Test for setting export and display values - */ - - // setting display and export value - choice.setOptions(exportValues, displayValues); - assertEquals(displayValues,choice.getOptionsDisplayValues()); - assertEquals(exportValues,choice.getOptionsExportValues()); - - /* - * Testing the sort option - */ - assertEquals(choice.getOptionsDisplayValues().get(0),"display02"); - choice.setSort(true); + assertEquals("The list box does not allow multiple selections.", exception.getMessage()); + + // ensure that the choice field does allow multiple selections + choice.setMultiSelect(true); + // now this call must succeed + choice.setValue(exportValues); + } + + @Test + public void testOptIsRemovedForNull() + { + // add test data + choice.setOptions(exportValues); + assertNotNull(choice.getCOSObject().getItem(COSName.OPT)); + // assert that the Opt entry is removed + choice.setOptions(null); + assertNull(choice.getCOSObject().getItem(COSName.OPT)); + // if there is no Opt entry an empty List shall be returned + assertEquals(Collections.emptyList(), choice.getOptions()); + } + + @Test + public void testSetExportAndDisplay() + { + // setting display and export value + choice.setOptions(exportValues, displayValues); + assertEquals(displayValues,choice.getOptionsDisplayValues()); + assertEquals(exportValues,choice.getOptionsExportValues()); + } + + @Test + public void testSortOption() + { + // add test data + choice.setOptions(exportValues, displayValues); + assertEquals("display02", choice.getOptionsDisplayValues().get(0)); + + // test the sort option + choice.setSort(true); + choice.setOptions(exportValues, displayValues); + assertEquals("display01", choice.getOptionsDisplayValues().get(0)); + assertEquals("display02", choice.getOptionsDisplayValues().get(1)); + assertEquals("display03", choice.getOptionsDisplayValues().get(2)); + } + + @Test + public void testEmptyOptionsNotNull() + { + // assert that the Opt entry is removed + choice.setOptions(null, displayValues); + assertNull(choice.getCOSObject().getItem(COSName.OPT)); + + // if there is no Opt entry an empty list shall be returned + assertEquals(Collections.emptyList(), choice.getOptions()); + assertEquals(Collections.emptyList(), choice.getOptionsDisplayValues()); + assertEquals(Collections.emptyList(), choice.getOptionsExportValues()); + } + + @Test + public void testExceptionForDifferentNumberOfEntries() + { + // test that an IllegalArgumentException is thrown when export and display + // value lists have different sizes + exportValues.remove(1); + + // without multiselect setting multiple items shall fail + Exception exception = assertThrows(IllegalArgumentException.class, () -> { choice.setOptions(exportValues, displayValues); - assertEquals(choice.getOptionsDisplayValues().get(0),"display01"); - - /* - * Setting options with an empty list - */ - // assert that the Opt entry is removed - choice.setOptions(null, displayValues); - assertNull(choice.getCOSObject().getItem(COSName.OPT)); - - // if there is no Opt entry an empty list shall be returned - assertEquals(choice.getOptions(), Collections.emptyList()); - assertEquals(choice.getOptionsDisplayValues(), Collections.emptyList()); - assertEquals(choice.getOptionsExportValues(), Collections.emptyList()); - - // test that an IllegalArgumentException is thrown when export and display - // value lists have different sizes - exportValues.remove(1); - - try - { - choice.setOptions(exportValues, displayValues); - fail( "Missing exception" ); - } - catch( IllegalArgumentException e ) - { - assertEquals( - "The number of entries for exportValue and displayValue shall be the same.", - e.getMessage() ); - } - } - finally - { - if( doc != null ) - { - doc.close(); - } - } + }); + + assertEquals("The number of entries for exportValue and displayValue shall be the same.", exception.getMessage()); + } + + @After + public void tearDown() + { + IOUtils.closeQuietly(doc); } } \ No newline at end of file From 8abc79536ae545f08049b5599266e865b4b9118e Mon Sep 17 00:00:00 2001 From: Kanstantsin Valeitsenak Date: Wed, 15 Jul 2026 13:44:38 +0200 Subject: [PATCH 5/6] update version number --- README.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 57889bf5..0d0a0484 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ repositories { } ... dependencies { - implementation 'com.github.ainceborn:PdfBox-Android-3:3.0.2' + implementation 'com.github.ainceborn:PdfBox-Android-3:3.0.3' } ``` diff --git a/gradle.properties b/gradle.properties index e8e5b388..6bc7377a 100755 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ org.gradle.jvmargs=-Xmx4096M -XX:MaxMetaspaceSize=1024m -Dkotlin.daemon.jvm.opti org.gradle.parallel=true org.gradle.caching=true -VERSION_NAME=3.0.2 +VERSION_NAME=3.0.3 VERSION_CODE=1 ANDROID_BUILD_MIN_SDK_VERSION=26 From 939e110fd4129ba1976103156135af09e29a5f65 Mon Sep 17 00:00:00 2001 From: Kanstantsin Valeitsenak Date: Wed, 15 Jul 2026 14:14:16 +0200 Subject: [PATCH 6/6] add PdfBoxInfo tools class --- library/build.gradle | 1 + library/src/main/java/com/ainceborn/PdfBoxInfo.java | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 library/src/main/java/com/ainceborn/PdfBoxInfo.java diff --git a/library/build.gradle b/library/build.gradle index bc0dfff0..c8aaeb6c 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -17,6 +17,7 @@ android { minSdkVersion Integer.parseInt(project.ANDROID_BUILD_MIN_SDK_VERSION) targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION) versionName project.VERSION_NAME + buildConfigField 'String', 'VERSION_NAME', "\"${project.VERSION_NAME}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-proguard-rules.txt' } diff --git a/library/src/main/java/com/ainceborn/PdfBoxInfo.java b/library/src/main/java/com/ainceborn/PdfBoxInfo.java new file mode 100644 index 00000000..5a84a3a5 --- /dev/null +++ b/library/src/main/java/com/ainceborn/PdfBoxInfo.java @@ -0,0 +1,9 @@ +package com.ainceborn; + +import com.ainceborn.pdfbox.BuildConfig; + +public class PdfBoxInfo { + public static String getVersionName() { + return BuildConfig.VERSION_NAME; + } +}