Author: msahyoun Date: Tue Sep 9 14:20:30 2014 New Revision: 1623830 URL: http://svn.apache.org/r1623830 Log: PDFBOX-2249 Basic support to handle listbox appearances
Modified: pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAppearance.java pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDChoiceField.java Modified: pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAppearance.java URL: http://svn.apache.org/viewvc/pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAppearance.java?rev=1623830&r1=1623829&r2=1623830&view=diff ============================================================================== --- pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAppearance.java (original) +++ pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAppearance.java Tue Sep 9 14:20:30 2014 @@ -21,47 +21,40 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; - import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.ListIterator; import java.util.Map; import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSFloat; +import org.apache.pdfbox.cos.COSInteger; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSNumber; import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.cos.COSString; - import org.apache.pdfbox.pdfparser.PDFStreamParser; import org.apache.pdfbox.pdfwriter.ContentStreamWriter; - import org.apache.pdfbox.pdmodel.PDResources; - import org.apache.pdfbox.pdmodel.common.COSObjectable; import org.apache.pdfbox.pdmodel.common.PDRectangle; - import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDFontDescriptor; import org.apache.pdfbox.pdmodel.font.PDSimpleFont; - import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; - import org.apache.pdfbox.util.PDFOperator; /** - * This one took me a while, but i'm proud to say that it handles - * the appearance of a textbox. This allows you to apply a value to - * a field in the document and handle the appearance so that the - * value is actually visible too. - * The problem was described by Ben Litchfield, the author of the - * example: org.apache.pdfbox.examlpes.fdf.ImportFDF. So Ben, here is the - * solution. + * This one took me a while, but i'm proud to say that it handles the appearance of a textbox. This allows you to apply + * a value to a field in the document and handle the appearance so that the value is actually visible too. The problem + * was described by Ben Litchfield, the author of the example: org.apache.pdfbox.examlpes.fdf.ImportFDF. So Ben, here is + * the solution. * * @author sug * @author <a href="mailto:b...@benlitchfield.com">Ben Litchfield</a> @@ -77,34 +70,74 @@ public class PDAppearance private final PDAcroForm acroForm; private List<COSObjectable> widgets = new ArrayList<COSObjectable>(); + /** + * The highlight color + * + * 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. + */ + private static final String HIGHLIGHT_COLOR = "0.600006 0.756866 0.854904 rg"; + + /** + * The default padding. + * + * Adobe adds a default padding of 1 to the widgets bounding box. + * + */ + private static final int DEFAULT_PADDING = 1; + + /** + * The padding area. + * + * The box from where he padding into the content area will be calculated. + * + * The default value is to do a padding of 1 on each side of the widgets bounding box. + * + * This might be overwritten by a new setting within the BMC/EMC sequence + */ + private PDRectangle paddingEdge = null; + + /** + * The content area. + * + * The inner box where the content will be printed The default value is to do a padding of 1 on each side of the + * paddingEdge. + * + * This might be overwritten by a new setting within the BMC/EMC sequence + */ + private PDRectangle contentArea = null; /** * Constructs a COSAppearnce from the given field. * - * @param theAcroForm the acro form that this field is part of. - * @param field the field which you wish to control the appearance of - * @throws IOException If there is an error creating the appearance. + * @param theAcroForm + * the acro form that this field is part of. + * @param field + * the field which you wish to control the appearance of + * @throws IOException + * If there is an error creating the appearance. */ - public PDAppearance( PDAcroForm theAcroForm, PDVariableText field ) throws IOException + public PDAppearance(PDAcroForm theAcroForm, PDVariableText field) throws IOException { acroForm = theAcroForm; parent = field; widgets = field.getKids(); - if( widgets == null ) + if (widgets == null) { widgets = new ArrayList<COSObjectable>(); - widgets.add( field.getWidget() ); + widgets.add(field.getWidget()); } defaultAppearance = getDefaultAppearance(); - } /** - * Returns the default apperance of a textbox. If the textbox - * does not have one, then it will be taken from the AcroForm. + * Returns the default appearance of a textbox. If the textbox does not have one, then it will be taken from the + * AcroForm. + * * @return The DA element */ private COSString getDefaultAppearance() @@ -113,13 +146,13 @@ public class PDAppearance COSString dap = parent.getDefaultAppearance(); if (dap == null) { - COSArray kids = (COSArray)parent.getDictionary().getDictionaryObject( COSName.KIDS ); - if( kids != null && kids.size() > 0 ) + COSArray kids = (COSArray) parent.getDictionary().getDictionaryObject(COSName.KIDS); + if (kids != null && kids.size() > 0) { - COSDictionary firstKid = (COSDictionary)kids.getObject( 0 ); - dap = (COSString)firstKid.getDictionaryObject( COSName.DA ); + COSDictionary firstKid = (COSDictionary) kids.getObject(0); + dap = (COSString) firstKid.getDictionaryObject(COSName.DA); } - if( dap == null ) + if (dap == null) { dap = (COSString) acroForm.getDictionary().getDictionaryObject(COSName.DA); } @@ -130,14 +163,14 @@ public class PDAppearance private int getQ() { int q = parent.getQ(); - if( parent.getDictionary().getDictionaryObject( COSName.Q ) == null ) + if (parent.getDictionary().getDictionaryObject(COSName.Q) == null) { - COSArray kids = (COSArray)parent.getDictionary().getDictionaryObject( COSName.KIDS ); - if( kids != null && kids.size() > 0 ) + COSArray kids = (COSArray) parent.getDictionary().getDictionaryObject(COSName.KIDS); + if (kids != null && kids.size() > 0) { - COSDictionary firstKid = (COSDictionary)kids.getObject( 0 ); - COSNumber qNum = (COSNumber)firstKid.getDictionaryObject( COSName.Q ); - if( qNum != null ) + COSDictionary firstKid = (COSDictionary) kids.getObject(0); + COSNumber qNum = (COSNumber) firstKid.getDictionaryObject(COSName.Q); + if (qNum != null) { q = qNum.intValue(); } @@ -151,39 +184,39 @@ public class PDAppearance * * @return The tokens in the original appearance stream */ - private List getStreamTokens( PDAppearanceStream appearanceStream ) throws IOException + private List getStreamTokens(PDAppearanceStream appearanceStream) throws IOException { List tokens = null; - if( appearanceStream != null ) + if (appearanceStream != null) { - tokens = getStreamTokens( appearanceStream.getStream() ); + tokens = getStreamTokens(appearanceStream.getStream()); } return tokens; } - private List getStreamTokens( COSString string ) throws IOException + private List getStreamTokens(COSString string) throws IOException { PDFStreamParser parser; List tokens = null; - if( string != null ) + if (string != null) { - ByteArrayInputStream stream = new ByteArrayInputStream( string.getBytes() ); - parser = new PDFStreamParser( stream, acroForm.getDocument().getDocument().getScratchFile() ); + ByteArrayInputStream stream = new ByteArrayInputStream(string.getBytes()); + parser = new PDFStreamParser(stream, acroForm.getDocument().getDocument().getScratchFile()); parser.parse(); tokens = parser.getTokens(); } return tokens; } - private List getStreamTokens( COSStream stream ) throws IOException + private List getStreamTokens(COSStream stream) throws IOException { PDFStreamParser parser; List tokens = null; - if( stream != null ) + if (stream != null) { - parser = new PDFStreamParser( stream ); + parser = new PDFStreamParser(stream); parser.parse(); tokens = parser.getTokens(); } @@ -195,204 +228,352 @@ public class PDAppearance * * @return true if it contains any content */ - private boolean containsMarkedContent( List stream ) + private boolean containsMarkedContent(List stream) { - return stream.contains( PDFOperator.getOperator( "BMC" ) ); + return stream.contains(PDFOperator.getOperator("BMC")); + } + + /** + * Apply padding to a rectangle. + * + * Padding is used to create different boxes within the widgets 'box model'. + * + * @return a new rectangle with padding applied + */ + private PDRectangle applyPadding(PDRectangle bbox, float padding) + { + PDRectangle area = new PDRectangle(bbox.getCOSArray()); + + area.setLowerLeftX(area.getLowerLeftX() + padding); + area.setLowerLeftY(area.getLowerLeftY() + padding); + area.setUpperRightX(area.getUpperRightX() - padding); + area.setUpperRightY(area.getUpperRightY() - padding); + + return area; } /** * This is the public method for setting the appearance stream. * - * @param apValue the String value which the apperance shoud represent + * @param apValue + * the String value which the appearance should represent * - * @throws IOException If there is an error creating the stream. + * @throws IOException + * If there is an error creating the stream. */ public void setAppearanceValue(String apValue) throws IOException { value = apValue; + Iterator<COSObjectable> widgetIter = widgets.iterator(); - while( widgetIter.hasNext() ) + while (widgetIter.hasNext()) { COSObjectable next = widgetIter.next(); PDField field = null; PDAnnotationWidget widget; - if( next instanceof PDField ) + if (next instanceof PDField) { - field = (PDField)next; + field = (PDField) next; widget = field.getWidget(); } else { - widget = (PDAnnotationWidget)next; + widget = (PDAnnotationWidget) next; } PDFormFieldAdditionalActions actions = null; - if( field != null ) + if (field != null) { actions = field.getActions(); } - if( actions != null && - actions.getF() != null && - widget.getDictionary().getDictionaryObject( COSName.AP ) ==null) + if (actions != null && actions.getF() != null + && widget.getDictionary().getDictionaryObject(COSName.AP) == null) { - //do nothing because the field will be formatted by acrobat - //when it is opened. See FreedomExpressions.pdf for an example of this. + // do nothing because the field will be formatted by acrobat + // when it is opened. See FreedomExpressions.pdf for an example of this. } else { PDAppearanceDictionary appearance = widget.getAppearance(); - if( appearance == null ) + if (appearance == null) { appearance = new PDAppearanceDictionary(); - widget.setAppearance( appearance ); + widget.setAppearance(appearance); } Map normalAppearance = appearance.getNormalAppearance(); - PDAppearanceStream appearanceStream = (PDAppearanceStream)normalAppearance.get( "default" ); - if( appearanceStream == null ) + PDAppearanceStream appearanceStream = (PDAppearanceStream) normalAppearance.get("default"); + if (appearanceStream == null) { COSStream cosStream = acroForm.getDocument().getDocument().createCOSStream(); - appearanceStream = new PDAppearanceStream( cosStream ); - appearanceStream.setBoundingBox( widget.getRectangle().createRetranslatedRectangle() ); - appearance.setNormalAppearance( appearanceStream ); + appearanceStream = new PDAppearanceStream(cosStream); + appearanceStream.setBoundingBox(widget.getRectangle().createRetranslatedRectangle()); + appearance.setNormalAppearance(appearanceStream); } - List tokens = getStreamTokens( appearanceStream ); - List daTokens = getStreamTokens( getDefaultAppearance() ); - PDFont pdFont = getFontAndUpdateResources( tokens, appearanceStream ); - - if (!containsMarkedContent( tokens )) + List tokens = getStreamTokens(appearanceStream); + List daTokens = getStreamTokens(getDefaultAppearance()); + PDFont pdFont = getFontAndUpdateResources(tokens, appearanceStream); + + // Special handling for listboxes to address PDFBOX-2249 + // TODO: Shall be addressed properly in a future release + if (parent instanceof PDChoiceField + && (parent.getFieldFlags() & ((PDChoiceField) parent).FLAG_COMBO) == 0) { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - - //BJL 9/25/2004 Must prepend existing stream - //because it might have operators to draw things like - //rectangles and such - ContentStreamWriter writer = new ContentStreamWriter( output ); - writer.writeTokens( tokens ); - - output.write( " /Tx BMC\n".getBytes("ISO-8859-1") ); - insertGeneratedAppearance( widget, output, pdFont, tokens, appearanceStream ); - output.write( " EMC".getBytes("ISO-8859-1") ); - writeToStream( output.toByteArray(), appearanceStream ); + generateListboxAppearance(widget, pdFont, tokens, daTokens, appearanceStream, value); } else { - if( tokens != null ) + if (!containsMarkedContent(tokens)) { - if( daTokens != null ) - { - int bmcIndex = tokens.indexOf( PDFOperator.getOperator( "BMC" )); - int emcIndex = tokens.indexOf( PDFOperator.getOperator( "EMC" )); - if( bmcIndex != -1 && emcIndex != -1 && - emcIndex == bmcIndex+1 ) - { - //if the EMC immediately follows the BMC index then should - //insert the daTokens inbetween the two markers. - tokens.addAll( emcIndex, daTokens ); - } - } ByteArrayOutputStream output = new ByteArrayOutputStream(); - ContentStreamWriter writer = new ContentStreamWriter( output ); - float fontSize = calculateFontSize( pdFont, appearanceStream.getBoundingBox(), tokens, null ); - boolean foundString = false; - for( int i=0; i<tokens.size(); i++ ) + + // BJL 9/25/2004 Must prepend existing stream + // because it might have operators to draw things like + // rectangles and such + ContentStreamWriter writer = new ContentStreamWriter(output); + writer.writeTokens(tokens); + + output.write(" /Tx BMC\n".getBytes("ISO-8859-1")); + insertGeneratedAppearance(widget, output, pdFont, tokens, appearanceStream); + output.write(" EMC".getBytes("ISO-8859-1")); + writeToStream(output.toByteArray(), appearanceStream); + } + else + { + if (tokens != null) { - if( tokens.get( i ) instanceof COSString ) + if (daTokens != null) { - foundString = true; - COSString drawnString =((COSString)tokens.get(i)); - drawnString.reset(); - drawnString.append( apValue.getBytes("ISO-8859-1") ); + int bmcIndex = tokens.indexOf(PDFOperator.getOperator("BMC")); + int emcIndex = tokens.indexOf(PDFOperator.getOperator("EMC")); + if (bmcIndex != -1 && emcIndex != -1 && emcIndex == bmcIndex + 1) + { + // if the EMC immediately follows the BMC index then should + // insert the daTokens inbetween the two markers. + tokens.addAll(emcIndex, daTokens); + } } - } - int setFontIndex = tokens.indexOf( PDFOperator.getOperator( "Tf" )); - tokens.set( setFontIndex-1, new COSFloat( fontSize ) ); - if( foundString ) - { - writer.writeTokens( tokens ); - } - else - { - int bmcIndex = tokens.indexOf( PDFOperator.getOperator( "BMC" ) ); - int emcIndex = tokens.indexOf( PDFOperator.getOperator( "EMC" ) ); - - if( bmcIndex != -1 ) + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ContentStreamWriter writer = new ContentStreamWriter(output); + float fontSize = calculateFontSize(pdFont, appearanceStream.getBoundingBox(), tokens, null); + boolean foundString = false; + for (int i = 0; i < tokens.size(); i++) { - writer.writeTokens( tokens, 0, bmcIndex+1 ); + if (tokens.get(i) instanceof COSString) + { + foundString = true; + COSString drawnString = ((COSString) tokens.get(i)); + drawnString.reset(); + drawnString.append(apValue.getBytes("ISO-8859-1")); + } } - else + int setFontIndex = tokens.indexOf(PDFOperator.getOperator("Tf")); + tokens.set(setFontIndex - 1, new COSFloat(fontSize)); + if (foundString) { - writer.writeTokens( tokens ); + writer.writeTokens(tokens); } - output.write( "\n".getBytes("ISO-8859-1") ); - insertGeneratedAppearance( widget, output, - pdFont, tokens, appearanceStream ); - if( emcIndex != -1 ) + else { - writer.writeTokens( tokens, emcIndex, tokens.size() ); + int bmcIndex = tokens.indexOf(PDFOperator.getOperator("BMC")); + int emcIndex = tokens.indexOf(PDFOperator.getOperator("EMC")); + + if (bmcIndex != -1) + { + writer.writeTokens(tokens, 0, bmcIndex + 1); + } + else + { + writer.writeTokens(tokens); + } + output.write("\n".getBytes("ISO-8859-1")); + insertGeneratedAppearance(widget, output, pdFont, tokens, appearanceStream); + if (emcIndex != -1) + { + writer.writeTokens(tokens, emcIndex, tokens.size()); + } } + writeToStream(output.toByteArray(), appearanceStream); + } + else + { + // hmm? } - writeToStream( output.toByteArray(), appearanceStream ); } - else + } + } + } + + } + + private void generateListboxAppearance(PDAnnotationWidget fieldWidget, PDFont pdFont, List tokens, List daTokens, + PDAppearanceStream appearanceStream, String fieldValue) throws IOException + { + + // create paddingEdge and contentArea from bounding box + // Default the contentArea to the boundingBox + // taking the padding into account + paddingEdge = applyPadding(appearanceStream.getBoundingBox(), DEFAULT_PADDING); + contentArea = applyPadding(paddingEdge, DEFAULT_PADDING); + + if (!containsMarkedContent(tokens)) + { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + // BJL 9/25/2004 Must prepend existing stream + // because it might have operators to draw things like + // rectangles and such + ContentStreamWriter writer = new ContentStreamWriter(output); + writer.writeTokens(tokens); + + output.write(" /Tx BMC\n".getBytes("ISO-8859-1")); + insertGeneratedListboxAppearance(fieldWidget, output, pdFont, tokens, appearanceStream); + output.write(" EMC".getBytes("ISO-8859-1")); + writeToStream(output.toByteArray(), appearanceStream); + } + else + { + if (tokens != null) + { + if (daTokens != null) + { + int bmcIndex = tokens.indexOf(PDFOperator.getOperator("BMC")); + int emcIndex = tokens.indexOf(PDFOperator.getOperator("EMC")); + if (bmcIndex != -1 && emcIndex != -1 && emcIndex == bmcIndex + 1) { - //hmm? + // if the EMC immediately follows the BMC index then should + // insert the daTokens inbetween the two markers. + tokens.addAll(emcIndex, daTokens); } } + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ContentStreamWriter writer = new ContentStreamWriter(output); + float fontSize = calculateListboxFontSize(pdFont, appearanceStream.getBoundingBox(), tokens, daTokens); + boolean foundString = false; + + int setFontIndex = tokens.indexOf(PDFOperator.getOperator("Tf")); + tokens.set(setFontIndex - 1, new COSFloat(fontSize)); + + int bmcIndex = tokens.indexOf(PDFOperator.getOperator("BMC")); + /* + * Get the contentArea. + * + * There might be an inner box defined which defines the area where the text is printed. This typically + * looks like ... q 1 1 98 70 re W ... + */ + + { + int beginTextIndex = tokens.indexOf(PDFOperator.getOperator("BT")); + if (beginTextIndex != -1) + { + + ListIterator innerTokens = tokens.listIterator(bmcIndex); + + while (innerTokens.hasNext()) + { + if (innerTokens.next() == PDFOperator.getOperator("re") + && innerTokens.next() == PDFOperator.getOperator("W")) + { + + COSArray array = new COSArray(); + array.add((COSNumber) tokens.get(innerTokens.previousIndex() - 5)); + array.add((COSNumber) tokens.get(innerTokens.previousIndex() - 4)); + array.add((COSNumber) tokens.get(innerTokens.previousIndex() - 3)); + array.add((COSNumber) tokens.get(innerTokens.previousIndex() - 2)); + + paddingEdge = new PDRectangle(array); + + // as the re operator is using start and width/height adjust the generated + // dimensions + paddingEdge.setUpperRightX(paddingEdge.getLowerLeftX() + paddingEdge.getUpperRightX()); + paddingEdge.setUpperRightY(paddingEdge.getLowerLeftY() + paddingEdge.getUpperRightY()); + + contentArea = applyPadding(paddingEdge, paddingEdge.getLowerLeftX() + - appearanceStream.getBoundingBox().getLowerLeftX()); + + break; + } + } + } + } + + int emcIndex = tokens.indexOf(PDFOperator.getOperator("EMC")); + + if (bmcIndex != -1) + { + writer.writeTokens(tokens, 0, bmcIndex + 1); + } + else + { + writer.writeTokens(tokens); + } + output.write("\n".getBytes("ISO-8859-1")); + insertGeneratedListboxAppearance(fieldWidget, output, pdFont, tokens, appearanceStream); + if (emcIndex != -1) + { + writer.writeTokens(tokens, emcIndex, tokens.size()); + } + + writeToStream(output.toByteArray(), appearanceStream); + } + else + { + // hmm? } } } - private void insertGeneratedAppearance( PDAnnotationWidget fieldWidget, OutputStream output, - PDFont pdFont, List tokens, PDAppearanceStream appearanceStream ) throws IOException + private void insertGeneratedAppearance(PDAnnotationWidget fieldWidget, OutputStream output, PDFont pdFont, + List tokens, PDAppearanceStream appearanceStream) throws IOException { - PrintWriter printWriter = new PrintWriter( output, true ); + PrintWriter printWriter = new PrintWriter(output, true); float fontSize = 0.0f; PDRectangle boundingBox = appearanceStream.getBoundingBox(); - if( boundingBox == null ) + if (boundingBox == null) { boundingBox = fieldWidget.getRectangle().createRetranslatedRectangle(); } - printWriter.println( "BT" ); - if( defaultAppearance != null ) + printWriter.println("BT"); + if (defaultAppearance != null) { String daString = defaultAppearance.getString(); - PDFStreamParser daParser = new PDFStreamParser(new ByteArrayInputStream( daString.getBytes("ISO-8859-1") ), null ); + PDFStreamParser daParser = new PDFStreamParser(new ByteArrayInputStream(daString.getBytes("ISO-8859-1")), + null); daParser.parse(); List<Object> daTokens = daParser.getTokens(); - fontSize = calculateFontSize( pdFont, boundingBox, tokens, daTokens ); - int fontIndex = daTokens.indexOf( PDFOperator.getOperator( "Tf" ) ); - if(fontIndex != -1 ) + fontSize = calculateFontSize(pdFont, boundingBox, tokens, daTokens); + int fontIndex = daTokens.indexOf(PDFOperator.getOperator("Tf")); + if (fontIndex != -1) { - daTokens.set( fontIndex-1, new COSFloat( fontSize ) ); + daTokens.set(fontIndex - 1, new COSFloat(fontSize)); } ContentStreamWriter daWriter = new ContentStreamWriter(output); - daWriter.writeTokens( daTokens ); + daWriter.writeTokens(daTokens); } - printWriter.println( getTextPosition( boundingBox, pdFont, fontSize, tokens ) ); + printWriter.println(getTextPosition(boundingBox, pdFont, fontSize, tokens)); int q = getQ(); - if( q == PDTextbox.QUADDING_LEFT ) + if (q == PDTextbox.QUADDING_LEFT) { - //do nothing because left is default + // do nothing because left is default } - else if( q == PDTextbox.QUADDING_CENTERED || - q == PDTextbox.QUADDING_RIGHT ) + else if (q == PDTextbox.QUADDING_CENTERED || q == PDTextbox.QUADDING_RIGHT) { float fieldWidth = boundingBox.getWidth(); - float stringWidth = (pdFont.getStringWidth( value )/1000)*fontSize; + float stringWidth = (pdFont.getStringWidth(value) / 1000) * fontSize; float adjustAmount = fieldWidth - stringWidth - 4; - if( q == PDTextbox.QUADDING_CENTERED ) + if (q == PDTextbox.QUADDING_CENTERED) { - adjustAmount = adjustAmount/2.0f; + adjustAmount = adjustAmount / 2.0f; } - printWriter.println( adjustAmount + " 0 Td" ); + printWriter.println(adjustAmount + " 0 Td"); } else { - throw new IOException( "Error: Unknown justification value:" + q ); + throw new IOException("Error: Unknown justification value:" + q); } // add the value as hex string to deal with non ISO-8859-1 data values if (!isMultiLineValue(value)) @@ -413,36 +594,171 @@ public class PDAppearance printWriter.flush(); } - private PDFont getFontAndUpdateResources( List tokens, PDAppearanceStream appearanceStream ) throws IOException + private void insertGeneratedListboxAppearance(PDAnnotationWidget fieldWidget, OutputStream output, PDFont pdFont, + List tokens, PDAppearanceStream appearanceStream) throws IOException + { + PrintWriter printWriter = new PrintWriter(output, true); + float fontSize = 0.0f; + PDRectangle boundingBox = appearanceStream.getBoundingBox(); + if (boundingBox == null) + { + boundingBox = fieldWidget.getRectangle().createRetranslatedRectangle(); + } + + List<Object> daTokens = null; + + if (defaultAppearance != null) + { + String daString = defaultAppearance.getString(); + PDFStreamParser daParser = new PDFStreamParser(new ByteArrayInputStream(daString.getBytes("ISO-8859-1")), + null); + daParser.parse(); + daTokens = daParser.getTokens(); + + fontSize = calculateListboxFontSize(pdFont, contentArea, tokens, daTokens); + int fontIndex = daTokens.indexOf(PDFOperator.getOperator("Tf")); + if (fontIndex != -1) + { + daTokens.set(fontIndex - 1, new COSFloat(fontSize)); + } + } + + // print the paddingEdge + printWriter.println("q"); + + printWriter.println(paddingEdge.getLowerLeftX() + " " + paddingEdge.getLowerLeftY() + " " + + paddingEdge.getWidth() + " " + paddingEdge.getHeight() + " " + " re"); + printWriter.println("W"); + printWriter.println("n"); + + // print the highlight color + printWriter.println(HIGHLIGHT_COLOR); + + /* + * for a listbox output the rectangle highlighting the selected value + */ + + COSArray indexEntries = ((PDChoiceField) parent).getSelectedOptions(); + + int selectedIndex = ((COSInteger) indexEntries.get(0)).intValue(); + + // The first entry which shall be presented might be adjusted by the optional TI key + // If this entry is present the first entry to be displayed is the keys value otherwise + // display starts with the first entry in Opt. + int topIndex = ((PDChoiceField) parent).getTopIndex(); + + if ("Ch".equals(parent.findFieldType()) && ((parent.getFieldFlags() & (0x1000000)) == 0)) + { + + float highlightBoxHeight = pdFont.getFontBoundingBox().getHeight() / 1000 * fontSize; + + printWriter.println(paddingEdge.getLowerLeftX() + " " + + (paddingEdge.getUpperRightY() - highlightBoxHeight * (selectedIndex - topIndex + 1)) + " " + + paddingEdge.getWidth() + " " + (highlightBoxHeight) + " re"); + printWriter.println("f"); + printWriter.println("0 g"); + printWriter.println("0 G"); + printWriter.println("1 w"); + } + + // start of text output + printWriter.println("BT"); + + if (defaultAppearance != null) + { + ContentStreamWriter daWriter = new ContentStreamWriter(output); + daWriter.writeTokens(daTokens); + } + + int q = getQ(); + if (q == PDTextbox.QUADDING_LEFT) + { + // do nothing because left is default + } + else if (q == PDTextbox.QUADDING_CENTERED || q == PDTextbox.QUADDING_RIGHT) + { + float fieldWidth = boundingBox.getWidth(); + float stringWidth = (pdFont.getStringWidth(value) / 1000) * fontSize; + float adjustAmount = fieldWidth - stringWidth - 4; + + if (q == PDTextbox.QUADDING_CENTERED) + { + adjustAmount = adjustAmount / 2.0f; + } + + printWriter.println(adjustAmount + " 0 Td"); + } + else + { + throw new IOException("Error: Unknown justification value:" + q); + } + + COSArray options = ((PDChoiceField) parent).getOptions(); + + float yTextPos = contentArea.getUpperRightY(); + + for (int i = topIndex; i < options.size(); i++) + { + COSBase option = options.getObject(i); + COSArray optionPair = (COSArray) option; + COSString optionKey = (COSString) optionPair.getObject(0); + COSString optionValue = (COSString) optionPair.getObject(1); + + if (i == topIndex) + { + yTextPos = yTextPos - pdFont.getFontDescriptor().getAscent() / 1000 * fontSize; + } + else + { + yTextPos = yTextPos - pdFont.getFontBoundingBox().getHeight() / 1000 * fontSize; + printWriter.println("BT"); + } + + printWriter.println(contentArea.getLowerLeftX() + " " + yTextPos + " Td"); + printWriter.println("<" + optionValue.getHexString() + "> Tj"); + + if (i - topIndex != (options.size() - 1)) + { + printWriter.println("ET"); + } + + } + + printWriter.println("ET"); + printWriter.println("Q"); + printWriter.flush(); + } + + private PDFont getFontAndUpdateResources(List tokens, PDAppearanceStream appearanceStream) throws IOException { PDFont retval = null; PDResources streamResources = appearanceStream.getResources(); PDResources formResources = acroForm.getDefaultResources(); - if( formResources != null ) + if (formResources != null) { - if( streamResources == null ) + if (streamResources == null) { streamResources = new PDResources(); - appearanceStream.setResources( streamResources ); + appearanceStream.setResources(streamResources); } COSString da = getDefaultAppearance(); - if( da != null ) + if (da != null) { String data = da.getString(); PDFStreamParser streamParser = new PDFStreamParser( - new ByteArrayInputStream( data.getBytes("ISO-8859-1") ), null ); + new ByteArrayInputStream(data.getBytes("ISO-8859-1")), null); streamParser.parse(); tokens = streamParser.getTokens(); } - int setFontIndex = tokens.indexOf( PDFOperator.getOperator( "Tf" )); - COSName cosFontName = (COSName)tokens.get( setFontIndex-2 ); + int setFontIndex = tokens.indexOf(PDFOperator.getOperator("Tf")); + COSName cosFontName = (COSName) tokens.get(setFontIndex - 2); String fontName = cosFontName.getName(); - retval = (PDFont)streamResources.getFonts().get( fontName ); - if( retval == null ) + retval = (PDFont) streamResources.getFonts().get(fontName); + if (retval == null) { - retval = (PDFont)formResources.getFonts().get( fontName ); + retval = (PDFont) formResources.getFonts().get(fontName); streamResources.addFont(retval, fontName); } } @@ -457,56 +773,57 @@ public class PDAppearance /** * Writes the stream to the actual stream in the COSStream. * - * @throws IOException If there is an error writing to the stream + * @throws IOException + * If there is an error writing to the stream */ - private void writeToStream( byte[] data, PDAppearanceStream appearanceStream ) throws IOException + private void writeToStream(byte[] data, PDAppearanceStream appearanceStream) throws IOException { OutputStream out = appearanceStream.getStream().createUnfilteredStream(); - out.write( data ); + out.write(data); out.flush(); } /** * w in an appearance stream represents the lineWidth. + * * @return the linewidth */ - private float getLineWidth( List tokens ) + private float getLineWidth(List tokens) { float retval = 1; - if( tokens != null ) + if (tokens != null) { - int btIndex = tokens.indexOf(PDFOperator.getOperator( "BT" )); - int wIndex = tokens.indexOf(PDFOperator.getOperator( "w" )); - //the w should only be used if it is before the first BT. - if( (wIndex > 0) && (wIndex < btIndex) ) + int btIndex = tokens.indexOf(PDFOperator.getOperator("BT")); + int wIndex = tokens.indexOf(PDFOperator.getOperator("w")); + // the w should only be used if it is before the first BT. + if ((wIndex > 0) && (wIndex < btIndex)) { - retval = ((COSNumber)tokens.get(wIndex-1)).floatValue(); + retval = ((COSNumber) tokens.get(wIndex - 1)).floatValue(); } } return retval; } - private PDRectangle getSmallestDrawnRectangle( PDRectangle boundingBox, List tokens ) + private PDRectangle getSmallestDrawnRectangle(PDRectangle boundingBox, List tokens) { PDRectangle smallest = boundingBox; - for( int i=0; i<tokens.size(); i++ ) + for (int i = 0; i < tokens.size(); i++) { - Object next = tokens.get( i ); - if( next == PDFOperator.getOperator( "re" ) ) + Object next = tokens.get(i); + if (next == PDFOperator.getOperator("re")) { - COSNumber x = (COSNumber)tokens.get( i-4 ); - COSNumber y = (COSNumber)tokens.get( i-3 ); - COSNumber width = (COSNumber)tokens.get( i-2 ); - COSNumber height = (COSNumber)tokens.get( i-1 ); + COSNumber x = (COSNumber) tokens.get(i - 4); + COSNumber y = (COSNumber) tokens.get(i - 3); + COSNumber width = (COSNumber) tokens.get(i - 2); + COSNumber height = (COSNumber) tokens.get(i - 1); PDRectangle potentialSmallest = new PDRectangle(); - potentialSmallest.setLowerLeftX( x.floatValue() ); - potentialSmallest.setLowerLeftY( y.floatValue() ); - potentialSmallest.setUpperRightX( x.floatValue() + width.floatValue() ); - potentialSmallest.setUpperRightY( y.floatValue() + height.floatValue() ); - if( smallest == null || - smallest.getLowerLeftX() < potentialSmallest.getLowerLeftX() || - smallest.getUpperRightY() > potentialSmallest.getUpperRightY() ) + potentialSmallest.setLowerLeftX(x.floatValue()); + potentialSmallest.setLowerLeftY(y.floatValue()); + potentialSmallest.setUpperRightX(x.floatValue() + width.floatValue()); + potentialSmallest.setUpperRightY(y.floatValue() + height.floatValue()); + if (smallest == null || smallest.getLowerLeftX() < potentialSmallest.getLowerLeftX() + || smallest.getUpperRightY() > potentialSmallest.getUpperRightY()) { smallest = potentialSmallest; } @@ -517,119 +834,171 @@ public class PDAppearance } /** - * 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 height. + * @throws IOException + * If there is an error getting the font height. */ - private float calculateFontSize( PDFont pdFont, PDRectangle boundingBox, List tokens, List daTokens ) - throws IOException + private float calculateFontSize(PDFont pdFont, PDRectangle boundingBox, List tokens, List daTokens) + throws IOException { float fontSize = 0; - if( daTokens != null ) + if (daTokens != null) { - //daString looks like "BMC /Helv 3.4 Tf EMC" + // daString looks like "BMC /Helv 3.4 Tf EMC" - int fontIndex = daTokens.indexOf( PDFOperator.getOperator( "Tf" ) ); - if(fontIndex != -1 ) + int fontIndex = daTokens.indexOf(PDFOperator.getOperator("Tf")); + if (fontIndex != -1) { - fontSize = ((COSNumber)daTokens.get(fontIndex-1)).floatValue(); + fontSize = ((COSNumber) daTokens.get(fontIndex - 1)).floatValue(); } } - + float widthBasedFontSize = Float.MAX_VALUE; - - if( parent.doNotScroll() ) + + if (parent.doNotScroll()) { - //if we don't scroll then we will shrink the font to fit into the text area. - float widthAtFontSize1 = pdFont.getStringWidth( value )/1000.f; + // if we don't scroll then we will shrink the font to fit into the text area. + float widthAtFontSize1 = pdFont.getStringWidth(value) / 1000.f; float availableWidth = getAvailableWidth(boundingBox, getLineWidth(tokens)); widthBasedFontSize = availableWidth / widthAtFontSize1; } - else if( fontSize == 0 ) + else if (fontSize == 0) { - float lineWidth = getLineWidth( tokens ); - float stringWidth = pdFont.getStringWidth( value ); + float lineWidth = getLineWidth(tokens); + float stringWidth = pdFont.getStringWidth(value); float height = 0; - if( pdFont instanceof PDSimpleFont ) + if (pdFont instanceof PDSimpleFont) { - height = ((PDSimpleFont)pdFont).getFontDescriptor().getFontBoundingBox().getHeight(); + height = ((PDSimpleFont) pdFont).getFontDescriptor().getFontBoundingBox().getHeight(); } else { - //now much we can do, so lets assume font is square and use width - //as the height + // now much we can do, so lets assume font is square and use width + // as the height height = pdFont.getAverageFontWidth(); } - height = height/1000f; + height = height / 1000f; - float availHeight = getAvailableHeight( boundingBox, lineWidth ); - fontSize = Math.min((availHeight/height), widthBasedFontSize); + float availHeight = getAvailableHeight(boundingBox, lineWidth); + fontSize = Math.min((availHeight / height), widthBasedFontSize); } return fontSize; } /** - * Calculates where to start putting the text in the box. - * The positioning is not quite as accurate as when Acrobat + * Calculate the fontsize if autosizing was set. + * + * @return the calculated font-size + * + * @throws IOException + * If there is an error getting the font height. + */ + private float calculateListboxFontSize(PDFont pdFont, PDRectangle contentArea, List tokens, List daTokens) + throws IOException + { + float fontSize = 0; + if (daTokens != null) + { + // daString looks like "BMC /Helv 3.4 Tf EMC" + + int fontIndex = daTokens.indexOf(PDFOperator.getOperator("Tf")); + if (fontIndex != -1) + { + fontSize = ((COSNumber) daTokens.get(fontIndex - 1)).floatValue(); + } + } + + // font size of 0 means that the font size has to be calculated + // dependent on the strings length so that the string with the + // largest width fits into the box available. + if (fontSize == 0) + { + + COSArray options = ((PDChoiceField) parent).getOptions(); + + float maxOptWidth = 0; + + for (int i = 0; i < options.size(); i++) + { + + COSBase option = options.getObject(i); + COSArray optionPair = (COSArray) option; + COSString optionValue = (COSString) optionPair.getObject(1); + maxOptWidth = Math.max(pdFont.getStringWidth(optionValue.getString()) / 1000.f, maxOptWidth); + } + + float availableWidth = getAvailableWidth(contentArea, getLineWidth(tokens)); + fontSize = availableWidth / maxOptWidth; + } + + return fontSize; + } + + /** + * Calculates where to start putting the text in the box. The positioning is not quite as accurate as when Acrobat * places the elements, but it works though. * * @return the sting for representing the start position of the text * - * @throws IOException If there is an error calculating the text position. + * @throws IOException + * If there is an error calculating the text position. */ - private String getTextPosition( PDRectangle boundingBox, PDFont pdFont, float fontSize, List tokens ) - throws IOException + private String getTextPosition(PDRectangle boundingBox, PDFont pdFont, float fontSize, List tokens) + throws IOException { - float lineWidth = getLineWidth( tokens ); + float lineWidth = getLineWidth(tokens); float pos = 0.0f; - if(parent.isMultiline()) + if (parent.isMultiline()) { - int rows = (int) (getAvailableHeight( boundingBox, lineWidth ) / ((int) fontSize)); - pos = ((rows)*fontSize)-fontSize; + int rows = (int) (getAvailableHeight(boundingBox, lineWidth) / ((int) fontSize)); + pos = ((rows) * fontSize) - fontSize; } else { - if( pdFont instanceof PDSimpleFont ) + if (pdFont instanceof PDSimpleFont) { - //BJL 9/25/2004 - //This algorithm is a little bit of black magic. It does - //not appear to be documented anywhere. Through examining a few - //PDF documents and the value that Acrobat places in there I - //have determined that the below method of computing the position - //is correct for certain documents, but maybe not all. It does - //work f1040ez.pdf and Form_1.pdf - PDFontDescriptor fd = ((PDSimpleFont)pdFont).getFontDescriptor(); + // BJL 9/25/2004 + // This algorithm is a little bit of black magic. It does + // not appear to be documented anywhere. Through examining a few + // PDF documents and the value that Acrobat places in there I + // have determined that the below method of computing the position + // is correct for certain documents, but maybe not all. It does + // work f1040ez.pdf and Form_1.pdf + PDFontDescriptor fd = ((PDSimpleFont) pdFont).getFontDescriptor(); float bBoxHeight = boundingBox.getHeight(); float fontHeight = fd.getFontBoundingBox().getHeight() + 2 * fd.getDescent(); - fontHeight = (fontHeight/1000) * fontSize; - pos = (bBoxHeight - fontHeight)/2; + fontHeight = (fontHeight / 1000) * fontSize; + pos = (bBoxHeight - fontHeight) / 2; } else { - throw new IOException( "Error: Don't know how to calculate the position for non-simple fonts" ); + throw new IOException("Error: Don't know how to calculate the position for non-simple fonts"); } } - PDRectangle innerBox = getSmallestDrawnRectangle( boundingBox, tokens ); - float xInset = 2+ 2*(boundingBox.getWidth() - innerBox.getWidth()); - return Math.round(xInset) + " "+ pos + " Td"; + PDRectangle innerBox = getSmallestDrawnRectangle(boundingBox, tokens); + float xInset = 2 + 2 * (boundingBox.getWidth() - innerBox.getWidth()); + return Math.round(xInset) + " " + pos + " Td"; } /** * calculates the available width of the box. + * * @return the calculated available width of the box */ - private float getAvailableWidth( PDRectangle boundingBox, float lineWidth ) + private float getAvailableWidth(PDRectangle boundingBox, float lineWidth) { return boundingBox.getWidth() - 2 * lineWidth; } /** * calculates the available height of the box. + * * @return the calculated available height of the box */ - private float getAvailableHeight( PDRectangle boundingBox, float lineWidth ) + private float getAvailableHeight(PDRectangle boundingBox, float lineWidth) { return boundingBox.getHeight() - 2 * lineWidth; } Modified: pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDChoiceField.java URL: http://svn.apache.org/viewvc/pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDChoiceField.java?rev=1623830&r1=1623829&r2=1623830&view=diff ============================================================================== --- pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDChoiceField.java (original) +++ pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDChoiceField.java Tue Sep 9 14:20:30 2014 @@ -43,86 +43,151 @@ public class PDChoiceField extends PDVar */ public static final int FLAG_EDIT = 1 << 18; + private PDAppearance appearance; + /** * @see org.apache.pdfbox.pdmodel.interactive.form.PDField#PDField(PDAcroForm,COSDictionary) * - * @param theAcroForm The acroForm for this field. - * @param field The field for this choice field. + * @param theAcroForm + * The acroForm for this field. + * @param field + * The field for this choice field. */ - public PDChoiceField( PDAcroForm theAcroForm, COSDictionary field) + public PDChoiceField(PDAcroForm theAcroForm, COSDictionary field) { super(theAcroForm, field); } + private void setListboxValue(String value) throws IOException + { + COSString fieldValue = new COSString(value); + getDictionary().setItem(COSName.V, fieldValue); + + // hmm, not sure what the case where the DV gets set to the field + // value, for now leave blank until we can come up with a case + // where it needs to be in there + // getDictionary().setItem( COSName.getPDFName( "DV" ), fieldValue ); + if (appearance == null) + { + this.appearance = new PDAppearance(getAcroForm(), this); + } + appearance.setAppearanceValue(value); + } + /** * @see org.apache.pdfbox.pdmodel.interactive.form.PDField#setValue(java.lang.String) * - * @param optionValue The new value for this text field. + * @param optionValue + * The new value for this text field. * - * @throws IOException If there is an error calculating the appearance stream or the value in not one - * of the existing options. + * @throws IOException + * If there is an error calculating the appearance stream or the value in not one of the existing + * options. */ public void setValue(String optionValue) throws IOException { int indexSelected = -1; - COSArray options = (COSArray)getDictionary().getDictionaryObject( COSName.OPT ); + COSArray options = (COSArray) getDictionary().getDictionaryObject(COSName.OPT); int fieldFlags = getFieldFlags(); boolean isEditable = (FLAG_COMBO & fieldFlags) != 0 && (FLAG_EDIT & fieldFlags) != 0; - - if( options.size() == 0 && ! isEditable ) + + if (options.size() == 0 && !isEditable) { - throw new IOException( "Error: You cannot set a value for a choice field if there are no options." ); + throw new IOException("Error: You cannot set a value for a choice field if there are no options."); } else { // YXJ: Changed the order of the loops. Acrobat produces PDF's // where sometimes there is 1 string and the rest arrays. // This code works either way. - for( int i=0; i<options.size() && indexSelected == -1; i++ ) + for (int i = 0; i < options.size() && indexSelected == -1; i++) { - COSBase option = options.getObject( i ); - if( option instanceof COSArray ) + COSBase option = options.getObject(i); + if (option instanceof COSArray) { - COSArray keyValuePair = (COSArray)option; - COSString key = (COSString)keyValuePair.getObject( 0 ); - COSString value = (COSString)keyValuePair.getObject( 1 ); - if( optionValue.equals( key.getString() ) || optionValue.equals( value.getString() ) ) + COSArray keyValuePair = (COSArray) option; + COSString key = (COSString) keyValuePair.getObject(0); + COSString value = (COSString) keyValuePair.getObject(1); + if (optionValue.equals(key.getString()) || optionValue.equals(value.getString())) { - //have the parent draw the appearance stream with the value - super.setValue( value.getString() ); - //but then use the key as the V entry - getDictionary().setItem( COSName.V, key ); + // have the parent draw the appearance stream with the value + if ((FLAG_COMBO & fieldFlags) != 0) + { + super.setValue(value.getString()); + } + else + { + COSArray indexEntries = new COSArray(); + indexEntries.add(COSInteger.get((long) i)); + getDictionary().setItem(COSName.I, indexEntries); + setListboxValue(value.getString()); + } + // but then use the key as the V entry + getDictionary().setItem(COSName.V, key); indexSelected = i; + } } else { - COSString value = (COSString)option; - if( optionValue.equals( value.getString() ) ) + COSString value = (COSString) option; + if (optionValue.equals(value.getString())) { - super.setValue( optionValue ); + super.setValue(optionValue); indexSelected = i; } } } } - if( indexSelected == -1 && isEditable ) + if (indexSelected == -1 && isEditable) { - super.setValue( optionValue ); + super.setValue(optionValue); } - else if( indexSelected == -1 ) + else if (indexSelected == -1) { - throw new IOException( "Error: '" + optionValue + "' was not an available option."); + throw new IOException("Error: '" + optionValue + "' was not an available option."); } else { - COSArray indexArray = (COSArray)getDictionary().getDictionaryObject( COSName.I ); - if( indexArray != null ) + COSArray indexArray = (COSArray) getDictionary().getDictionaryObject(COSName.I); + if (indexArray != null) { indexArray.clear(); - indexArray.add( COSInteger.get( indexSelected ) ); + indexArray.add(COSInteger.get(indexSelected)); } } } + /** + * This will get the indices of the selected options "I". + * + * @return COSArray containing the indices of all selected options. + */ + public COSArray getSelectedOptions() + { + return (COSArray) getDictionary().getDictionaryObject(COSName.I); + } + + /** + * This will get the top index "TI" value. + * + * The value returned will be the first item to display in the listbox. + * + * @return the top index, default value 0. + */ + public int getTopIndex() + { + return getDictionary().getInt(COSName.getPDFName("TI"), 0); + } + + /** + * This will get the option values "Opt". + * + * @return COSArray containing all options. + */ + public COSArray getOptions() + { + return (COSArray) getDictionary().getDictionaryObject(COSName.OPT); + } + }