# PDTextAppearanceHandler.java

    /*
     * Copyright 2018 The Apache Software Foundation.
     *
     * Licensed 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.awt.geom.GeneralPath;
    import java.awt.geom.PathIterator;
    import java.io.IOException;
    import java.util.HashSet;
    import java.util.Set;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.pdfbox.cos.COSName;
    import org.apache.pdfbox.pdmodel.PDAppearanceContentStream;
    import org.apache.pdfbox.pdmodel.PDDocument;
    import org.apache.pdfbox.pdmodel.common.PDRectangle;
    import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
    import org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName;
    import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
    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.interactive.annotation.PDAnnotationText;
    import org.apache.pdfbox.util.Matrix;

    /**
     *
     * @author Tilman Hausherr
     */
    public class PDTextAppearanceHandler extends PDAbstractAppearanceHandler
    {
        private static final Log LOG = LogFactory.getLog(PDTextAppearanceHandler.class);

        private static final Set<String> SUPPORTED_NAMES = new HashSet<>();

        static
        {
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_NOTE);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_INSERT);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_CROSS);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_HELP);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_CIRCLE);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_PARAGRAPH);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_NEW_PARAGRAPH);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_CHECK);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_STAR);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_RIGHT_ARROW);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_RIGHT_POINTER);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_CROSS_HAIRS);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_UP_ARROW);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_UP_LEFT_ARROW);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_COMMENT);
            SUPPORTED_NAMES.add(PDAnnotationText.NAME_KEY);
        }

        public PDTextAppearanceHandler(PDAnnotation annotation)
        {
            super(annotation);
        }

        public PDTextAppearanceHandler(PDAnnotation annotation, PDDocument document)
        {
            super(annotation, document);
        }

        @Override
        public void generateNormalAppearance()
        {
            PDAnnotationText annotation = (PDAnnotationText) getAnnotation();
            if (!SUPPORTED_NAMES.contains(annotation.getName()))
            {
                return;
            }

            try (PDAppearanceContentStream contentStream = getNormalAppearanceAsContentStream())
            {
                PDColor bgColor = getColor();
                if (bgColor == null)
                {
                    // White is used by Adobe when /C entry is missing
                    contentStream.setNonStrokingColor(1f);
                }
                else
                {
                    contentStream.setNonStrokingColor(bgColor);
                }
                // stroking color is always black which is the PDF default

                setOpacity(contentStream, annotation.getConstantOpacity());

                switch (annotation.getName())
                {
                    case PDAnnotationText.NAME_NOTE:
                        drawNote(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_CROSS:
                        drawCross(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_CIRCLE:
                        drawCircles(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_INSERT:
                        drawInsert(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_HELP:
                        drawHelp(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_PARAGRAPH:
                        drawParagraph(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_NEW_PARAGRAPH:
                        drawNewParagraph(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_STAR:
                        drawStar(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_CHECK:
                        drawCheck(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_RIGHT_ARROW:
                        drawRightArrow(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_RIGHT_POINTER:
                        drawRightPointer(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_CROSS_HAIRS:
                        drawCrossHairs(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_UP_ARROW:
                        drawUpArrow(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_UP_LEFT_ARROW:
                        drawUpLeftArrow(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_COMMENT:
                        drawComment(annotation, contentStream);
                        break;
                    case PDAnnotationText.NAME_KEY:
                        drawKey(annotation, contentStream);
                        break;
                    default:
                        break;
                }
            }
            catch (IOException e)
            {
                LOG.error(e);
            }
        }

        private PDRectangle adjustRectAndBBox(PDAnnotationText annotation, float width, float height)
        {
            // For /Note (other types have different values):
            // Adobe takes the left upper bound as anchor, and adjusts the rectangle to 18 x 20.
            // Observed with files 007071.pdf, 038785.pdf, 038787.pdf,
            // but not with 047745.pdf p133 and 084374.pdf p48, both have the NoZoom flag.
            // there the BBox is also set to fixed values, but the rectangle is left untouched.
            // When no flags are there, Adobe sets /F 24 = NoZoom NoRotate.

            PDRectangle rect = getRectangle();
            PDRectangle bbox;
            if (!annotation.isNoZoom())
            {
                rect.setUpperRightX(rect.getLowerLeftX() + width);
                rect.setLowerLeftY(rect.getUpperRightY() - height);
                annotation.setRectangle(rect);
            }
            if (!annotation.getCOSObject().containsKey(COSName.F))
            {
                // We set these flags because Adobe does so, but PDFBox doesn't support them when rendering.
                annotation.setNoRotate(true);
                annotation.setNoZoom(true);
            }
            bbox = new PDRectangle(width, height);
            annotation.getNormalAppearanceStream().setBBox(bbox);
            return bbox;
        }

        private void drawNote(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 18, 20);
            contentStream.setMiterLimit(4);

            // get round edge the easy way. Adobe uses 4 lines with 4 arcs of radius 0.785 which is bigger.
            contentStream.setLineJoinStyle(1);

            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.61f); // value from Adobe
            contentStream.addRect(1, 1, bbox.getWidth() - 2, bbox.getHeight() - 2);
            contentStream.moveTo(bbox.getWidth() / 4,         bbox.getHeight() / 7 * 2);
            contentStream.lineTo(bbox.getWidth() * 3 / 4 - 1, bbox.getHeight() / 7 * 2);
            contentStream.moveTo(bbox.getWidth() / 4,         bbox.getHeight() / 7 * 3);
            contentStream.lineTo(bbox.getWidth() * 3 / 4 - 1, bbox.getHeight() / 7 * 3);
            contentStream.moveTo(bbox.getWidth() / 4,         bbox.getHeight() / 7 * 4);
            contentStream.lineTo(bbox.getWidth() * 3 / 4 - 1, bbox.getHeight() / 7 * 4);
            contentStream.moveTo(bbox.getWidth() / 4,         bbox.getHeight() / 7 * 5);
            contentStream.lineTo(bbox.getWidth() * 3 / 4 - 1, bbox.getHeight() / 7 * 5);
            contentStream.fillAndStroke();
        }

        private void drawCircles(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 20, 20);

            // strategy used by Adobe:
            // 1) add small circle in white using /ca /CA 0.6 and width 1
            // 2) fill
            // 3) add small circle in one direction
            // 4) add large circle in other direction
            // 5) stroke + fill
            // with square width 20 small r = 6.36, large r = 9.756

            float smallR = 6.36f;
            float largeR = 9.756f;

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.saveGraphicsState();
            contentStream.setLineWidth(1);
            PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
            gs.setAlphaSourceFlag(false);
            gs.setStrokingAlphaConstant(0.6f);
            gs.setNonStrokingAlphaConstant(0.6f);
            gs.setBlendMode(BlendMode.NORMAL);
            contentStream.setGraphicsStateParameters(gs);
            contentStream.setNonStrokingColor(1f);
            drawCircle(contentStream, bbox.getWidth() / 2, bbox.getHeight() / 2, smallR);
            contentStream.fill();
            contentStream.restoreGraphicsState();

            contentStream.setLineWidth(0.59f); // value from Adobe
            drawCircle(contentStream, bbox.getWidth() / 2, bbox.getHeight() / 2, smallR);
            drawCircle2(contentStream, bbox.getWidth() / 2, bbox.getHeight() / 2, largeR);
            contentStream.fillAndStroke();
        }

        private void drawInsert(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 17, 20);

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(0);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe
            contentStream.moveTo(bbox.getWidth() / 2 - 1, bbox.getHeight() - 2);
            contentStream.lineTo(1, 1);
            contentStream.lineTo(bbox.getWidth() - 2, 1);
            contentStream.closeAndFillAndStroke();
        }

        private void drawCross(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 19, 19);

            // should be a square, but who knows...
            float min = Math.min(bbox.getWidth(), bbox.getHeight());

            // small = offset nearest bbox edge
            // large = offset second nearest bbox edge
            float small = min / 10;
            float large = min / 5;

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe

            contentStream.moveTo(small, large);
            contentStream.lineTo(large, small);
            contentStream.lineTo(min / 2, min / 2 - small);
            contentStream.lineTo(min - large, small);
            contentStream.lineTo(min - small, large);
            contentStream.lineTo(min / 2 + small, min / 2);
            contentStream.lineTo(min - small, min - large);
            contentStream.lineTo(min - large, min - small);
            contentStream.lineTo(min / 2, min / 2 + small);
            contentStream.lineTo(large, min - small);
            contentStream.lineTo(small, min - large);
            contentStream.lineTo(min / 2 - small, min / 2);
            contentStream.closeAndFillAndStroke();

            // alternatively, this could also be drawn with Zapf Dingbats "a21"
            // see DrawStar()
        }

        private void drawHelp(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 20, 20);

            float min = Math.min(bbox.getWidth(), bbox.getHeight());

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe

            // Adobe first fills a white circle with CA ca 0.6, so do we
            contentStream.saveGraphicsState();
            contentStream.setLineWidth(1);
            PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
            gs.setAlphaSourceFlag(false);
            gs.setStrokingAlphaConstant(0.6f);
            gs.setNonStrokingAlphaConstant(0.6f);
            gs.setBlendMode(BlendMode.NORMAL);
            contentStream.setGraphicsStateParameters(gs);
            contentStream.setNonStrokingColor(1f);
            drawCircle2(contentStream, min / 2, min / 2, min / 2 - 1);
            contentStream.fill();
            contentStream.restoreGraphicsState();

            contentStream.saveGraphicsState();
            // rescale so that "?" fits into circle and move "?" to circle center
            // values gathered by trial and error
            contentStream.transform(Matrix.getScaleInstance(0.001f * min / 2.25f, 0.001f * min / 2.25f));
            contentStream.transform(Matrix.getTranslateInstance(500, 375));

            // we get the shape of an Helvetica bold "?" and use that one.
            // Adobe uses a different font (which one?), or created the shape from scratch.
            GeneralPath path = Standard14Fonts.getGlyphPath(FontName.HELVETICA_BOLD, "question");
            addPath(contentStream, path);
            contentStream.restoreGraphicsState();
            // draw the outer circle counterclockwise to fill area between circle and "?"
            drawCircle2(contentStream, min / 2, min / 2, min / 2 - 1);
            contentStream.fillAndStroke();
        }

        private void drawParagraph(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 20, 20);

            float min = Math.min(bbox.getWidth(), bbox.getHeight());

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe

            // Adobe first fills a white circle with CA ca 0.6, so do we
            contentStream.saveGraphicsState();
            contentStream.setLineWidth(1);
            PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
            gs.setAlphaSourceFlag(false);
            gs.setStrokingAlphaConstant(0.6f);
            gs.setNonStrokingAlphaConstant(0.6f);
            gs.setBlendMode(BlendMode.NORMAL);
            contentStream.setGraphicsStateParameters(gs);
            contentStream.setNonStrokingColor(1f);
            drawCircle2(contentStream, min / 2, min / 2, min / 2 - 1);
            contentStream.fill();
            contentStream.restoreGraphicsState();

            contentStream.saveGraphicsState();
            // rescale so that "?" fits into circle and move "?" to circle center
            // values gathered by trial and error
            contentStream.transform(Matrix.getScaleInstance(0.001f * min / 3, 0.001f * min / 3));
            contentStream.transform(Matrix.getTranslateInstance(850, 900));

            // we get the shape of an Helvetica "?" and use that one.
            // Adobe uses a different font (which one?), or created the shape from scratch.
            GeneralPath path = Standard14Fonts.getGlyphPath(FontName.HELVETICA, "paragraph");
            addPath(contentStream, path);
            contentStream.restoreGraphicsState();
            contentStream.fillAndStroke();
            drawCircle(contentStream, min / 2, min / 2, min / 2 - 1);
            contentStream.stroke();
        }

        private void drawNewParagraph(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            adjustRectAndBBox(annotation, 13, 20);

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(0);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe

            // small triangle (values from Adobe)
            contentStream.moveTo(6.4995f, 20);
            contentStream.lineTo(0.295f, 7.287f);
            contentStream.lineTo(12.705f, 7.287f);
            contentStream.closeAndFillAndStroke();

            // rescale and translate so that "NP" fits below the triangle
            // values gathered by trial and error
            contentStream.transform(Matrix.getScaleInstance(0.001f * 4, 0.001f * 4));
            contentStream.transform(Matrix.getTranslateInstance(200, 0));
            addPath(contentStream,
                    Standard14Fonts.getGlyphPath(FontName.HELVETICA_BOLD, "N"));
            contentStream.transform(Matrix.getTranslateInstance(1300, 0));
            addPath(contentStream,
                    Standard14Fonts.getGlyphPath(FontName.HELVETICA_BOLD, "P"));
            contentStream.fill();
        }

        private void drawStar(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 20, 19);

            float min = Math.min(bbox.getWidth(), bbox.getHeight());

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe

            contentStream.transform(Matrix.getScaleInstance(0.001f * min / 0.8f, 0.001f * min / 0.8f));

            // we get the shape of a Zapf Dingbats star (0x2605) and use that one.
            // Adobe uses a different font (which one?), or created the shape from scratch.
            GeneralPath path = Standard14Fonts.getGlyphPath(FontName.ZAPF_DINGBATS, "a35");
            addPath(contentStream, path);
            contentStream.fillAndStroke();
        }

        //TODO this is mostly identical to drawStar, except for scale, translation and symbol
        // maybe use a table with all values and draw from there
        // this could also optionally use outer circle
        private void drawCheck(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 20, 19);

            float min = Math.min(bbox.getWidth(), bbox.getHeight());

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe

            contentStream.transform(Matrix.getScaleInstance(0.001f * min / 0.8f, 0.001f * min / 0.8f));
            contentStream.transform(Matrix.getTranslateInstance(0, 50));

            // we get the shape of a Zapf Dingbats check (0x2714) and use that one.
            // Adobe uses a different font (which one?), or created the shape from scratch.
            GeneralPath path = Standard14Fonts.getGlyphPath(FontName.ZAPF_DINGBATS, "a20");
            addPath(contentStream, path);
            contentStream.fillAndStroke();
        }

        //TODO this is mostly identical to drawStar, except for scale, translation and symbol
        private void drawRightPointer(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 20, 17);

            float min = Math.min(bbox.getWidth(), bbox.getHeight());

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe

            contentStream.transform(Matrix.getScaleInstance(0.001f * min / 0.8f, 0.001f * min / 0.8f));
            contentStream.transform(Matrix.getTranslateInstance(0, 50));

            // we get the shape of a Zapf Dingbats right pointer (0x27A4) and use that one.
            // Adobe uses a different font (which one?), or created the shape from scratch.
            GeneralPath path = Standard14Fonts.getGlyphPath(FontName.ZAPF_DINGBATS, "a174");
            addPath(contentStream, path);
            contentStream.fillAndStroke();
        }

        private void drawCrossHairs(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 20, 20);

            float min = Math.min(bbox.getWidth(), bbox.getHeight());

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(0);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.61f); // value from Adobe

            contentStream.transform(Matrix.getScaleInstance(0.001f * min / 1.5f, 0.001f * min / 1.5f));
            contentStream.transform(Matrix.getTranslateInstance(0, 50));

            // we get the shape of a Symbol crosshair (0x2295) and use that one.
            // Adobe uses a different font (which one?), or created the shape from scratch.
            GeneralPath path = Standard14Fonts.getGlyphPath(FontName.SYMBOL, "circleplus");
            addPath(contentStream, path);
            contentStream.fillAndStroke();
        }

        private void drawUpArrow(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                     throws IOException
        {
            adjustRectAndBBox(annotation, 17, 20);

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe

            contentStream.moveTo(1, 7);
            contentStream.lineTo(5, 7);
            contentStream.lineTo(5, 1);
            contentStream.lineTo(12, 1);
            contentStream.lineTo(12, 7);
            contentStream.lineTo(16, 7);
            contentStream.lineTo(8.5f, 19);
            contentStream.closeAndFillAndStroke();
        }

        private void drawUpLeftArrow(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                     throws IOException
        {
            adjustRectAndBBox(annotation, 17, 17);

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe

            contentStream.transform(Matrix.getRotateInstance(Math.toRadians(45), 8, -4));

            contentStream.moveTo(1, 7);
            contentStream.lineTo(5, 7);
            contentStream.lineTo(5, 1);
            contentStream.lineTo(12, 1);
            contentStream.lineTo(12, 7);
            contentStream.lineTo(16, 7);
            contentStream.lineTo(8.5f, 19);
            contentStream.closeAndFillAndStroke();
        }

        private void drawRightArrow(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                throws IOException
        {
            PDRectangle bbox = adjustRectAndBBox(annotation, 20, 20);

            float min = Math.min(bbox.getWidth(), bbox.getHeight());

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(0.59f); // value from Adobe

            // Adobe first fills a white circle with CA ca 0.6, so do we
            contentStream.saveGraphicsState();
            contentStream.setLineWidth(1);
            PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
            gs.setAlphaSourceFlag(false);
            gs.setStrokingAlphaConstant(0.6f);
            gs.setNonStrokingAlphaConstant(0.6f);
            gs.setBlendMode(BlendMode.NORMAL);
            contentStream.setGraphicsStateParameters(gs);
            contentStream.setNonStrokingColor(1f);
            drawCircle2(contentStream, min / 2, min / 2, min / 2 - 1);
            contentStream.fill();
            contentStream.restoreGraphicsState();

            contentStream.saveGraphicsState();
            // rescale so that the glyph fits into circle and move it to circle center
            // values gathered by trial and error
            contentStream.transform(Matrix.getScaleInstance(0.001f * min / 1.3f, 0.001f * min / 1.3f));
            contentStream.transform(Matrix.getTranslateInstance(200, 300));

            // we get the shape of a Zapf Dingbats right arrow (0x2794) and use that one.
            // Adobe uses a different font (which one?), or created the shape from scratch.
            GeneralPath path = Standard14Fonts.getGlyphPath(FontName.ZAPF_DINGBATS, "a160");
            addPath(contentStream, path);
            contentStream.restoreGraphicsState();
            // surprisingly, this one not counterclockwise.
            drawCircle(contentStream, min / 2, min / 2, min / 2 - 1);
            contentStream.fillAndStroke();
        }

        private void drawComment(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                     throws IOException
        {
            adjustRectAndBBox(annotation, 18, 18);

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(200);

            // Adobe first fills a white rectangle with CA ca 0.6, so do we
            contentStream.saveGraphicsState();
            contentStream.setLineWidth(1);
            PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
            gs.setAlphaSourceFlag(false);
            gs.setStrokingAlphaConstant(0.6f);
            gs.setNonStrokingAlphaConstant(0.6f);
            gs.setBlendMode(BlendMode.NORMAL);
            contentStream.setGraphicsStateParameters(gs);
            contentStream.setNonStrokingColor(1f);
            contentStream.addRect(0.3f, 0.3f, 18-0.6f, 18-0.6f);
            contentStream.fill();
            contentStream.restoreGraphicsState();

            contentStream.transform(Matrix.getScaleInstance(0.003f, 0.003f));
            contentStream.transform(Matrix.getTranslateInstance(500, -300));

            // outer shape was gathered from Font Awesome by "printing" comment.svg
            // into a PDF and looking at the content stream
            contentStream.moveTo(2549, 5269);
            contentStream.curveTo(1307, 5269, 300, 4451, 300, 3441);
            contentStream.curveTo(300, 3023, 474, 2640, 764, 2331);
            contentStream.curveTo(633, 1985, 361, 1691, 357, 1688);
            contentStream.curveTo(299, 1626, 283, 1537, 316, 1459);
            contentStream.curveTo(350, 1382, 426, 1332, 510, 1332);
            contentStream.curveTo(1051, 1332, 1477, 1558, 1733, 1739);
            contentStream.curveTo(1987, 1659, 2261, 1613, 2549, 1613);
            contentStream.curveTo(3792, 1613, 4799, 2431, 4799, 3441);
            contentStream.curveTo(4799, 4451, 3792, 5269, 2549, 5269);
            contentStream.closePath();

            // can't use addRect: if we did that, we wouldn't get the donut effect
            contentStream.moveTo(0.3f / 0.003f - 500, 0.3f / 0.003f + 300);
            contentStream.lineTo(0.3f / 0.003f - 500, 0.3f / 0.003f + 300 + 17.4f / 0.003f);
            contentStream.lineTo(0.3f / 0.003f - 500 + 17.4f / 0.003f, 0.3f / 0.003f + 300 + 17.4f / 0.003f);
            contentStream.lineTo(0.3f / 0.003f - 500 + 17.4f / 0.003f, 0.3f / 0.003f + 300);

            contentStream.closeAndFillAndStroke();
        }

        private void drawKey(PDAnnotationText annotation, final PDAppearanceContentStream contentStream)
                     throws IOException
        {
            adjustRectAndBBox(annotation, 13, 18);

            contentStream.setMiterLimit(4);
            contentStream.setLineJoinStyle(1);
            contentStream.setLineCapStyle(0);
            contentStream.setLineWidth(200);

            contentStream.transform(Matrix.getScaleInstance(0.003f, 0.003f));
            contentStream.transform(Matrix.getRotateInstance(Math.toRadians(45), 2500, -800));

            // shape was gathered from Font Awesome by "printing" key.svg into a PDF
            // and looking at the content stream
            contentStream.moveTo(4799, 4004);
            contentStream.curveTo(4799, 3149, 4107, 2457, 3253, 2457);
            contentStream.curveTo(3154, 2457, 3058, 2466, 2964, 2484);
            contentStream.lineTo(2753, 2246);
            contentStream.curveTo(2713, 2201, 2656, 2175, 2595, 2175);
            contentStream.lineTo(2268, 2175);
            contentStream.lineTo(2268, 1824);
            contentStream.curveTo(2268, 1707, 2174, 1613, 2057, 1613);
            contentStream.lineTo(1706, 1613);
            contentStream.lineTo(1706, 1261);
            contentStream.curveTo(1706, 1145, 1611, 1050, 1495, 1050);
            contentStream.lineTo(510, 1050);
            contentStream.curveTo(394, 1050, 300, 1145, 300, 1261);
            contentStream.lineTo(300, 1947);
            contentStream.curveTo(300, 2003, 322, 2057, 361, 2097);
            contentStream.lineTo(1783, 3519);
            contentStream.curveTo(1733, 3671, 1706, 3834, 1706, 4004);
            contentStream.curveTo(1706, 4858, 2398, 5550, 3253, 5550);
            contentStream.curveTo(4109, 5550, 4799, 4860, 4799, 4004);
            contentStream.closePath();
            contentStream.moveTo(3253, 4425);
            contentStream.curveTo(3253, 4192, 3441, 4004, 3674, 4004);
            contentStream.curveTo(3907, 4004, 4096, 4192, 4096, 4425);
            contentStream.curveTo(4096, 4658, 3907, 4847, 3674, 4847);
            contentStream.curveTo(3441, 4847, 3253, 4658, 3253, 4425);
            contentStream.fillAndStroke();
        }

        private void addPath(final PDAppearanceContentStream contentStream, GeneralPath path) throws IOException
        {
            double curX = 0;
            double curY = 0;
            PathIterator it = path.getPathIterator(new AffineTransform());
            double[] coords = new double[6];
            while (!it.isDone())
            {
                int type = it.currentSegment(coords);
                switch (type)
                {
                    case PathIterator.SEG_CLOSE:
                        contentStream.closePath();
                        break;
                    case PathIterator.SEG_CUBICTO:
                        contentStream.curveTo((float) coords[0], (float) coords[1], (float) coords[2],
                                              (float) coords[3], (float) coords[4], (float) coords[5]);
                        curX = coords[4];
                        curY = coords[5];
                        break;
                    case PathIterator.SEG_QUADTO:
                        // Convert quadratic Bézier curve to cubic
                        // https://fontforge.github.io/bezier.html
                        // CP1 = QP0 + 2/3 *(QP1-QP0)
                        // CP2 = QP2 + 2/3 *(QP1-QP2)
                        double cp1x = curX + 2d / 3d * (coords[0] - curX);
                        double cp1y = curY + 2d / 3d * (coords[1] - curY);
                        double cp2x = coords[2] + 2d / 3d * (coords[0] - coords[2]);
                        double cp2y = coords[3] + 2d / 3d * (coords[1] - coords[3]);
                        contentStream.curveTo((float) cp1x, (float) cp1y,
                                              (float) cp2x, (float) cp2y,
                                              (float) coords[2], (float) coords[3]);
                        curX = coords[2];
                        curY = coords[3];
                        break;
                    case PathIterator.SEG_LINETO:
                        contentStream.lineTo((float) coords[0], (float) coords[1]);
                        curX = coords[0];
                        curY = coords[1];
                        break;
                    case PathIterator.SEG_MOVETO:
                        contentStream.moveTo((float) coords[0], (float) coords[1]);
                        curX = coords[0];
                        curY = coords[1];
                        break;
                    default:
                        break;
                }
                it.next();
            }
        }

        @Override
        public void generateRolloverAppearance()
        {
            // No rollover appearance generated
        }

        @Override
        public void generateDownAppearance()
        {
            // No down appearance generated
        }
    }