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 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; + } +} 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; 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); } } 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