| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.pdfbox.pdmodel.interactive.form; |
| |
| import java.awt.geom.AffineTransform; |
| import java.awt.geom.GeneralPath; |
| import java.awt.geom.Point2D; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.List; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.fontbox.util.BoundingBox; |
| import org.apache.pdfbox.contentstream.operator.Operator; |
| import org.apache.pdfbox.cos.COSDictionary; |
| import org.apache.pdfbox.cos.COSName; |
| 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.PDRectangle; |
| import org.apache.pdfbox.pdmodel.font.PDFont; |
| import org.apache.pdfbox.pdmodel.font.PDSimpleFont; |
| import org.apache.pdfbox.pdmodel.font.PDType3CharProc; |
| import org.apache.pdfbox.pdmodel.font.PDType3Font; |
| import org.apache.pdfbox.pdmodel.font.PDVectorFont; |
| import org.apache.pdfbox.pdmodel.graphics.color.PDColor; |
| import org.apache.pdfbox.pdmodel.interactive.AppearanceStyle; |
| import org.apache.pdfbox.pdmodel.interactive.PlainText; |
| import org.apache.pdfbox.pdmodel.interactive.PlainTextFormatter; |
| import org.apache.pdfbox.pdmodel.interactive.action.PDAction; |
| import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript; |
| import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions; |
| import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; |
| import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceCharacteristicsDictionary; |
| import org.apache.pdfbox.pdmodel.PDAppearanceContentStream; |
| import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; |
| import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry; |
| import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; |
| import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; |
| import org.apache.pdfbox.util.Matrix; |
| |
| /** |
| * Create the AcroForms field appearance helper. |
| * |
| * @author Stephan Gerhard |
| * @author Ben Litchfield |
| */ |
| class AppearanceGeneratorHelper |
| { |
| private static final Logger LOG = LogManager.getLogger(AppearanceGeneratorHelper.class); |
| |
| private static final Operator BMC = Operator.getOperator("BMC"); |
| private static final Operator EMC = Operator.getOperator("EMC"); |
| |
| private final PDVariableText field; |
| |
| private PDDefaultAppearanceString defaultAppearance; |
| private String value; |
| |
| /** |
| * 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 float[] HIGHLIGHT_COLOR = {153/255f, 193/255f, 215/255f}; |
| |
| /** |
| * The scaling factor for font units to PDF units |
| */ |
| private static final int FONTSCALE = 1000; |
| |
| /** |
| * The default font size used for multiline text |
| */ |
| private static final float DEFAULT_FONT_SIZE = 12; |
| |
| /** |
| * 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. |
| */ |
| private static final float DEFAULT_PADDING = 0.5f; |
| |
| /** |
| * Constructs a COSAppearance from the given field. |
| * |
| * @param field the field which you wish to control the appearance of |
| * @throws IOException |
| */ |
| AppearanceGeneratorHelper(PDVariableText field) throws IOException |
| { |
| this.field = field; |
| validateAndEnsureAcroFormResources(); |
| |
| try |
| { |
| this.defaultAppearance = field.getDefaultAppearanceString(); |
| } |
| catch (IOException ex) |
| { |
| throw new IOException("Could not process default appearance string '" + |
| field.getDefaultAppearance() + "' for field '" + |
| field.getFullyQualifiedName() + "': " + ex.getMessage(), ex); |
| } |
| } |
| |
| /* |
| * Adobe Reader/Acrobat are adding resources which are at the field/widget level |
| * to the AcroForm level. |
| */ |
| 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 |
| PDResources acroFormResources = field.getAcroForm().getDefaultResources(); |
| if (acroFormResources == null) |
| { |
| return; |
| } |
| |
| for (PDAnnotationWidget widget : field.getWidgets()) |
| { |
| PDAppearanceStream stream = widget.getNormalAppearanceStream(); |
| if (stream == null) |
| { |
| continue; |
| } |
| PDResources widgetResources = stream.getResources(); |
| if (widgetResources == null) |
| { |
| continue; |
| } |
| COSDictionary widgetFontDict = widgetResources.getCOSObject() |
| .getCOSDictionary(COSName.FONT); |
| COSDictionary acroFormFontDict = acroFormResources.getCOSObject() |
| .getCOSDictionary(COSName.FONT); |
| for (COSName fontResourceName : widgetResources.getFontNames()) |
| { |
| try |
| { |
| if (acroFormResources.getFont(fontResourceName) == null) |
| { |
| LOG.debug("Adding font resource {} from widget to AcroForm", |
| fontResourceName); |
| // use the COS-object to preserve a possible indirect object reference |
| acroFormFontDict.setItem(fontResourceName, |
| widgetFontDict.getItem(fontResourceName)); |
| } |
| } |
| catch (IOException e) |
| { |
| LOG.warn("Unable to match field level font with AcroForm font", e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * This is the public method for setting the appearance stream. |
| * |
| * @param apValue the String value which the appearance should represent |
| * @throws IOException If there is an error creating the stream. |
| */ |
| public void setAppearanceValue(String apValue) throws IOException |
| { |
| value = getFormattedValue(apValue); |
| |
| // Treat multiline field values in single lines as single lime values. |
| // This is in line with how Adobe Reader behaves when entering text |
| // interactively but NOT how it behaves when the field value has been |
| // 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]", " "); |
| } |
| |
| for (PDAnnotationWidget widget : field.getWidgets()) |
| { |
| if (widget.getCOSObject().containsKey("PMD")) |
| { |
| LOG.warn( |
| "widget of field {} is a PaperMetaData widget, no appearance stream created", |
| field.getFullyQualifiedName()); |
| continue; |
| } |
| |
| // some fields have the /Da at the widget level if the |
| // widgets differ in layout. |
| PDDefaultAppearanceString acroFormAppearance = defaultAppearance; |
| |
| if (widget.getCOSObject().getDictionaryObject(COSName.DA) != null) |
| { |
| defaultAppearance = getWidgetDefaultAppearanceString(widget); |
| } |
| |
| PDRectangle rect = widget.getRectangle(); |
| if (rect == null) |
| { |
| widget.getCOSObject().removeItem(COSName.AP); |
| LOG.warn("widget of field {} has no rectangle, no appearance stream created", |
| field.getFullyQualifiedName()); |
| continue; |
| } |
| |
| PDAppearanceDictionary appearanceDict = widget.getAppearance(); |
| if (appearanceDict == null) |
| { |
| appearanceDict = new PDAppearanceDictionary(); |
| widget.setAppearance(appearanceDict); |
| } |
| |
| PDAppearanceEntry appearance = appearanceDict.getNormalAppearance(); |
| // TODO support appearances other than "normal" |
| |
| PDAppearanceStream appearanceStream; |
| if (isValidAppearanceStream(appearance)) |
| { |
| appearanceStream = appearance.getAppearanceStream(); |
| } |
| else |
| { |
| appearanceStream = prepareNormalAppearanceStream(widget); |
| appearanceDict.setNormalAppearance(appearanceStream); |
| // TODO support appearances other than "normal" |
| } |
| PDAppearanceCharacteristicsDictionary appearanceCharacteristics = |
| 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. |
| * |
| */ |
| if (appearanceCharacteristics != null || appearanceStream.getContentStream().getLength() == 0) |
| { |
| initializeAppearanceContent(widget, appearanceCharacteristics, appearanceStream); |
| } |
| |
| setAppearanceContent(widget, appearanceStream); |
| |
| |
| // restore the field level appearance |
| defaultAppearance = acroFormAppearance; |
| } |
| } |
| |
| private String getFormattedValue(String apValue) |
| { |
| // 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) |
| { |
| return apValue; |
| } |
| PDAction actionF = actions.getF(); |
| if (actionF != null) |
| { |
| if (field.getAcroForm().getScriptingHandler() != null) |
| { |
| ScriptingHandler scriptingHandler = field.getAcroForm().getScriptingHandler(); |
| return scriptingHandler.format((PDActionJavaScript) actionF, apValue); |
| } |
| LOG.info("Field contains a formatting action but no ScriptingHandler " + |
| "has been supplied - formatted value might be incorrect"); |
| } |
| return apValue; |
| } |
| |
| private static boolean isValidAppearanceStream(PDAppearanceEntry appearance) |
| { |
| if (appearance == null) |
| { |
| return false; |
| } |
| if (!appearance.isStream()) |
| { |
| return false; |
| } |
| PDRectangle bbox = appearance.getAppearanceStream().getBBox(); |
| if (bbox == null) |
| { |
| return false; |
| } |
| return Math.abs(bbox.getWidth()) > 0 && Math.abs(bbox.getHeight()) > 0; |
| } |
| |
| private PDAppearanceStream prepareNormalAppearanceStream(PDAnnotationWidget widget) |
| { |
| PDAppearanceStream appearanceStream = new PDAppearanceStream(field.getAcroForm().getDocument()); |
| |
| // Calculate the entries for the bounding box and the transformation matrix |
| // settings for the appearance stream |
| int rotation = resolveRotation(widget); |
| PDRectangle rect = widget.getRectangle(); |
| Matrix matrix = Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0); |
| Point2D.Float point2D = matrix.transformPoint(rect.getWidth(), rect.getHeight()); |
| |
| PDRectangle bbox = new PDRectangle(Math.abs((float) point2D.getX()), Math.abs((float) point2D.getY())); |
| appearanceStream.setBBox(bbox); |
| |
| AffineTransform at = calculateMatrix(bbox, rotation); |
| if (!at.isIdentity()) |
| { |
| appearanceStream.setMatrix(at); |
| } |
| appearanceStream.setFormType(1); |
| appearanceStream.setResources(new PDResources()); |
| return appearanceStream; |
| } |
| |
| 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) |
| { |
| // 0 is the default value if the R key doesn't exist |
| return characteristicsDictionary.getRotation(); |
| } |
| return 0; |
| } |
| |
| /** |
| * 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 |
| * |
| * @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 |
| { |
| 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(); |
| } |
| } |
| |
| writeToStream(output.toByteArray(), appearanceStream); |
| |
| } |
| } |
| |
| /** |
| * Constructs and sets new contents for given appearance stream. |
| */ |
| private void setAppearanceContent(PDAnnotationWidget widget, |
| PDAppearanceStream appearanceStream) throws IOException |
| { |
| // first copy any needed resources from the document’s DR dictionary into |
| // the stream’s Resources dictionary |
| defaultAppearance.copyNeededResourcesTo(appearanceStream); |
| |
| // then replace the existing contents of the appearance stream from /Tx BMC |
| // to the matching EMC |
| try (ByteArrayOutputStream output = new ByteArrayOutputStream()) |
| { |
| ContentStreamWriter writer = new ContentStreamWriter(output); |
| |
| List<Object> tokens = new PDFStreamParser(appearanceStream).parse(); |
| int bmcIndex = tokens.indexOf(BMC); |
| if (bmcIndex == -1) |
| { |
| // append to existing stream |
| writer.writeTokens(tokens); |
| writer.writeTokens(COSName.TX, BMC); |
| } |
| else |
| { |
| // prepend content before BMC |
| writer.writeTokens(tokens.subList(0, bmcIndex + 1)); |
| } |
| |
| // insert field contents |
| insertGeneratedAppearance(widget, appearanceStream, output); |
| |
| int emcIndex = tokens.indexOf(EMC); |
| if (emcIndex == -1) |
| { |
| // append EMC |
| writer.writeTokens(EMC); |
| } |
| else |
| { |
| // append contents after EMC |
| writer.writeTokens(tokens.subList(emcIndex, tokens.size())); |
| } |
| writeToStream(output.toByteArray(), appearanceStream); |
| } |
| } |
| |
| /** |
| * Generate and insert text content and clipping around it. |
| */ |
| private void insertGeneratedAppearance(PDAnnotationWidget widget, |
| PDAppearanceStream appearanceStream, |
| OutputStream output) throws IOException |
| { |
| try (PDAppearanceContentStream contents = new PDAppearanceContentStream(appearanceStream, output)) |
| { |
| 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); |
| PDRectangle contentRect = applyPadding(clipRect, padding); |
| |
| contents.saveGraphicsState(); |
| |
| // Acrobat always adds a clipping path |
| contents.addRect(clipRect.getLowerLeftX(), clipRect.getLowerLeftY(), |
| clipRect.getWidth(), clipRect.getHeight()); |
| contents.clip(); |
| |
| // get the font |
| PDFont font = defaultAppearance.getFont(); |
| if (font == null) |
| { |
| throw new IllegalArgumentException("font is null, check whether /DA entry is incomplete or incorrect"); |
| } |
| if (font.getName().contains("+")) |
| { |
| LOG.warn("Font '{}' of field '{}' contains subsetted font '{}'", |
| defaultAppearance.getFontName().getName(), field.getFullyQualifiedName(), |
| font.getName()); |
| LOG.warn("This may bring trouble with PDField.setValue(), PDAcroForm.flatten() or " + |
| "PDAcroForm.refreshAppearances()"); |
| LOG.warn("You should replace this font with a non-subsetted font:"); |
| LOG.warn("PDFont font = PDType0Font.load(doc, new FileInputStream(fontfile), false);"); |
| LOG.warn("acroForm.getDefaultResources().put(COSName.getPDFName(\"{}\", font);", |
| defaultAppearance.getFontName().getName()); |
| } |
| // calculate the fontSize (because 0 = autosize) |
| float fontSize = defaultAppearance.getFontSize(); |
| |
| if (Float.compare(fontSize, 0) == 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); |
| } |
| |
| // start the text output |
| contents.beginText(); |
| |
| // write font and color from the /DA string, with the calculated font size |
| defaultAppearance.writeTo(contents, fontSize); |
| |
| // calculate the y-position of the baseline |
| float y; |
| |
| // calculate font metrics at font size |
| float fontScaleY = fontSize / FONTSCALE; |
| float fontBoundingBoxAtSize = font.getBoundingBox().getHeight() * fontScaleY; |
| |
| float fontCapAtSize; |
| float fontDescentAtSize; |
| |
| if (font.getFontDescriptor() != null) { |
| fontCapAtSize = font.getFontDescriptor().getCapHeight() * fontScaleY; |
| fontDescentAtSize = font.getFontDescriptor().getDescent() * fontScaleY; |
| } else { |
| float fontCapHeight = resolveCapHeight(font); |
| float fontDescent = resolveDescent(font); |
| LOG.debug("missing font descriptor - resolved Cap/Descent to {}/{}", fontCapHeight, |
| fontDescent); |
| fontCapAtSize = fontCapHeight * fontScaleY; |
| fontDescentAtSize = fontDescent * fontScaleY; |
| } |
| |
| 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; |
| } |
| else |
| { |
| // calculate the position based on the content rectangle |
| y = clipRect.getLowerLeftY() + (clipRect.getHeight() - fontCapAtSize) / 2; |
| |
| // check to ensure that ascents and descents fit |
| if (y - clipRect.getLowerLeftY() < -fontDescentAtSize) { |
| |
| float fontDescentBased = -fontDescentAtSize + contentRect.getLowerLeftY(); |
| float fontCapBased = contentRect.getHeight() - contentRect.getLowerLeftY() - fontCapAtSize; |
| |
| y = Math.min(fontDescentBased, Math.max(y, fontCapBased)); |
| } |
| } |
| } |
| |
| // 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(); |
| } |
| } |
| |
| /* |
| * 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) |
| { |
| // 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) |
| { |
| return new AffineTransform(); |
| } |
| float tx = 0, ty = 0; |
| switch (rotation) |
| { |
| case 90: |
| tx = bbox.getUpperRightY(); |
| break; |
| case 180: |
| tx = bbox.getUpperRightY(); |
| ty = bbox.getUpperRightX(); |
| break; |
| case 270: |
| ty = bbox.getUpperRightX(); |
| break; |
| default: |
| break; |
| } |
| Matrix matrix = Matrix.getRotateInstance(Math.toRadians(rotation), tx, ty); |
| return matrix.createAffineTransform(); |
| } |
| |
| private boolean isMultiLine() |
| { |
| return field instanceof PDTextField && ((PDTextField) field).isMultiline(); |
| } |
| |
| /** |
| * Determine if the appearance shall provide a comb output. |
| * |
| * <p> |
| * 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, |
| * or combs, as the value of MaxLen, and the text is laid out into those combs. |
| * </p> |
| * |
| * @return the comb state |
| */ |
| 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 appearanceStream the appearance stream used |
| * @param font the font to be used |
| * @param fontSize the font size to be used |
| * @throws IOException |
| */ |
| private void insertGeneratedCombAppearance(PDAppearanceContentStream contents, PDAppearanceStream appearanceStream, |
| PDFont font, float fontSize) throws IOException |
| { |
| 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; |
| float ascentAtFontSize = font.getFontDescriptor().getAscent() / FONTSCALE * fontSize; |
| float baselineOffset = paddingEdge.getLowerLeftY() + |
| (appearanceStream.getBBox().getHeight() - ascentAtFontSize)/2; |
| |
| float prevCharWidth = 0f; |
| |
| float xOffset = combWidth / 2; |
| |
| // add to initial offset if right aligned or centered |
| if (quadding == 2) |
| { |
| xOffset = xOffset + (maxLen - numChars) * combWidth; |
| } |
| else if (quadding == 1) |
| { |
| xOffset = xOffset + Math.floorDiv(maxLen - numChars, 2) * combWidth; |
| } |
| |
| for (int i = 0; i < numChars; i++) |
| { |
| String combString = value.substring(i, i+1); |
| float currCharWidth = font.getStringWidth(combString) / FONTSCALE * fontSize/2; |
| |
| xOffset = xOffset + prevCharWidth/2 - currCharWidth/2; |
| |
| contents.newLineAtOffset(xOffset, baselineOffset); |
| contents.showText(combString); |
| |
| baselineOffset = 0; |
| prevCharWidth = currCharWidth; |
| xOffset = combWidth; |
| } |
| } |
| |
| private void insertGeneratedListboxSelectionHighlight(PDAppearanceContentStream contents, PDAppearanceStream appearanceStream, |
| PDFont font, float fontSize) throws IOException |
| { |
| PDListBox listBox = (PDListBox) field; |
| List<Integer> indexEntries = listBox.getSelectedOptionsIndex(); |
| List<String> values = listBox.getValue(); |
| List<String> options = listBox.getOptionsExportValues(); |
| |
| if (!values.isEmpty() && !options.isEmpty() && indexEntries.isEmpty()) |
| { |
| // create indexEntries from options |
| indexEntries = new ArrayList<>(values.size()); |
| for (String v : values) |
| { |
| indexEntries.add(options.indexOf(v)); |
| } |
| } |
| |
| // 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 = listBox.getTopIndex(); |
| |
| float highlightBoxHeight = font.getBoundingBox().getHeight() * fontSize / FONTSCALE; |
| |
| // the padding area |
| PDRectangle paddingEdge = applyPadding(appearanceStream.getBBox(), 1); |
| |
| 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); |
| contents.fill(); |
| } |
| contents.setNonStrokingColor(0f); |
| } |
| |
| |
| 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) |
| { |
| float fieldWidth = appearanceStream.getBBox().getWidth(); |
| float stringWidth = (font.getStringWidth(value) / FONTSCALE) * fontSize; |
| float adjustAmount = fieldWidth - stringWidth - 4; |
| |
| if (q == PDVariableText.QUADDING_CENTERED) |
| { |
| adjustAmount = adjustAmount / 2.0f; |
| } |
| |
| contents.newLineAtOffset(adjustAmount, 0); |
| } |
| else if (q != PDVariableText.QUADDING_LEFT) |
| { |
| throw new IOException("Error: Unknown justification value:" + q); |
| } |
| |
| List<String> options = ((PDListBox) field).getOptionsDisplayValues(); |
| int numOptions = options.size(); |
| |
| float yTextPos = contentRect.getUpperRightY(); |
| |
| int topIndex = ((PDListBox) field).getTopIndex(); |
| float ascent = font.getFontDescriptor().getAscent(); |
| float height = font.getBoundingBox().getHeight(); |
| |
| for (int i = topIndex; i < numOptions; i++) |
| { |
| if (i == topIndex) |
| { |
| yTextPos = yTextPos - ascent / FONTSCALE * fontSize; |
| } |
| else |
| { |
| yTextPos = yTextPos - height / FONTSCALE * fontSize; |
| contents.beginText(); |
| } |
| |
| contents.newLineAtOffset(contentRect.getLowerLeftX(), yTextPos); |
| contents.showText(options.get(i)); |
| |
| if (i != (numOptions - 1)) |
| { |
| contents.endText(); |
| } |
| } |
| } |
| |
| /** |
| * Writes the stream to the actual stream in the COSStream. |
| * |
| * @throws IOException If there is an error writing to the stream |
| */ |
| 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. |
| * |
| * @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 |
| { |
| float fontSize = defaultAppearance.getFontSize(); |
| |
| // zero is special, it means the text is auto-sized |
| if (Float.compare(fontSize, 0) == 0) |
| { |
| if (isMultiLine()) |
| { |
| PlainText textContent = new PlainText(value); |
| if (textContent.getParagraphs() != null) |
| { |
| float width = contentRect.getWidth() - contentRect.getLowerLeftX(); |
| float fs = MINIMUM_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()) |
| { |
| numLines += paragraph.getLines(font, fs, width).size(); |
| } |
| // calculate the height required for this font size |
| float fontScaleY = fs / FONTSCALE; |
| float leading = font.getBoundingBox().getHeight() * fontScaleY; |
| float height = leading * numLines; |
| |
| // if this font size didn't fit, use the prior size that did fit |
| if (height > contentRect.getHeight()) |
| { |
| return Math.max(fs - 1, MINIMUM_FONT_SIZE); |
| } |
| fs++; |
| } |
| 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(); |
| |
| // fit width |
| float width = font.getStringWidth(value) * font.getFontMatrix().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 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); |
| } |
| } |
| return fontSize; |
| } |
| |
| /* |
| * Resolve the cap height. |
| * |
| * This is a very basic implementation using the height of "H" as reference. |
| */ |
| private float resolveCapHeight(PDFont font) throws IOException { |
| return resolveGlyphHeight(font, "H".codePointAt(0)); |
| } |
| |
| /* |
| * Resolve the descent. |
| * |
| * This is a very basic implementation using the height of "y" - "a" as reference. |
| */ |
| private float resolveDescent(PDFont font) throws IOException { |
| return resolveGlyphHeight(font, "y".codePointAt(0)) - resolveGlyphHeight(font, "a".codePointAt(0)); |
| } |
| |
| // this calculates the real (except for type 3 fonts) individual glyph bounds |
| private float resolveGlyphHeight(PDFont font, int code) throws IOException { |
| GeneralPath path = null; |
| if (font instanceof PDType3Font) { |
| // It is difficult to calculate the real individual glyph bounds for type 3 |
| // fonts |
| // because these are not vector fonts, the content stream could contain almost |
| // anything |
| // that is found in page content streams. |
| PDType3Font t3Font = (PDType3Font) font; |
| PDType3CharProc charProc = t3Font.getCharProc(code); |
| if (charProc != null) { |
| BoundingBox fontBBox = t3Font.getBoundingBox(); |
| PDRectangle glyphBBox = charProc.getGlyphBBox(); |
| if (glyphBBox != null) { |
| // PDFBOX-3850: glyph bbox could be larger than the font bbox |
| glyphBBox.setLowerLeftX(Math.max(fontBBox.getLowerLeftX(), glyphBBox.getLowerLeftX())); |
| glyphBBox.setLowerLeftY(Math.max(fontBBox.getLowerLeftY(), glyphBBox.getLowerLeftY())); |
| glyphBBox.setUpperRightX(Math.min(fontBBox.getUpperRightX(), glyphBBox.getUpperRightX())); |
| glyphBBox.setUpperRightY(Math.min(fontBBox.getUpperRightY(), glyphBBox.getUpperRightY())); |
| path = glyphBBox.toGeneralPath(); |
| } |
| } |
| } else if (font instanceof PDVectorFont) { |
| PDVectorFont vectorFont = (PDVectorFont) font; |
| path = vectorFont.getPath(code); |
| } else if (font instanceof PDSimpleFont) { |
| PDSimpleFont simpleFont = (PDSimpleFont) font; |
| |
| // these two lines do not always work, e.g. for the TT fonts in file 032431.pdf |
| // which is why PDVectorFont is tried first. |
| String name = simpleFont.getEncoding().getName(code); |
| path = simpleFont.getPath(name); |
| } else { |
| // shouldn't happen, please open issue in JIRA |
| LOG.warn("Unknown font class: {}", font.getClass()); |
| } |
| if (path == null) { |
| return -1; |
| } |
| return (float) path.getBounds2D().getHeight(); |
| } |
| |
| /** |
| * Resolve the bounding box. |
| * |
| * @param fieldWidget the annotation widget. |
| * @param appearanceStream the annotations appearance stream. |
| * @return the resolved boundingBox. |
| */ |
| private PDRectangle resolveBoundingBox(PDAnnotationWidget fieldWidget, |
| PDAppearanceStream appearanceStream) |
| { |
| PDRectangle boundingBox = appearanceStream.getBBox(); |
| if (boundingBox == null) |
| { |
| boundingBox = fieldWidget.getRectangle().createRetranslatedRectangle(); |
| } |
| return boundingBox; |
| } |
| |
| /** |
| * Apply padding to a box. |
| * |
| * @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); |
| } |
| } |