Package org.apache.pdfbox.pdmodel.interactive.form

Source Code of org.apache.pdfbox.pdmodel.interactive.form.PDAppearanceString

/*
* 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.io.ByteArrayInputStream;
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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSFloat;
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.interactive.action.PDFormFieldAdditionalActions;
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.PDAnnotationWidget;
import org.apache.pdfbox.contentstream.operator.Operator;

/**
* A default appearance string contains any graphics state or text state operators needed to
* establish the graphics state parameters, such as text size and colour, for displaying the field's
* variable text. Only operators that are allowed within text objects shall occur in this string.
*
* @author Stephan Gerhard
* @author Ben Litchfield
*/
public final class PDAppearanceString
{

    private static final Log LOG = LogFactory.getLog(PDAppearanceString.class);

    private final PDVariableText parent;

    private String value;
    private final COSString defaultAppearance;

    private final PDAcroForm acroForm;
    private List<COSObjectable> widgets = new ArrayList<COSObjectable>();

    /**
     * Constructs a COSAppearance from the given field.
     *
     * @param theAcroForm the AcroForm that this field is part of.
     * @param field the field which you wish to control the appearance of
     */
    public PDAppearanceString(PDAcroForm theAcroForm, PDVariableText field)
    {
        acroForm = theAcroForm;
        parent = field;

        widgets = field.getKids();
        if (widgets == null)
        {
            widgets = new ArrayList<COSObjectable>();
            widgets.add(field.getWidget());
        }
        defaultAppearance = getDefaultAppearance();
    }

    /**
     * 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()
    {
        COSString dap = parent.getDefaultAppearance();
        if (dap == null)
        {
            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);
            }
            if (dap == null)
            {
                dap = (COSString) acroForm.getDictionary().getDictionaryObject(COSName.DA);
            }
        }
        return dap;
    }

    private int getQ()
    {
        int q = parent.getQ();
        if (parent.getDictionary().getDictionaryObject(COSName.Q) == null)
        {
            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)
                {
                    q = qNum.intValue();
                }
            }
        }
        return q;
    }

    /**
     * Extracts the original appearance stream into a list of tokens.
     *
     * @return The tokens in the original appearance stream
     */
    private List<Object> getStreamTokens(PDAppearanceStream appearanceStream) throws IOException
    {
        List<Object> tokens = new ArrayList<Object>();
        if (appearanceStream != null)
        {
            tokens = getStreamTokens(appearanceStream.getCOSStream());
        }
        return tokens;
    }

    private List<Object> getStreamTokens(COSString string) throws IOException
    {
        List<Object> tokens = new ArrayList<Object>();
        if (string != null)
        {
            ByteArrayInputStream stream = new ByteArrayInputStream(string.getBytes());
            PDFStreamParser parser = new PDFStreamParser(stream);
            parser.parse();
            tokens = parser.getTokens();
        }
        return tokens;
    }

    private List<Object> getStreamTokens(COSStream stream) throws IOException
    {
        List<Object> tokens = new ArrayList<Object>();
        if (stream != null)
        {
            PDFStreamParser parser = new PDFStreamParser(stream);
            parser.parse();
            tokens = parser.getTokens();
        }
        return tokens;
    }

    /**
     * Tests if the appearance stream already contains content.
     *
     * @param streamTokens individual tokens within the appearance stream
     *
     * @return true if it contains any content
     */
    private boolean containsMarkedContent(List<Object> streamTokens)
    {
        return streamTokens.contains(Operator.getOperator("BMC"));
    }

    /**
     * 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 = apValue;
        Iterator<COSObjectable> widgetIter = widgets.iterator();
        while (widgetIter.hasNext())
        {
            COSObjectable next = widgetIter.next();
            PDField field = null;
            PDAnnotationWidget widget;
            if (next instanceof PDField)
            {
                field = (PDField) next;
                widget = field.getWidget();
            }
            else
            {
                widget = (PDAnnotationWidget) next;
            }
            PDFormFieldAdditionalActions actions = null;
            if (field != null)
            {
                actions = field.getActions();
            }
            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.
            }
            else
            {
                PDAppearanceDictionary appearance = widget.getAppearance();
                if (appearance == null)
                {
                    appearance = new PDAppearanceDictionary();
                    widget.setAppearance(appearance);
                }

                PDAppearanceEntry normalAppearance = appearance.getNormalAppearance();
                // TODO support more than one appearance stream
                PDAppearanceStream appearanceStream =
                        normalAppearance.isStream() ? normalAppearance.getAppearanceStream() : null;
                if (appearanceStream == null)
                {
                    COSStream cosStream = acroForm.getDocument().getDocument().createCOSStream();
                    appearanceStream = new PDAppearanceStream(cosStream);
                    appearanceStream.setBBox(widget.getRectangle()
                            .createRetranslatedRectangle());
                    appearance.setNormalAppearance(appearanceStream);
                }

                List<Object> tokens = getStreamTokens(appearanceStream);
                List<Object> daTokens = getStreamTokens(getDefaultAppearance());
                PDFont pdFont = getFontAndUpdateResources(tokens, appearanceStream);

                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"));
                    insertGeneratedAppearance(widget, 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(Operator.getOperator("BMC"));
                            int emcIndex = tokens.indexOf(Operator.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.getBBox(), tokens, daTokens);
                        int setFontIndex = tokens.indexOf(Operator.getOperator("Tf"));
                        tokens.set(setFontIndex - 1, new COSFloat(fontSize));

                        int bmcIndex = tokens.indexOf(Operator.getOperator("BMC"));
                        int emcIndex = tokens.indexOf(Operator.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?
                    }
                }
            }
        }
    }

    private void insertGeneratedAppearance(PDAnnotationWidget fieldWidget, OutputStream output,
            PDFont pdFont, List<Object> tokens, PDAppearanceStream appearanceStream)
            throws IOException
    {
        PrintWriter printWriter = new PrintWriter(output, true);
        float fontSize = 0.0f;
        PDRectangle boundingBox = appearanceStream.getBBox();
        if (boundingBox == null)
        {
            boundingBox = fieldWidget.getRectangle().createRetranslatedRectangle();
        }
        printWriter.println("BT");
        if (defaultAppearance != null)
        {
            String daString = defaultAppearance.getString();
            PDFStreamParser daParser = new PDFStreamParser(new ByteArrayInputStream(
                    daString.getBytes("ISO-8859-1")));
            daParser.parse();
            List<Object> daTokens = daParser.getTokens();
            fontSize = calculateFontSize(pdFont, boundingBox, tokens, daTokens);
            int fontIndex = daTokens.indexOf(Operator.getOperator("Tf"));
            if (fontIndex != -1)
            {
                daTokens.set(fontIndex - 1, new COSFloat(fontSize));
            }
            ContentStreamWriter daWriter = new ContentStreamWriter(output);
            daWriter.writeTokens(daTokens);
        }

        PDRectangle borderEdge = getSmallestDrawnRectangle(boundingBox, tokens);

        // 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.
        // Need to revisit this for forms being generated with other software.
        float paddingLeft = Math.max(2, Math.round(4 * borderEdge.getLowerLeftX()));
        float paddingRight = Math.max(2,
                Math.round(4 * (boundingBox.getUpperRightX() - borderEdge.getUpperRightX())));
        float verticalOffset = getVerticalOffset(boundingBox, pdFont, fontSize, tokens);

        // Acrobat shifts the value so it aligns to the bottom if
        // the font's caps are larger than the height of the borderEdge
        //
        // This is based on a small sample of test files and might not be generally the case.
        // The fontHeight calculation has been taken from getVerticalOffset().
        // We potentially need to revisit that calculation
        float fontHeight = boundingBox.getHeight() - verticalOffset * 2;

        if (fontHeight + 2 * borderEdge.getLowerLeftX() > borderEdge.getHeight())
        {
            verticalOffset = pdFont.getBoundingBox().getHeight() / 1000 * fontSize
                    - borderEdge.getHeight();
        }

        float leftOffset = 0f;

        // Acrobat aligns left regardless of the quadding if the text is wider than the remaining width
        float stringWidth = (pdFont.getStringWidth(value) / 1000) * fontSize;
        int q = getQ();
        if (q == PDTextField.QUADDING_LEFT
                || stringWidth > borderEdge.getWidth() - paddingLeft - paddingRight)
        {
            leftOffset = paddingLeft;
        }
        else if (q == PDTextField.QUADDING_CENTERED)
        {
            leftOffset = (boundingBox.getWidth() - stringWidth) / 2;
        }
        else if (q == PDTextField.QUADDING_RIGHT)
        {
            leftOffset = boundingBox.getWidth() - stringWidth - paddingRight;
        }
        else
        {
            // Unknown quadding value - default to left
            printWriter.println(paddingLeft + " " + verticalOffset + " Td");
            LOG.debug("Unknown justification value, defaulting to left: " + q);
        }

        printWriter.println(leftOffset + " " + verticalOffset + " Td");

        // add the value as hex string to deal with non ISO-8859-1 data values
        if (!isMultiLineValue(value) || stringWidth > borderEdge.getWidth() - paddingLeft -
                paddingRight)
        {
            printWriter.println("<" + new COSString(value).getHexString() + "> Tj");
        }
        else
        {
            String[] paragraphs = value.split("\n");
            for (int i = 0; i < paragraphs.length; i++)
            {
                boolean lastLine = i == (paragraphs.length - 1);
                String endingTag = lastLine ? "> Tj\n" : "> Tj 0 -13 Td";
                printWriter.print("<" + new COSString(paragraphs[i]).getHexString() + endingTag);
            }
        }
        printWriter.println("ET");
        printWriter.flush();
    }

    private PDFont getFontAndUpdateResources(List<Object> tokens,
            PDAppearanceStream appearanceStream) throws IOException
    {
        PDFont retval = null;
        PDResources streamResources = appearanceStream.getResources();
        PDResources formResources = acroForm.getDefaultResources();
        if (formResources != null)
        {
            if (streamResources == null)
            {
                streamResources = new PDResources();
                appearanceStream.setResources(streamResources);
            }

            COSString da = getDefaultAppearance();
            if (da != null)
            {
                String data = da.getString();
                PDFStreamParser streamParser = new PDFStreamParser(new ByteArrayInputStream(
                        data.getBytes("ISO-8859-1")));
                streamParser.parse();
                tokens = streamParser.getTokens();
            }

            int setFontIndex = tokens.indexOf(Operator.getOperator("Tf"));
            COSName cosFontName = (COSName) tokens.get(setFontIndex - 2);
            retval = streamResources.getFont(cosFontName);
            if (retval == null)
            {
                retval = formResources.getFont(cosFontName);
                streamResources.put(cosFontName, retval);
            }
        }
        return retval;
    }

    private boolean isMultiLineValue(String multiLineValue)
    {
        return (parent.isMultiline() && multiLineValue.contains("\n"));
    }

    /**
     * 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
    {
        OutputStream out = appearanceStream.getCOSStream().createUnfilteredStream();
        out.write(data);
        out.flush();
    }

    /**
     * w in an appearance stream represents the lineWidth.
     *
     * @return the linewidth
     */
    private float getLineWidth(List<Object> tokens)
    {
        float retval = 1;
        if (tokens != null)
        {
            int btIndex = tokens.indexOf(Operator.getOperator("BT"));
            int wIndex = tokens.indexOf(Operator.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();
            }
        }
        return retval;
    }

    private PDRectangle getSmallestDrawnRectangle(PDRectangle boundingBox, List<Object> tokens)
    {
        PDRectangle smallest = boundingBox;
        for (int i = 0; i < tokens.size(); i++)
        {
            Object next = tokens.get(i);
            if (next == Operator.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);
                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())
                {
                    smallest = potentialSmallest;
                }
            }
        }
        return smallest;
    }

    /**
     * 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.
     */
    private float calculateFontSize(PDFont pdFont, PDRectangle boundingBox, List<Object> tokens,
            List<Object> daTokens) throws IOException
    {
        float fontSize = 0;
        if (daTokens != null)
        {
            // daString looks like "BMC /Helv 3.4 Tf EMC"
            // use the fontsize of the default existing apperance stream
            int fontIndex = daTokens.indexOf(Operator.getOperator("Tf"));
            if (fontIndex != -1)
            {
                fontSize = ((COSNumber) daTokens.get(fontIndex - 1)).floatValue();
            }
        }

        float widthBasedFontSize = Float.MAX_VALUE;

        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;
            float availableWidth = getAvailableWidth(boundingBox, getLineWidth(tokens));
            widthBasedFontSize = availableWidth / widthAtFontSize1;
        }
        else if (fontSize == 0)
        {
            float lineWidth = getLineWidth(tokens);
            float height = pdFont.getFontDescriptor().getFontBoundingBox().getHeight() / 1000f;
            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 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.
     */
    private float getVerticalOffset(PDRectangle boundingBox, PDFont pdFont, float fontSize,
            List<Object> tokens) throws IOException
    {
        float lineWidth = getLineWidth(tokens);
        float verticalOffset = 0.0f;
        if (parent.isMultiline())
        {
            int rows = (int) (getAvailableHeight(boundingBox, lineWidth) / ((int) fontSize));
            verticalOffset = ((rows) * fontSize) - fontSize;
        }
        else
        {
            // 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 = pdFont.getFontDescriptor();
            float bBoxHeight = boundingBox.getHeight();
            float fontHeight = fd.getFontBoundingBox().getHeight() + 2 * fd.getDescent();
            fontHeight = (fontHeight / 1000) * fontSize;
            verticalOffset = (bBoxHeight - fontHeight) / 2;
        }
        return verticalOffset;
    }

    /**
     * calculates the available width of the box.
     *
     * @return the calculated available width of the box
     */
    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)
    {
        return boundingBox.getHeight() - 2 * lineWidth;
    }
}
TOP

Related Classes of org.apache.pdfbox.pdmodel.interactive.form.PDAppearanceString

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.