PDAbstractAppearanceHandler.java

/*
 * 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.annotation.handlers;

import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.apache.pdfbox.cos.COSStream;
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.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.PDAppearanceContentStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationSquareCircle;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;

/**
 * Generic handler to generate the fields appearance.
 * 
 * Individual handler will provide specific implementations for different field
 * types.
 * 
 */
public abstract class PDAbstractAppearanceHandler implements PDAppearanceHandler
{
    private final PDAnnotation annotation;
    private PDFont defaultFont;
    protected final PDDocument document;

    /**
     * Line ending styles where the line has to be drawn shorter (minus line width).
     */
    protected static final Set<String> SHORT_STYLES = createShortStyles();

    static final double ARROW_ANGLE = Math.toRadians(30);

    /**
     * Line ending styles where there is an interior color.
     */
    protected static final Set<String> INTERIOR_COLOR_STYLES = createInteriorColorStyles();
    
    /**
     * Line ending styles where the shape changes its angle, e.g. arrows.
     */
    protected static final Set<String> ANGLED_STYLES = createAngledStyles();

    public PDAbstractAppearanceHandler(PDAnnotation annotation)
    {
        this(annotation, null);
    }

    public PDAbstractAppearanceHandler(PDAnnotation annotation, PDDocument document)
    {
        this.annotation = annotation;
        this.document = document;
    }

    protected PDFont getDefaultFont()
    {
        if (defaultFont == null)
        {
            defaultFont = new PDType1Font(FontName.HELVETICA);
        }
        return defaultFont;
    }
    
    PDAnnotation getAnnotation()
    {
        return annotation;
    }

    PDColor getColor()
    {
        return annotation.getColor();
    }

    PDRectangle getRectangle()
    {
        return annotation.getRectangle();
    }
    
    protected COSStream createCOSStream()
    {
        return document == null ? new COSStream() : document.getDocument().createCOSStream();
    }

    /**
     * Get the annotations appearance dictionary.
     * 
     * <p>
     * This will get the annotations appearance dictionary. If this is not
     * existent an empty appearance dictionary will be created.
     * 
     * @return the annotations appearance dictionary
     */
    PDAppearanceDictionary getAppearance()
    {
        PDAppearanceDictionary appearanceDictionary = annotation.getAppearance();
        if (appearanceDictionary == null)
        {
            appearanceDictionary = new PDAppearanceDictionary();
            annotation.setAppearance(appearanceDictionary);
        }
        return appearanceDictionary;
    }

    /**
     * Get the annotations normal appearance content stream.
     * 
     * <p>
     * This will get the annotations normal appearance content stream, to 'draw' to. It will be
     * uncompressed.
     *
     * @return the appearance entry representing the normal appearance.
     * @throws IOException 
     */
    PDAppearanceContentStream getNormalAppearanceAsContentStream() throws IOException
    {
        return getNormalAppearanceAsContentStream(false);
    }
    
    /**
     * Get the annotations normal appearance content stream.
     * 
     * <p>
     * This will get the annotations normal appearance content stream, to 'draw' to.
     * 
     * @param compress whether the content stream is to be compressed. Set this to true when
     * creating long content streams.
     * @return the appearance entry representing the normal appearance.
     * @throws IOException
     */
    PDAppearanceContentStream getNormalAppearanceAsContentStream(boolean compress) throws IOException
    {
        PDAppearanceEntry appearanceEntry = getNormalAppearance();
        return getAppearanceEntryAsContentStream(appearanceEntry, compress);
    }
    
    /**
     * Get the annotations down appearance.
     * 
     * <p>
     * This will get the annotations down appearance. If this is not existent an
     * empty appearance entry will be created.
     * 
     * @return the appearance entry representing the down appearance.
     */
    PDAppearanceEntry getDownAppearance()
    {
        PDAppearanceDictionary appearanceDictionary = getAppearance();
        PDAppearanceEntry downAppearanceEntry = appearanceDictionary.getDownAppearance();

        if (downAppearanceEntry.isSubDictionary())
        {
            downAppearanceEntry = new PDAppearanceEntry(createCOSStream());
            appearanceDictionary.setDownAppearance(downAppearanceEntry);
        }

        return downAppearanceEntry;
    }

    /**
     * Get the annotations rollover appearance.
     * 
     * <p>
     * This will get the annotations rollover appearance. If this is not
     * existent an empty appearance entry will be created.
     * 
     * @return the appearance entry representing the rollover appearance.
     */
    PDAppearanceEntry getRolloverAppearance()
    {
        PDAppearanceDictionary appearanceDictionary = getAppearance();
        PDAppearanceEntry rolloverAppearanceEntry = appearanceDictionary.getRolloverAppearance();

        if (rolloverAppearanceEntry.isSubDictionary())
        {
            rolloverAppearanceEntry = new PDAppearanceEntry(createCOSStream());
            appearanceDictionary.setRolloverAppearance(rolloverAppearanceEntry);
        }

        return rolloverAppearanceEntry;
    }

    /**
     * Get a padded rectangle.
     * 
     * <p>Creates a new rectangle with padding applied to each side.
     * .
     * @param rectangle the rectangle.
     * @param padding the padding to apply.
     * @return the padded rectangle.
     */
    PDRectangle getPaddedRectangle(PDRectangle rectangle, float padding)
    {
        return new PDRectangle(rectangle.getLowerLeftX() + padding, rectangle.getLowerLeftY() + padding,
                rectangle.getWidth() - 2 * padding, rectangle.getHeight() - 2 * padding);
    }
    
    /**
     * Get a rectangle enlarged by the differences.
     *
     * <p>
     * Creates a new rectangle with differences added to each side. If there are no valid
     * differences, then the original rectangle is returned.
     *
     * @param rectangle the rectangle.
     * @param differences the differences to apply.
     * @return the padded rectangle.
     */
    PDRectangle addRectDifferences(PDRectangle rectangle, float[] differences)
    {
        if (differences == null || differences.length != 4)
        {
            return rectangle;
        }
        
        return new PDRectangle(rectangle.getLowerLeftX() - differences[0],
                rectangle.getLowerLeftY() - differences[1],
                rectangle.getWidth() + differences[0] + differences[2],
                rectangle.getHeight() + differences[1] + differences[3]);
    }
    
    /**
     * Get a rectangle with the differences applied to each side.
     *
     * <p>
     * Creates a new rectangle with differences added to each side. If there are no valid
     * differences, then the original rectangle is returned.
     *
     * @param rectangle the rectangle.
     * @param differences the differences to apply.
     * @return the padded rectangle.
     */
    PDRectangle applyRectDifferences(PDRectangle rectangle, float[] differences)
    {
        if (differences == null || differences.length != 4)
        {
            return rectangle;
        }
        return new PDRectangle(rectangle.getLowerLeftX() + differences[0],
                rectangle.getLowerLeftY() + differences[1],
                rectangle.getWidth() - differences[0] - differences[2],
                rectangle.getHeight() - differences[1] - differences[3]);
    }

    void setOpacity(PDAppearanceContentStream contentStream, float opacity) throws IOException
    {
        if (opacity < 1)
        {
            PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
            gs.setStrokingAlphaConstant(opacity);
            gs.setNonStrokingAlphaConstant(opacity);

            contentStream.setGraphicsStateParameters(gs);
        }
    }

    /**
     * Draw a line ending style.
     * 
     * @param style
     * @param cs
     * @param x
     * @param y
     * @param width
     * @param hasStroke
     * @param hasBackground
     * @param ending false if left, true if right of an imagined horizontal line (important for
     * arrows).
     *
     * @throws IOException
     */
    void drawStyle(String style, final PDAppearanceContentStream cs, float x, float y,
                   float width, boolean hasStroke, boolean hasBackground, boolean ending) throws IOException
    {
        int sign = ending ? -1 : 1;
        switch (style)
        {
            case PDAnnotationLine.LE_OPEN_ARROW:
            case PDAnnotationLine.LE_CLOSED_ARROW:
                drawArrow(cs, x + sign * width, y, sign * width * 9);
                break;
            case PDAnnotationLine.LE_BUTT:
                cs.moveTo(x, y - width * 3);
                cs.lineTo(x, y + width * 3);
                break;
            case PDAnnotationLine.LE_DIAMOND:
                drawDiamond(cs, x, y, width * 3);
                break;
            case PDAnnotationLine.LE_SQUARE:
                cs.addRect(x - width * 3, y - width * 3, width * 6, width * 6);
                break;
            case PDAnnotationLine.LE_CIRCLE:
                drawCircle(cs, x, y, width * 3);
                break;
            case PDAnnotationLine.LE_R_OPEN_ARROW:
            case PDAnnotationLine.LE_R_CLOSED_ARROW:
                drawArrow(cs, x + (-sign) * width, y, (-sign) * width * 9);
                break;
            case PDAnnotationLine.LE_SLASH:
                float width9 = width * 9;
                // the line is 18 x linewidth at an angle of 60°
                cs.moveTo(x + (float) (Math.cos(Math.toRadians(60)) * width9),
                          y + (float) (Math.sin(Math.toRadians(60)) * width9));
                cs.lineTo(x + (float) (Math.cos(Math.toRadians(240)) * width9),
                          y + (float) (Math.sin(Math.toRadians(240)) * width9));
                break;
            default:
                return;
        }
        if (PDAnnotationLine.LE_R_CLOSED_ARROW.equals(style) || 
            PDAnnotationLine.LE_CLOSED_ARROW.equals(style))
        {
            cs.closePath();
        }
        cs.drawShape(width, hasStroke, 
                     // make sure to only paint a background color (/IC value) 
                     // for interior color styles, even if an /IC value is set.
                     INTERIOR_COLOR_STYLES.contains(style) && hasBackground);
    }

    /**
     * Add the two arms of a horizontal arrow.
     * 
     * @param cs Content stream
     * @param x
     * @param y
     * @param len The arm length. Positive goes to the right, negative goes to the left.
     * 
     * @throws IOException If the content stream could not be written
     */
    void drawArrow(PDAppearanceContentStream cs, float x, float y, float len) throws IOException
    {
        // strategy for arrows: angle 30°, arrow arm length = 9 * line width
        // cos(angle) = x position
        // sin(angle) = y position
        // this comes very close to what Adobe is doing
        float armX = x + (float) (Math.cos(ARROW_ANGLE) * len);
        float armYdelta = (float) (Math.sin(ARROW_ANGLE) * len);
        cs.moveTo(armX, y + armYdelta);
        cs.lineTo(x, y);
        cs.lineTo(armX, y - armYdelta);
    }

    /**
     * Add a square diamond shape (corner on top) to the path.
     *
     * @param cs Content stream
     * @param x
     * @param y
     * @param r Radius (to a corner)
     * 
     * @throws IOException If the content stream could not be written
     */
    void drawDiamond(PDAppearanceContentStream cs, float x, float y, float r) throws IOException
    {
        cs.moveTo(x - r, y);
        cs.lineTo(x, y + r);
        cs.lineTo(x + r, y);
        cs.lineTo(x, y - r);
        cs.closePath();
    }

    /**
     * Add a circle shape to the path in clockwise direction.
     *
     * @param cs Content stream
     * @param x
     * @param y
     * @param r Radius
     * 
     * @throws IOException If the content stream could not be written.
     */
    void drawCircle(PDAppearanceContentStream cs, float x, float y, float r) throws IOException
    {
        // http://stackoverflow.com/a/2007782/535646
        float magic = r * 0.551784f;
        cs.moveTo(x, y + r);
        cs.curveTo(x + magic, y + r, x + r, y + magic, x + r, y);
        cs.curveTo(x + r, y - magic, x + magic, y - r, x, y - r);
        cs.curveTo(x - magic, y - r, x - r, y - magic, x - r, y);
        cs.curveTo(x - r, y + magic, x - magic, y + r, x, y + r);
        cs.closePath();
    }

    /**
     * Add a circle shape to the path in counterclockwise direction. You'll need this e.g. when
     * drawing a doughnut shape. See "Nonzero Winding Number Rule" for more information.
     *
     * @param cs Content stream
     * @param x
     * @param y
     * @param r Radius
     *
     * @throws IOException If the content stream could not be written.
     */
    void drawCircle2(PDAppearanceContentStream cs, float x, float y, float r) throws IOException
    {
        // http://stackoverflow.com/a/2007782/535646
        float magic = r * 0.551784f;
        cs.moveTo(x, y + r);
        cs.curveTo(x - magic, y + r, x - r, y + magic, x - r, y);
        cs.curveTo(x - r, y - magic, x - magic, y - r, x, y - r);
        cs.curveTo(x + magic, y - r, x + r, y - magic, x + r, y);
        cs.curveTo(x + r, y + magic, x + magic, y + r, x, y + r);
        cs.closePath();
    }

    private static Set<String> createShortStyles()
    {
        Set<String> shortStyles = new HashSet<>();
        shortStyles.add(PDAnnotationLine.LE_OPEN_ARROW);
        shortStyles.add(PDAnnotationLine.LE_CLOSED_ARROW);
        shortStyles.add(PDAnnotationLine.LE_SQUARE);
        shortStyles.add(PDAnnotationLine.LE_CIRCLE);
        shortStyles.add(PDAnnotationLine.LE_DIAMOND);
        return Collections.unmodifiableSet(shortStyles);
    }

    private static Set<String> createInteriorColorStyles()
    {
        Set<String> interiorColorStyles = new HashSet<>();
        interiorColorStyles.add(PDAnnotationLine.LE_CLOSED_ARROW);
        interiorColorStyles.add(PDAnnotationLine.LE_CIRCLE);
        interiorColorStyles.add(PDAnnotationLine.LE_DIAMOND);
        interiorColorStyles.add(PDAnnotationLine.LE_R_CLOSED_ARROW);
        interiorColorStyles.add(PDAnnotationLine.LE_SQUARE);
        return Collections.unmodifiableSet(interiorColorStyles);
    }

    private static Set<String> createAngledStyles()
    {
        Set<String> angledStyles = new HashSet<>();
        angledStyles.add(PDAnnotationLine.LE_CLOSED_ARROW);
        angledStyles.add(PDAnnotationLine.LE_OPEN_ARROW);
        angledStyles.add(PDAnnotationLine.LE_R_CLOSED_ARROW);
        angledStyles.add(PDAnnotationLine.LE_R_OPEN_ARROW);
        angledStyles.add(PDAnnotationLine.LE_BUTT);
        angledStyles.add(PDAnnotationLine.LE_SLASH);
        return Collections.unmodifiableSet(angledStyles);
    }

    /**
     * Get the annotations normal appearance.
     * 
     * <p>
     * This will get the annotations normal appearance. If this is not existent
     * an empty appearance entry will be created.
     * 
     * @return the appearance entry representing the normal appearance.
     */
    private PDAppearanceEntry getNormalAppearance()
    {
        PDAppearanceDictionary appearanceDictionary = getAppearance();
        PDAppearanceEntry normalAppearanceEntry = appearanceDictionary.getNormalAppearance();

        if (normalAppearanceEntry == null || normalAppearanceEntry.isSubDictionary())
        {
            normalAppearanceEntry = new PDAppearanceEntry(createCOSStream());
            appearanceDictionary.setNormalAppearance(normalAppearanceEntry);
        }

        return normalAppearanceEntry;
    }
    
    
    private PDAppearanceContentStream getAppearanceEntryAsContentStream(
              PDAppearanceEntry appearanceEntry, boolean compress) throws IOException
    {
        PDAppearanceStream appearanceStream = appearanceEntry.getAppearanceStream();
        setTransformationMatrix(appearanceStream);

        // ensure there are resources
        PDResources resources = appearanceStream.getResources();
        if (resources == null)
        {
            resources = new PDResources();
            appearanceStream.setResources(resources);
        }

        return new PDAppearanceContentStream(appearanceStream, compress);
    }
    
    private void setTransformationMatrix(PDAppearanceStream appearanceStream)
    {
        PDRectangle bbox = getRectangle();
        appearanceStream.setBBox(bbox);
        AffineTransform transform = AffineTransform.getTranslateInstance(-bbox.getLowerLeftX(),
                -bbox.getLowerLeftY());
        appearanceStream.setMatrix(transform);
    }

    PDRectangle handleBorderBox(PDAnnotationSquareCircle annotation, float lineWidth)
    {
        // There are two options. The handling is not part of the PDF specification but
        // implementation specific to Adobe Reader
        // - if /RD is set the border box is the /Rect entry inset by the respective
        //   border difference.
        // - if /RD is not set the border box is defined by the /Rect entry. The /RD entry will
        //   be set to be the line width and the /Rect is enlarged by the /RD amount
        PDRectangle borderBox;
        float[] rectDifferences = annotation.getRectDifferences();
        if (rectDifferences.length == 0)
        {
            borderBox = getPaddedRectangle(getRectangle(), lineWidth / 2);
            // the differences rectangle
            annotation.setRectDifferences(lineWidth / 2);
            annotation.setRectangle(addRectDifferences(getRectangle(), annotation.getRectDifferences()));
            // when the normal appearance stream was generated BBox and Matrix have been set to the
            // values of the original /Rect. As the /Rect was changed that needs to be adjusted too.
            PDRectangle rect = getRectangle();
            PDAppearanceStream appearanceStream = annotation.getNormalAppearanceStream();
            AffineTransform transform =
                    AffineTransform.getTranslateInstance(-rect.getLowerLeftX(), -rect.getLowerLeftY());
            appearanceStream.setBBox(rect);
            appearanceStream.setMatrix(transform);
        }
        else
        {
            borderBox = applyRectDifferences(getRectangle(), rectDifferences);
            borderBox = getPaddedRectangle(borderBox, lineWidth / 2);
        }
        return borderBox;
    }
}