All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.lowagie.text.pdf.PdfGraphics2D Maven / Gradle / Ivy

There is a newer version: 2.0.3
Show newest version
/*
 * $Id: PdfGraphics2D.java 3611 2008-11-05 19:45:31Z blowagie $
 *
 * Copyright 2002 by Jim Moore .
 *
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the License.
 *
 * The Original Code is 'iText, a free JAVA-PDF library'.
 *
 * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by
 * the Initial Developer are Copyright (C) 1999, 2000, 2001, 2002 by Bruno Lowagie.
 * All Rights Reserved.
 * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer
 * are Copyright (C) 2000, 2001, 2002 by Paulo Soares. All Rights Reserved.
 *
 * Contributor(s): all the names of the contributors are added in the source code
 * where applicable.
 *
 * Alternatively, the contents of this file may be used under the terms of the
 * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the
 * provisions of LGPL are applicable instead of those above.  If you wish to
 * allow use of your version of this file only under the terms of the LGPL
 * License and not to allow others to use your version of this file under
 * the MPL, indicate your decision by deleting the provisions above and
 * replace them with the notice and other provisions required by the LGPL.
 * If you do not delete the provisions above, a recipient may use your version
 * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the MPL as stated above or under the terms of the GNU
 * Library General Public License as published by the Free Software Foundation;
 * either version 2 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more
 * details.
 *
 * If you didn't download this code from the following link, you should check if
 * you aren't using an obsolete version:
 * https://github.com/LibrePDF/OpenPDF
 */

package com.lowagie.text.pdf;

import com.lowagie.text.pdf.internal.PolylineShape;
import com.lowagie.text.utils.SystemPropertyUtil;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Paint;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.RenderingHints.Key;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.TexturePaint;
import java.awt.Transparency;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextAttribute;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.ImageObserver;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.awt.image.renderable.RenderableImage;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.AttributedCharacterIterator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.ImageOutputStream;

public class PdfGraphics2D extends Graphics2D {
    
    private static final int FILL = 1;
    private static final int STROKE = 2;
    private static final int CLIP = 3;
    private BasicStroke strokeOne = new BasicStroke(1);
    
    private static final AffineTransform IDENTITY = new AffineTransform();
    
    private static final Set	 LOGICAL_FONT_NAMES                = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("Dialog", "DialogInput", "Monospaced", "Serif", "SansSerif")));
    private static final String			 BOLD_FONT_FACE_NAME_SUFFIX        = ".bold";
    private static final String			 BOLD_ITALIC_FONT_FACE_NAME_SUFFIX = ".bolditalic";
    
    private Font font;
    private BaseFont baseFont;
    private float fontSize;
    private AffineTransform transform;
    private Paint paint;
    private Color background;
    private float width;
    private float height;
    
    private Area clip;
    
    private RenderingHints rhints = new RenderingHints(null);
    
    private Stroke stroke;
    private Stroke originalStroke;
    
    private PdfContentByte cb;
    
    /** Storage for BaseFont objects created. */
    private Map baseFonts;
    
    private boolean disposeCalled = false;
    
    private FontMapper fontMapper;
    
    private List kids;
    
    private boolean kid = false;
    
    private Graphics2D dg2 = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB).createGraphics();
    
    private boolean onlyShapes = false;
    
    private Stroke oldStroke;
    private Paint paintFill;
    private Paint paintStroke;
    
    private MediaTracker mediaTracker;

    // Added by Jurij Bilas
    protected boolean underline;          // indicates if the font style is underlined

    protected PdfGState[] fillGState = new PdfGState[256];
    protected PdfGState[] strokeGState = new PdfGState[256];
    protected int currentFillGState = 255;
    protected int currentStrokeGState = 255;
    
    public static final int AFM_DIVISOR = 1000; // used to calculate coordinates

    private boolean convertImagesToJPEG = false;
    private float jpegQuality = .95f;

    // Added by Alexej Suchov
    private float alpha;

    // Added by Alexej Suchov
    private Composite composite;

    // Added by Alexej Suchov
    private Paint realPaint;
    
    private final CompositeFontDrawer compositeFontDrawer = new CompositeFontDrawer();

    // make use of compositeFontDrawer configurable ... may be set via property or directly via setter
    private boolean isCompositeFontDrawerEnabled = SystemPropertyUtil.getBoolean("com.github.librepdf.openpdf.compositeFontDrawerEnabled", true);
    
    private PdfGraphics2D() {
        dg2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        setRenderingHint(HyperLinkKey.KEY_INSTANCE, HyperLinkKey.VALUE_HYPERLINKKEY_OFF);
    }

    /**
     * Shortcut constructor for PDFGraphics2D.
     * @param cb the PdfContentByte
     * @param width the width
     * @param height the height
     */
    public PdfGraphics2D(PdfContentByte cb, float width, float height) {
        this(cb, width, height, null, false, false, 0);
    }

    /**
     * Constructor for PDFGraphics2D.
     * @param cb                    The PdfContentByte
     * @param width                 The width
     * @param height                The height
     * @param fontMapper            The fonts to put in the BaseFont Map
     * @param onlyShapes            True if there are only shapes, false otherwise
     * @param convertImagesToJPEG   True to convert to JPEG, false otherwise
     * @param quality               The JPEG quality
     */
    public PdfGraphics2D(PdfContentByte cb, float width, float height, FontMapper fontMapper, boolean onlyShapes, boolean convertImagesToJPEG, float quality) {
        super();
        dg2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        setRenderingHint(HyperLinkKey.KEY_INSTANCE, HyperLinkKey.VALUE_HYPERLINKKEY_OFF);
        this.convertImagesToJPEG = convertImagesToJPEG;
        this.jpegQuality = quality;
        this.onlyShapes = onlyShapes;
        this.transform = new AffineTransform();
        this.baseFonts = new HashMap<>();
        if (!onlyShapes) {
            this.fontMapper = fontMapper;
            if (this.fontMapper == null)
                this.fontMapper = new DefaultFontMapper();
        }
        paint = Color.black;
        background = Color.white;
        setFont(new Font("sanserif", Font.PLAIN, 12));
        this.cb = cb;
        cb.saveState();
        this.width = width;
        this.height = height;
        clip = new Area(new Rectangle2D.Float(0, 0, width, height));
        clip(clip);
        originalStroke = stroke = oldStroke = strokeOne;
        setStrokeDiff(stroke, null);
        cb.saveState();
    }
    
    /**
     * @see Graphics2D#draw(Shape)
     */
    public void draw(Shape s) {
        followPath(s, STROKE);
    }
    
    /**
     * @see Graphics2D#drawImage(Image, AffineTransform, ImageObserver)
     */
    public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) {
        return drawImage(img, null, xform, null, obs);
    }
    
    /**
     * @see Graphics2D#drawImage(BufferedImage, BufferedImageOp, int, int)
     */
    public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
        BufferedImage result = img;
        if (op != null) {
            result = op.createCompatibleDestImage(img, img.getColorModel());
            result = op.filter(img, result);
        }
        drawImage(result, x, y, null);
    }
    
    /**
     * @see Graphics2D#drawRenderedImage(RenderedImage, AffineTransform)
     */
    public void drawRenderedImage(RenderedImage img, AffineTransform xform) {
        BufferedImage image = null;
        if (img instanceof BufferedImage) {
            image = (BufferedImage)img;
        } else {
            ColorModel cm = img.getColorModel();
            int width = img.getWidth();
            int height = img.getHeight();
            WritableRaster raster = cm.createCompatibleWritableRaster(width, height);
            boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
            Hashtable properties = new Hashtable<>();
            String[] keys = img.getPropertyNames();
            if (keys!=null) {
                for (String key : keys) {
                    properties.put(key, img.getProperty(key));
                }
            }
            BufferedImage result = new BufferedImage(cm, raster, isAlphaPremultiplied, properties);
            img.copyData(raster);
            image=result;
        }
        drawImage(image, xform, null);
    }
    
    /**
     * @see Graphics2D#drawRenderableImage(RenderableImage, AffineTransform)
     */
    public void drawRenderableImage(RenderableImage img, AffineTransform xform) {
        drawRenderedImage(img.createDefaultRendering(), xform);
    }
    
    /**
     * @see Graphics#drawString(String, int, int)
     */
    public void drawString(String s, int x, int y) {
        drawString(s, (float)x, (float)y);
    }
    
    /**
     * Calculates position and/or stroke thickness depending on the font size
     * @param d value to be converted
     * @param i font size
     * @return position and/or stroke thickness depending on the font size
     */
    public static double asPoints(double d, int i) {
        return d * i / AFM_DIVISOR;
    }
    /**
     * This routine goes through the attributes and sets the font
     * before calling the actual string drawing routine
     * @param iter  the AttributedCharacterIterator
     */
    @SuppressWarnings("unchecked")
    protected void doAttributes(AttributedCharacterIterator iter) {
        underline = false;
        Set set = iter.getAttributes().keySet();
        for (AttributedCharacterIterator.Attribute attribute : set) {
            if (!(attribute instanceof TextAttribute))
                continue;
            TextAttribute textattribute = (TextAttribute) attribute;
            if (textattribute.equals(TextAttribute.FONT)) {
                Font font = (Font) iter.getAttributes().get(textattribute);
                setFont(font);
            } else if (textattribute.equals(TextAttribute.UNDERLINE)) {
                if (iter.getAttributes().get(textattribute) == TextAttribute.UNDERLINE_ON)
                    underline = true;
            } else if (textattribute.equals(TextAttribute.SIZE)) {
                Object obj = iter.getAttributes().get(textattribute);
                if (obj instanceof Integer) {
                    int i = (Integer) obj;
                    setFont(getFont().deriveFont(getFont().getStyle(), i));
                } else if (obj instanceof Float) {
                    float f = (Float) obj;
                    setFont(getFont().deriveFont(getFont().getStyle(), f));
                }
            } else if (textattribute.equals(TextAttribute.FOREGROUND)) {
                setColor((Color) iter.getAttributes().get(textattribute));
            } else if (textattribute.equals(TextAttribute.FAMILY)) {
                Font font = getFont();
                Map fontAttributes = (Map)font.getAttributes();
                fontAttributes.put(TextAttribute.FAMILY, iter.getAttributes().get(textattribute));
                setFont(font.deriveFont(fontAttributes));
            } else if (textattribute.equals(TextAttribute.POSTURE)) {
                Font font = getFont();
                Map fontAttributes = (Map)font.getAttributes();
                fontAttributes.put(TextAttribute.POSTURE, iter.getAttributes().get(textattribute));
                setFont(font.deriveFont(fontAttributes));
            } else if (textattribute.equals(TextAttribute.WEIGHT)) {
                Font font = getFont();
                Map fontAttributes = (Map)font.getAttributes();
                fontAttributes.put(TextAttribute.WEIGHT, iter.getAttributes().get(textattribute));
                setFont(font.deriveFont(fontAttributes));
            }
        }
    }

    /**
     * @see Graphics2D#drawString(String, float, float)
     */
    public void drawString(String s, float x, float y) {
        if (s.length() == 0) {
            return;
        }
        setFillPaint();
        if (onlyShapes) {
            drawGlyphVector(this.font.layoutGlyphVector(getFontRenderContext(), s.toCharArray(), 0, s.length(), java.awt.Font.LAYOUT_LEFT_TO_RIGHT), x, y);
//            Use the following line to compile in JDK 1.3    
//            drawGlyphVector(this.font.createGlyphVector(getFontRenderContext(), s), x, y);
        } else {
            if (!Float.isFinite(fontSize) || fontSize < PdfContentByte.MIN_FONT_SIZE) {
                return;
            }
            double width = 0;
            if (isCompositeFontDrawerEnabled && CompositeFontDrawer.isSupported() && compositeFontDrawer.isCompositeFont(font)) {
                width = compositeFontDrawer.drawString(s, font, x, y, this::getCachedBaseFont, this::drawString);
            } else {
                // Splitting string to the parts depending on they visibility preserves
                // the position of the visible parts of string and prevents alignment of the text in width
                // (increasing value of the character spacing parameter of the PdfContentByte)
                List substrings = splitIntoSubstringsByVisibility(s);
                for (String str : substrings) {
                    width += drawString(str, baseFont, x + width, y);
                }
            }
            if(underline) {
                // These two are supposed to be taken from the .AFM file
                //int UnderlinePosition = -100;
                int UnderlineThickness = 50;
                //
                double d = asPoints(UnderlineThickness, (int)fontSize);
                Stroke savedStroke = originalStroke;
                setStroke(new BasicStroke((float)d));
                y = (float)(y + asPoints(UnderlineThickness, (int)fontSize));
                Line2D line = new Line2D.Double(x, y, width+x, y);
                draw(line);
                setStroke(savedStroke);
            }
        }
    }
    
    private double drawString(String s, BaseFont baseFont, double x, double y) {
        boolean restoreTextRenderingMode = false;
        AffineTransform at = getTransform();
        try {
            AffineTransform at2 = getTransform();
            at2.translate(x, y);
            at2.concatenate(font.getTransform());
            setTransform(at2);
            AffineTransform inverse = this.normalizeMatrix();
            AffineTransform flipper = AffineTransform.getScaleInstance(1,-1);
            inverse.concatenate(flipper);
            double[] mx = new double[6];
            inverse.getMatrix(mx);
            cb.beginText();
            cb.setFontAndSize(baseFont, fontSize);
            // Check if we need to simulate an italic font.
            if (font.isItalic()) {
                float angle = baseFont.getFontDescriptor(BaseFont.ITALICANGLE, 1000);
                float angle2 = font.getItalicAngle();
                // When there are different fonts for italic, bold, italic bold
                // the font.getName() will be different from the font.getFontName()
                // value. When they are the same value then we are normally dealing
                // with a single font that has been made into an italic or bold
                // font. When there are only a plain and a bold font available,
                // we need to enter this logic too.
                if (Objects.equals(font.getFontName(), font.getName()) || (angle == 0f && angle2 == 0f)) {
                    // We don't have an italic version of this font so we need
                    // to set the font angle ourselves to produce an italic font.
                    if (angle2 == 0) {
                        // The JavaVM didn't have an angle setting for making
                        // the font an italic font so use a default of
                        // italic angle of 15 degrees.
                        angle2 = 15.0f;
                    } else {
                        // This sign of the angle for Java and PDF seams
                        // seams to be reversed.
                        angle2 = -angle2;
                    }
                    if (angle == 0) {
                         mx[2] = angle2 / 100.0f;
                    }
                }
           }
           cb.setTextMatrix((float)mx[0], (float)mx[1], (float)mx[2], (float)mx[3], (float)mx[4], (float)mx[5]);
           Float fontTextAttributeWidth = (Float)font.getAttributes().get(TextAttribute.WIDTH);
           fontTextAttributeWidth = (fontTextAttributeWidth == null)
                                    ? TextAttribute.WIDTH_REGULAR
                                    : fontTextAttributeWidth;

            if (!TextAttribute.WIDTH_REGULAR.equals(fontTextAttributeWidth)) {
                cb.setHorizontalScaling(100.0f / fontTextAttributeWidth);
            }

            // Check if we need to simulate a bold font.
            // Do nothing if the BaseFont is already bold. This test is not foolproof but it will work most of the times.
            if (!baseFont.getPostscriptFontName().toLowerCase(Locale.ROOT).contains("bold")) {
                // Get the weight of the font so we can detect fonts with a weight
                // that makes them bold, but the Font.isBold() value is false.
                Float weight = (Float) font.getAttributes().get(TextAttribute.WEIGHT);
                if (weight == null) {
                    weight = (font.isBold()) ? TextAttribute.WEIGHT_BOLD
                                             : TextAttribute.WEIGHT_REGULAR;
                }
                String fontFaceName = font.getFontName();
                String fontLogicalName = font.getName();
                if ((font.isBold() || (weight >= TextAttribute.WEIGHT_SEMIBOLD))
                    && (fontFaceName.equals(fontLogicalName) ||
                    		// bold logical font face name has suffix ".bold" / ".bolditalic"
                    		(LOGICAL_FONT_NAMES.contains(fontLogicalName) &&
                    				(fontFaceName.equals(fontLogicalName + BOLD_FONT_FACE_NAME_SUFFIX) ||
                    				 fontFaceName.equals(fontLogicalName + BOLD_ITALIC_FONT_FACE_NAME_SUFFIX))))) {
                    // Simulate a bold font.
                    float strokeWidth = font.getSize2D() * (weight - TextAttribute.WEIGHT_REGULAR) / 30f;
                    if (strokeWidth != 1) {
                        if(realPaint instanceof Color){
                            cb.setTextRenderingMode(PdfContentByte.TEXT_RENDER_MODE_FILL_STROKE);
                            oldStroke = new BasicStroke(strokeWidth);
                            cb.setLineWidth(strokeWidth);
                            Color color = (Color)realPaint;
                            int alpha = color.getAlpha();
                            if (alpha != currentStrokeGState) {
                                currentStrokeGState = alpha;
                                PdfGState gs = strokeGState[alpha];
                                if (gs == null) {
                                    gs = new PdfGState();
                                    gs.setStrokeOpacity(alpha / 255f);
                                    strokeGState[alpha] = gs;
                                }
                                cb.setGState(gs);
                            }
                            setStrokePaint();
                            restoreTextRenderingMode = true;
                        }
                    }
                }
            }

            double width = 0;
            if (font.getSize2D() > 0) {
                float scale = 1000 / font.getSize2D();
                Font derivedFont = font.deriveFont(AffineTransform.getScaleInstance(scale, scale));
                width = derivedFont.getStringBounds(s, getFontRenderContext()).getWidth();
                if (derivedFont.isTransformed()) {
                    width /= scale;
                }
            }
            // if the hyperlink flag is set add an action to the text
            Object url = getRenderingHint(HyperLinkKey.KEY_INSTANCE);
            if (url != null && !url.equals(HyperLinkKey.VALUE_HYPERLINKKEY_OFF)) {
                float scale = 1000 / font.getSize2D();
                Font derivedFont = font.deriveFont(AffineTransform.getScaleInstance(scale, scale));
                double height = derivedFont.getStringBounds(s, getFontRenderContext()).getHeight();
                if (derivedFont.isTransformed()) {
                    height /= scale;
                }
                double leftX = cb.getXTLM();
                double leftY = cb.getYTLM();
                PdfAction action = new  PdfAction(url.toString());
                cb.setAction(action, (float)leftX, (float)leftY, (float)(leftX+width), (float)(leftY+height));
            }
            if (s.length() > 1) {
                float adv = ((float)width - baseFont.getWidthPoint(s, fontSize)) / (s.length() - 1);
                cb.setCharacterSpacing(adv);
            }
            cb.showText(s);
            if (s.length() > 1) {
                cb.setCharacterSpacing(0);
            }
            if (!TextAttribute.WIDTH_REGULAR.equals(fontTextAttributeWidth)) {
                cb.setHorizontalScaling(100);
            }

            // Restore the original TextRenderingMode if needed.
            if (restoreTextRenderingMode) {
                cb.setTextRenderingMode(PdfContentByte.TEXT_RENDER_MODE_FILL);
            }

            cb.endText();
            return width;
        } finally {
            setTransform(at);
        }
    }

    /**
     * @see Graphics#drawString(AttributedCharacterIterator, int, int)
     */
    public void drawString(AttributedCharacterIterator iterator, int x, int y) {
        drawString(iterator, (float)x, (float)y);
    }
    
    /**
     * @see Graphics2D#drawString(AttributedCharacterIterator, float, float)
     */
    public void drawString(AttributedCharacterIterator iter, float x, float y) {
/*
        StringBuffer sb = new StringBuffer();
        for(char c = iter.first(); c != AttributedCharacterIterator.DONE; c = iter.next()) {
            sb.append(c);
        }
        drawString(sb.toString(),x,y);
*/
        StringBuilder stringbuffer = new StringBuilder(iter.getEndIndex());
        for(char c = iter.first(); c != '\uFFFF'; c = iter.next())
        {
            if(iter.getIndex() == iter.getRunStart())
            {
                if(stringbuffer.length() > 0)
                {
                    drawString(stringbuffer.toString(), x, y);
                    FontMetrics fontmetrics = getFontMetrics();
                    x = (float)(x + fontmetrics.getStringBounds(stringbuffer.toString(), this).getWidth());
                    stringbuffer.delete(0, stringbuffer.length());
                }
                doAttributes(iter);
            }
            stringbuffer.append(c);
        }
        
        drawString(stringbuffer.toString(), x, y);
        underline = false;
    }
    
    /**
     * @see Graphics2D#drawGlyphVector(GlyphVector, float, float)
     */
    public void drawGlyphVector(GlyphVector g, float x, float y) {
        Shape s = g.getOutline(x, y);
        fill(s);
    }
    
    /**
     * @see Graphics2D#fill(Shape)
     */
    public void fill(Shape s) {
        followPath(s, FILL);
    }
    
    /**
     * @see Graphics2D#hit(Rectangle, Shape, boolean)
     */
    public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
        if (onStroke) {
            s = stroke.createStrokedShape(s);
        }
        s = transform.createTransformedShape(s);
        Area area = new Area(s);
        if (clip != null)
            area.intersect(clip);
        return area.intersects(rect.x, rect.y, rect.width, rect.height);
    }
    
    /**
     * @see Graphics2D#getDeviceConfiguration()
     */
    public GraphicsConfiguration getDeviceConfiguration() {
        return dg2.getDeviceConfiguration();
    }
    
    /**
     * Method contributed by Alexej Suchov
     * @see Graphics2D#setComposite(Composite)
     */
    public void setComposite(Composite comp) {
        
        if (comp instanceof AlphaComposite) {

            AlphaComposite composite = (AlphaComposite) comp;

            if (composite.getRule() == 3) {

                alpha = composite.getAlpha();
                this.composite = composite;

                if (realPaint != null && (realPaint instanceof Color)) {

                    Color c = (Color) realPaint;
                    paint = new Color(c.getRed(), c.getGreen(), c.getBlue(),
                            (int) (c.getAlpha() * alpha));
                }
                return;
            }
        }

        this.composite = comp;
        alpha = 1.0F;

    }
    
    /**
     * Method contributed by Alexej Suchov
     * @see Graphics2D#setPaint(Paint)
     */
    public void setPaint(Paint paint) {
        if (paint == null)
            return;
        this.paint = paint;
        realPaint = paint;

        if ((composite instanceof AlphaComposite) && (paint instanceof Color)) {
            
            AlphaComposite co = (AlphaComposite) composite;
            
            if (co.getRule() == 3) {
                Color c = (Color) paint;
                this.paint = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (c.getAlpha() * alpha));
                realPaint = paint;
            }
        }

    }

    private Stroke transformStroke(Stroke stroke) {
        if (!(stroke instanceof BasicStroke))
            return stroke;
        BasicStroke st = (BasicStroke)stroke;
        float scale = (float)Math.sqrt(Math.abs(transform.getDeterminant()));
        float[] dash = st.getDashArray();
        if (dash != null) {
            for (int k = 0; k < dash.length; ++k)
                dash[k] *= scale;
        }
        return new BasicStroke(st.getLineWidth() * scale, st.getEndCap(), st.getLineJoin(), st.getMiterLimit(), dash, st.getDashPhase() * scale);
    }
    
    private void setStrokeDiff(Stroke newStroke, Stroke oldStroke) {
        if (newStroke == oldStroke)
            return;
        if (!(newStroke instanceof BasicStroke))
            return;
        BasicStroke nStroke = (BasicStroke)newStroke;
        boolean oldOk = (oldStroke instanceof BasicStroke);
        BasicStroke oStroke = null;
        if (oldOk)
            oStroke = (BasicStroke)oldStroke;
        if (!oldOk || nStroke.getLineWidth() != oStroke.getLineWidth())
            cb.setLineWidth(nStroke.getLineWidth());
        if (!oldOk || nStroke.getEndCap() != oStroke.getEndCap()) {
            switch (nStroke.getEndCap()) {
            case BasicStroke.CAP_BUTT:
                cb.setLineCap(0);
                break;
            case BasicStroke.CAP_SQUARE:
                cb.setLineCap(2);
                break;
            default:
                cb.setLineCap(1);
            }
        }
        if (!oldOk || nStroke.getLineJoin() != oStroke.getLineJoin()) {
            switch (nStroke.getLineJoin()) {
            case BasicStroke.JOIN_MITER:
                cb.setLineJoin(0);
                break;
            case BasicStroke.JOIN_BEVEL:
                cb.setLineJoin(2);
                break;
            default:
                cb.setLineJoin(1);
            }
        }
        if (!oldOk || nStroke.getMiterLimit() != oStroke.getMiterLimit())
            cb.setMiterLimit(nStroke.getMiterLimit());
        boolean makeDash;
        if (oldOk) {
            if (nStroke.getDashArray() != null) {
                if (nStroke.getDashPhase() != oStroke.getDashPhase()) {
                    makeDash = true;
                }
                else makeDash = !java.util.Arrays.equals(nStroke.getDashArray(), oStroke.getDashArray());
            }
            else makeDash = oStroke.getDashArray() != null;
        }
        else {
            makeDash = true;
        }
        if (makeDash) {
            float[] dash = nStroke.getDashArray();
            if (dash == null)
                cb.setLiteral("[]0 d\n");
            else {
                cb.setLiteral('[');
                int lim = dash.length;
                for (float dash1 : dash) {
                    cb.setLiteral(dash1);
                    cb.setLiteral(' ');
                }
                cb.setLiteral(']');
                cb.setLiteral(nStroke.getDashPhase());
                cb.setLiteral(" d\n");
            }
        }
    }
    
    /**
     * @see Graphics2D#setStroke(Stroke)
     */
    public void setStroke(Stroke s) {
        originalStroke = s;
        this.stroke = transformStroke(s);
    }
    
    
    /**
     * Sets a rendering hint
     * @param arg0 the Key
     * @param arg1 the Object to associate to it
     */
    public void setRenderingHint(Key arg0, Object arg1) {
         if (arg1 != null) {
             rhints.put(arg0, arg1);
         } else {
             if (arg0 instanceof HyperLinkKey)
             {
                 rhints.put(arg0, HyperLinkKey.VALUE_HYPERLINKKEY_OFF);
             }
             else
             {
                 rhints.remove(arg0);
             }
         }
    }
    
    /**
     * @param arg0 a key
     * @return the rendering hint
     */
    public Object getRenderingHint(Key arg0) {
        return rhints.get(arg0);
    }
    
    /**
     * @see Graphics2D#setRenderingHints(Map)
     */
    public void setRenderingHints(Map hints) {
        rhints.clear();
        rhints.putAll(hints);
    }
    
    /**
     * @see Graphics2D#addRenderingHints(Map)
     */
    public void addRenderingHints(Map hints) {
        rhints.putAll(hints);
    }
    
    /**
     * @see Graphics2D#getRenderingHints()
     */
    public RenderingHints getRenderingHints() {
        return rhints;
    }
    
    /**
     * @see Graphics#translate(int, int)
     */
    public void translate(int x, int y) {
        translate((double)x, (double)y);
    }
    
    /**
     * @see Graphics2D#translate(double, double)
     */
    public void translate(double tx, double ty) {
        transform.translate(tx,ty);
    }
    
    /**
     * @see Graphics2D#rotate(double)
     */
    public void rotate(double theta) {
        transform.rotate(theta);
    }
    
    /**
     * @see Graphics2D#rotate(double, double, double)
     */
    public void rotate(double theta, double x, double y) {
        transform.rotate(theta, x, y);
    }
    
    /**
     * @see Graphics2D#scale(double, double)
     */
    public void scale(double sx, double sy) {
        transform.scale(sx, sy);
        this.stroke = transformStroke(originalStroke);
    }
    
    /**
     * @see Graphics2D#shear(double, double)
     */
    public void shear(double shx, double shy) {
        transform.shear(shx, shy);
    }
    
    /**
     * @see Graphics2D#transform(AffineTransform)
     */
    public void transform(AffineTransform tx) {
        transform.concatenate(tx);
        this.stroke = transformStroke(originalStroke);
    }
    
    /**
     * @see Graphics2D#setTransform(AffineTransform)
     */
    public void setTransform(AffineTransform t) {
        transform = new AffineTransform(t);
        this.stroke = transformStroke(originalStroke);
    }
    
    /**
     * @see Graphics2D#getTransform()
     */
    public AffineTransform getTransform() {
        return new AffineTransform(transform);
    }
    
    /**
     * Method contributed by Alexej Suchov
     * @see Graphics2D#getPaint()
     */
    public Paint getPaint() {
        if (realPaint != null) {
            return realPaint;
        } else {
            return paint;
        }
    }
    
    /**
     * @see Graphics2D#getComposite()
     */
    public Composite getComposite() {
        return composite;
    }
    
    /**
     * @see Graphics2D#setBackground(Color)
     */
    public void setBackground(Color color) {
        background = color;
    }
    
    /**
     * @see Graphics2D#getBackground()
     */
    public Color getBackground() {
        return background;
    }
    
    /**
     * @see Graphics2D#getStroke()
     */
    public Stroke getStroke() {
        return originalStroke;
    }
    
    
    /**
     * @see Graphics2D#getFontRenderContext()
     */
    public FontRenderContext getFontRenderContext() {
        boolean antialias = RenderingHints.VALUE_TEXT_ANTIALIAS_ON.equals(getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING));
        boolean fractions = RenderingHints.VALUE_FRACTIONALMETRICS_ON.equals(getRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS));
        return new FontRenderContext(new AffineTransform(), antialias, fractions);
    }
    
    /**
     * @see Graphics#create()
     */
    public Graphics create() {
        PdfGraphics2D g2 = new PdfGraphics2D();
        g2.rhints.putAll( this.rhints );
        g2.onlyShapes = this.onlyShapes;
        g2.transform = new AffineTransform(this.transform);
        g2.baseFonts = this.baseFonts;
        g2.fontMapper = this.fontMapper;
        g2.paint = this.paint;
        g2.fillGState = this.fillGState;
        g2.currentFillGState = this.currentFillGState;
        g2.currentStrokeGState = this.currentStrokeGState;
        g2.strokeGState = this.strokeGState;
        g2.background = this.background;
        g2.mediaTracker = this.mediaTracker;
        g2.convertImagesToJPEG = this.convertImagesToJPEG;
        g2.jpegQuality = this.jpegQuality;
        g2.setFont(this.font);
        g2.cb = this.cb.getDuplicate();
        g2.cb.saveState();
        g2.width = this.width;
        g2.height = this.height;
        g2.followPath(new Area(new Rectangle2D.Float(0, 0, width, height)), CLIP);
        if (this.clip != null)
            g2.clip = new Area(this.clip);
        g2.composite = composite;
        g2.stroke = stroke;
        g2.originalStroke = originalStroke;
        g2.strokeOne = (BasicStroke)g2.transformStroke(g2.strokeOne);
        g2.oldStroke = g2.strokeOne;
        g2.setStrokeDiff(g2.oldStroke, null);
        g2.cb.saveState();
        if (g2.clip != null)
            g2.followPath(g2.clip, CLIP);
        g2.kid = true;
        if (this.kids == null)
            this.kids = new ArrayList<>();
        this.kids.add(cb.getInternalBuffer().size());
        this.kids.add(g2);
        return g2;
    }
    
    public PdfContentByte getContent() {
        return this.cb;
    }
    /**
     * @see Graphics#getColor()
     */
    public Color getColor() {
        if (paint instanceof Color) {
            return (Color)paint;
        } else {
            return Color.black;
        }
    }
    
    /**
     * @see Graphics#setColor(Color)
     */
    public void setColor(Color color) {
        setPaint(color);
    }
    
    /**
     * @see Graphics#setPaintMode()
     */
    public void setPaintMode() {}
    
    /**
     * @see Graphics#setXORMode(Color)
     */
    public void setXORMode(Color c1) {
        
    }
    
    /**
     * @see Graphics#getFont()
     */
    public Font getFont() {
        return font;
    }
    
    /**
     * @see Graphics#setFont(Font)
     */
    /**
     * Sets the current font.
     */
    public void setFont(Font f) {
        if (f == null)
            return;
        if (onlyShapes) {
            font = f;
            return;
        }
        if (f == font)
            return;
        font = f;
        fontSize = f.getSize2D();
        baseFont = getCachedBaseFont(f);
    }
    
    private BaseFont getCachedBaseFont(Font f) {
        synchronized (baseFonts) {
            BaseFont bf = baseFonts.get(f.getFontName());
            if (bf == null) {
                bf = fontMapper.awtToPdf(f);
                baseFonts.put(f.getFontName(), bf);
            }
            return bf;
        }
    }
    
    /**
     * @see Graphics#getFontMetrics(Font)
     */
    public FontMetrics getFontMetrics(Font f) {
        return dg2.getFontMetrics(f);
    }
    
    /**
     * @see Graphics#getClipBounds()
     */
    public Rectangle getClipBounds() {
        if (clip == null)
            return null;
        return getClip().getBounds();
    }
    
    /**
     * @see Graphics#clipRect(int, int, int, int)
     */
    public void clipRect(int x, int y, int width, int height) {
        Rectangle2D rect = new Rectangle2D.Double(x,y,width,height);
        clip(rect);
    }
    
    /**
     * @see Graphics#setClip(int, int, int, int)
     */
    public void setClip(int x, int y, int width, int height) {
        Rectangle2D rect = new Rectangle2D.Double(x,y,width,height);
        setClip(rect);
    }
    
    /**
     * @see Graphics2D#clip(Shape)
     */
    public void clip(Shape s) {
        if (s == null) {
            setClip(null);
            return;
        }
        s = transform.createTransformedShape(s);
        if (clip == null)
            clip = new Area(s);
        else
            clip.intersect(new Area(s));
        followPath(s, CLIP);
    }
    
    /**
     * @see Graphics#getClip()
     */
    public Shape getClip() {
        try {
            return transform.createInverse().createTransformedShape(clip);
        }
        catch (NoninvertibleTransformException e) {
            return null;
        }
    }
    
    /**
     * @see Graphics#setClip(Shape)
     */
    public void setClip(Shape s) {
        cb.restoreState();
        cb.saveState();
        if (s != null)
            s = transform.createTransformedShape(s);
        if (s == null) {
            clip = null;
        }
        else {
            clip = new Area(s);
            followPath(s, CLIP);
        }
        paintFill = paintStroke = null;
        currentFillGState = currentStrokeGState = -1;
        oldStroke = strokeOne;
    }
    
    /**
     * @see Graphics#copyArea(int, int, int, int, int, int)
     */
    public void copyArea(int x, int y, int width, int height, int dx, int dy) {
        
    }
    
    /**
     * @see Graphics#drawLine(int, int, int, int)
     */
    public void drawLine(int x1, int y1, int x2, int y2) {
        Line2D line = new Line2D.Double(x1, y1, x2, y2);
        draw(line);
    }
    
    /**
     * @see Graphics#fillRect(int, int, int, int)
     */
    public void drawRect(int x, int y, int width, int height) {
        draw(new Rectangle(x, y, width, height));
    }
    
    /**
     * @see Graphics#fillRect(int, int, int, int)
     */
    public void fillRect(int x, int y, int width, int height) {
        fill(new Rectangle(x,y,width,height));
    }
    
    /**
     * @see Graphics#clearRect(int, int, int, int)
     */
    public void clearRect(int x, int y, int width, int height) {
        Paint temp = paint;
        setPaint(background);
        fillRect(x,y,width,height);
        setPaint(temp);
    }
    
    /**
     * @see Graphics#drawRoundRect(int, int, int, int, int, int)
     */
    public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
        RoundRectangle2D rect = new RoundRectangle2D.Double(x,y,width,height,arcWidth, arcHeight);
        draw(rect);
    }
    
    /**
     * @see Graphics#fillRoundRect(int, int, int, int, int, int)
     */
    public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
        RoundRectangle2D rect = new RoundRectangle2D.Double(x,y,width,height,arcWidth, arcHeight);
        fill(rect);
    }
    
    /**
     * @see Graphics#drawOval(int, int, int, int)
     */
    public void drawOval(int x, int y, int width, int height) {
        Ellipse2D oval = new Ellipse2D.Float(x, y, width, height);
        draw(oval);
    }
    
    /**
     * @see Graphics#fillOval(int, int, int, int)
     */
    public void fillOval(int x, int y, int width, int height) {
        Ellipse2D oval = new Ellipse2D.Float(x, y, width, height);
        fill(oval);
    }
    
    /**
     * @see Graphics#drawArc(int, int, int, int, int, int)
     */
    public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
        Arc2D arc = new Arc2D.Double(x,y,width,height,startAngle, arcAngle, Arc2D.OPEN);
        draw(arc);

    }
    
    /**
     * @see Graphics#fillArc(int, int, int, int, int, int)
     */
    public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
        Arc2D arc = new Arc2D.Double(x,y,width,height,startAngle, arcAngle, Arc2D.PIE);
        fill(arc);
    }
    
    /**
     * @see Graphics#drawPolyline(int[], int[], int)
     */
    public void drawPolyline(int[] x, int[] y, int nPoints) {
        PolylineShape polyline = new PolylineShape(x, y, nPoints);
        draw(polyline);
    }
    
    /**
     * @see Graphics#drawPolygon(int[], int[], int)
     */
    public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {
        Polygon poly = new Polygon(xPoints, yPoints, nPoints);
        draw(poly);
    }
    
    /**
     * @see Graphics#fillPolygon(int[], int[], int)
     */
    public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {
        Polygon poly = new Polygon();
        for (int i = 0; i < nPoints; i++) {
            poly.addPoint(xPoints[i], yPoints[i]);
        }
        fill(poly);
    }
    
    /**
     * @see Graphics#drawImage(Image, int, int, ImageObserver)
     */
    public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
        return drawImage(img, x, y, null, observer);
    }
    
    /**
     * @see Graphics#drawImage(Image, int, int, int, int, ImageObserver)
     */
    public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) {
        return drawImage(img, x, y, width, height, null, observer);
    }
    
    /**
     * @see Graphics#drawImage(Image, int, int, Color, ImageObserver)
     */
    public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) {
        waitForImage(img);
        return drawImage(img, x, y, img.getWidth(observer), img.getHeight(observer), bgcolor, observer);
    }
    
    /**
     * @see Graphics#drawImage(Image, int, int, int, int, Color, ImageObserver)
     */
    public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) {
        waitForImage(img);
        double scalex = width/(double)img.getWidth(observer);
        double scaley = height/(double)img.getHeight(observer);
        AffineTransform tx = AffineTransform.getTranslateInstance(x,y);
        tx.scale(scalex,scaley);
        return drawImage(img, null, tx, bgcolor, observer);
    }
    
    /**
     * @see Graphics#drawImage(Image, int, int, int, int, int, int, int, int, ImageObserver)
     */
    public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) {
        return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null, observer);
    }
    
    /**
     * @see Graphics#drawImage(Image, int, int, int, int, int, int, int, int, Color, ImageObserver)
     */
    public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) {
        waitForImage(img);
        double dwidth = (double)dx2-dx1;
        double dheight = (double)dy2-dy1;
        double swidth = (double)sx2-sx1;
        double sheight = (double)sy2-sy1;
        
        //if either width or height is 0, then there is nothing to draw
        if (dwidth == 0 || dheight == 0 || swidth == 0 || sheight == 0) return true;
        
        double scalex = dwidth/swidth;
        double scaley = dheight/sheight;
        
        double transx = sx1*scalex;
        double transy = sy1*scaley;
        AffineTransform tx = AffineTransform.getTranslateInstance(dx1-transx,dy1-transy);
        tx.scale(scalex,scaley);
        
        BufferedImage mask = new BufferedImage(img.getWidth(observer), img.getHeight(observer), BufferedImage.TYPE_BYTE_BINARY);
        Graphics g = mask.getGraphics();
        g.fillRect(sx1,sy1, (int)swidth, (int)sheight);
        drawImage(img, mask, tx, null, observer);
        g.dispose();
        return true;
    }
    
    /**
     * @see Graphics#dispose()
     */
    public void dispose() {
        if (kid)
            return;
        if (!disposeCalled) {
            disposeCalled = true;
            cb.restoreState();
            cb.restoreState();
            dg2.dispose();
            dg2 = null;
            if (kids != null) {
                ByteBuffer buf = new ByteBuffer();
                internalDispose(buf);
                ByteBuffer buf2 = cb.getInternalBuffer();
                buf2.reset();
                buf2.append(buf);
            }
        }
    }
    
    private void internalDispose(ByteBuffer buf) {
        int last = 0;
        int pos = 0;
        ByteBuffer buf2 = cb.getInternalBuffer();
        if (kids != null) {
            for (int k = 0; k < kids.size(); k += 2) {
                pos = (Integer) kids.get(k);
                PdfGraphics2D g2 = (PdfGraphics2D)kids.get(k + 1);
                g2.cb.restoreState();
                g2.cb.restoreState();
                buf.append(buf2.getBuffer(), last, pos - last);
                g2.dg2.dispose();
                g2.dg2 = null;
                g2.internalDispose(buf);
                last = pos;
            }
        }
        buf.append(buf2.getBuffer(), last, buf2.size() - last);
    }
    
    ///////////////////////////////////////////////
    //
    //
    //        implementation specific methods
    //
    //

    /**
     * Enables/Disables the composite font drawer due to issues with custom font mappers that do not always default to one specific font but allow custom fonts.
     * @param compositeFontDrawerEnabled true if the composite font drawer should be used else false.
     */
    public void setCompositeFontDrawerEnabled(boolean compositeFontDrawerEnabled) {
        isCompositeFontDrawerEnabled = compositeFontDrawerEnabled;
    }

    private void followPath(Shape s, int drawType) {
        if (s==null) return;
        if (drawType==STROKE) {
            if (!(stroke instanceof BasicStroke)) {
                s = stroke.createStrokedShape(s);
                followPath(s, FILL);
                return;
            }
        }
        if (drawType==STROKE) {
            setStrokeDiff(stroke, oldStroke);
            oldStroke = stroke;
            setStrokePaint();
        }
        else if (drawType==FILL)
            setFillPaint();
        PathIterator points;
        int traces = 0;
        if (drawType == CLIP)
            points = s.getPathIterator(IDENTITY);
        else
            points = s.getPathIterator(transform);
        float[] coords = new float[6];
        while(!points.isDone()) {
            ++traces;
            int segtype = points.currentSegment(coords);
            normalizeY(coords);
            switch(segtype) {
                case PathIterator.SEG_CLOSE:
                    cb.closePath();
                    break;

                case PathIterator.SEG_CUBICTO:
                    cb.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
                    break;

                case PathIterator.SEG_LINETO:
                    cb.lineTo(coords[0], coords[1]);
                    break;

                case PathIterator.SEG_MOVETO:
                    cb.moveTo(coords[0], coords[1]);
                    break;

                case PathIterator.SEG_QUADTO:
                    cb.curveTo(coords[0], coords[1], coords[2], coords[3]);
                    break;
            }
            points.next();
        }
        switch (drawType) {
        case FILL:
            if (traces > 0) {
                if (points.getWindingRule() == PathIterator.WIND_EVEN_ODD)
                    cb.eoFill();
                else
                    cb.fill();
            }
            break;
        case STROKE:
            if (traces > 0)
                cb.stroke();
            break;
        default: //drawType==CLIP
            if (traces == 0)
                cb.rectangle(0, 0, 0, 0);
            if (points.getWindingRule() == PathIterator.WIND_EVEN_ODD)
                cb.eoClip();
            else
                cb.clip();
            cb.newPath();
        }
    }
    
    private float normalizeY(float y) {
        return this.height - y;
    }
    
    private void normalizeY(float[] coords) {
        coords[1] = normalizeY(coords[1]);
        coords[3] = normalizeY(coords[3]);
        coords[5] = normalizeY(coords[5]);
    }
    
    private AffineTransform normalizeMatrix() {
        double[] mx = new double[6];
        AffineTransform result = AffineTransform.getTranslateInstance(0,0);
        result.getMatrix(mx);
        mx[3]=-1;
        mx[5]=height;
        result = new AffineTransform(mx);
        result.concatenate(transform);
        return result;
    }
    
    private boolean drawImage(Image img, Image mask, AffineTransform xform, Color bgColor, ImageObserver obs) {
        if (xform==null)
            xform = new AffineTransform();
        else
            xform = new AffineTransform(xform);
        xform.translate(0, img.getHeight(obs));
        xform.scale(img.getWidth(obs), img.getHeight(obs));
        
        AffineTransform inverse = this.normalizeMatrix();
        AffineTransform flipper = AffineTransform.getScaleInstance(1,-1);
        inverse.concatenate(xform);
        inverse.concatenate(flipper);
        
        double[] mx = new double[6];
        inverse.getMatrix(mx);
        if (currentFillGState != 255) {
            PdfGState gs = fillGState[255];
            if (gs == null) {
                gs = new PdfGState();
                gs.setFillOpacity(1);
                fillGState[255] = gs;
            }
            cb.setGState(gs);
        }
        
        try {
            com.lowagie.text.Image image = null;
            if(!convertImagesToJPEG){
                image = com.lowagie.text.Image.getInstance(img, bgColor);
            }
            else{
                BufferedImage scaled = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_RGB);
                Graphics2D g3 = scaled.createGraphics();
                g3.drawImage(img, 0, 0, img.getWidth(null), img.getHeight(null), null);
                g3.dispose();
                
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ImageWriteParam iwparam = new JPEGImageWriteParam(Locale.getDefault());
                iwparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                iwparam.setCompressionQuality(jpegQuality);//Set here your compression rate
                ImageWriter iw = ImageIO.getImageWritersByFormatName("jpg").next();
                ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
                iw.setOutput(ios);
                iw.write(null, new IIOImage(scaled, null, null), iwparam);
                iw.dispose();
                ios.close();

                scaled.flush();
                scaled = null;
                image = com.lowagie.text.Image.getInstance(baos.toByteArray());
                
            }
            if (mask!=null) {
                com.lowagie.text.Image msk = com.lowagie.text.Image.getInstance(mask, null, true);
                msk.makeMask();
                msk.setInverted(true);
                image.setImageMask(msk);
            }
            cb.addImage(image, (float)mx[0], (float)mx[1], (float)mx[2], (float)mx[3], (float)mx[4], (float)mx[5]);
            Object url = getRenderingHint(HyperLinkKey.KEY_INSTANCE);
            if (url != null && !url.equals(HyperLinkKey.VALUE_HYPERLINKKEY_OFF)) {
                PdfAction action = new  PdfAction(url.toString());
                cb.setAction(action, (float)mx[4], (float)mx[5], (float)(mx[0]+mx[4]), (float)(mx[3]+mx[5]));
            }
        } catch (Exception ex) {
            throw new IllegalArgumentException();
        }
        if (currentFillGState != 255 && currentFillGState != -1) {
            PdfGState gs = fillGState[currentFillGState];
            cb.setGState(gs);
        }        
        return true;
    }
    
    private boolean checkNewPaint(Paint oldPaint) {
        if (paint == oldPaint)
            return false;
        return !((paint instanceof Color) && paint.equals(oldPaint));
    }
    
    private void setFillPaint() {
        if (checkNewPaint(paintFill)) {
            paintFill = paint;
            setPaint(false, 0, 0, true);
        }
    }
    
    private void setStrokePaint() {
        if (checkNewPaint(paintStroke)) {
            paintStroke = paint;
            setPaint(false, 0, 0, false);
        }
    }
    
    private void setPaint(boolean invert, double xoffset, double yoffset, boolean fill) {
        if (paint instanceof Color) {
            Color color = (Color)paint;
            int alpha = color.getAlpha();
            if (fill) {
                if (alpha != currentFillGState) {
                    currentFillGState = alpha;
                    PdfGState gs = fillGState[alpha];
                    if (gs == null) {
                        gs = new PdfGState();
                        gs.setFillOpacity(alpha / 255f);
                        fillGState[alpha] = gs;
                    }
                    cb.setGState(gs);
                }
                cb.setColorFill(color);
            }
            else {
                if (alpha != currentStrokeGState) {
                    currentStrokeGState = alpha;
                    PdfGState gs = strokeGState[alpha];
                    if (gs == null) {
                        gs = new PdfGState();
                        gs.setStrokeOpacity(alpha / 255f);
                        strokeGState[alpha] = gs;
                    }
                    cb.setGState(gs);
                }
                cb.setColorStroke(color);
            }
        }
        else if (paint instanceof GradientPaint) {
            GradientPaint gp = (GradientPaint)paint;
            Point2D p1 = gp.getPoint1();
            transform.transform(p1, p1);
            Point2D p2 = gp.getPoint2();
            transform.transform(p2, p2);
            Color c1 = gp.getColor1();
            Color c2 = gp.getColor2();
            PdfShading shading = PdfShading.simpleAxial(cb.getPdfWriter(), (float)p1.getX(), normalizeY((float)p1.getY()), (float)p2.getX(), normalizeY((float)p2.getY()), c1, c2);
            PdfShadingPattern pat = new PdfShadingPattern(shading);
            if (fill)
                cb.setShadingFill(pat);
            else
                cb.setShadingStroke(pat);
        }
        else if (paint instanceof TexturePaint) {
            try {
                TexturePaint tp = (TexturePaint)paint;
                BufferedImage img = tp.getImage();
                Rectangle2D rect = tp.getAnchorRect();
                com.lowagie.text.Image image = com.lowagie.text.Image.getInstance(img, null);
                PdfPatternPainter pattern = cb.createPattern(image.getWidth(), image.getHeight());
                AffineTransform inverse = this.normalizeMatrix();
                inverse.translate(rect.getX(), rect.getY());
                inverse.scale(rect.getWidth() / image.getWidth(), -rect.getHeight() / image.getHeight());
                double[] mx = new double[6];
                inverse.getMatrix(mx);
                pattern.setPatternMatrix((float)mx[0], (float)mx[1], (float)mx[2], (float)mx[3], (float)mx[4], (float)mx[5]) ;
                image.setAbsolutePosition(0,0);
                pattern.addImage(image);
                if (fill)
                    cb.setPatternFill(pattern);
                else
                    cb.setPatternStroke(pattern);
            } catch (Exception ex) {
                if (fill)
                    cb.setColorFill(Color.gray);
                else
                    cb.setColorStroke(Color.gray);
            }
        }
        else {
            try {
                BufferedImage img = null;
                int type = BufferedImage.TYPE_4BYTE_ABGR;
                if (paint.getTransparency() == Transparency.OPAQUE) {
                    type = BufferedImage.TYPE_3BYTE_BGR;
                }
                img = new BufferedImage((int)width, (int)height, type);
                Graphics2D g = (Graphics2D)img.getGraphics();
                g.transform(transform);
                AffineTransform inv = transform.createInverse();
                Shape fillRect = new Rectangle2D.Double(0,0,img.getWidth(),img.getHeight());
                fillRect = inv.createTransformedShape(fillRect);
                g.setPaint(paint);
                g.fill(fillRect);
                if (invert) {
                    AffineTransform tx = new AffineTransform();
                    tx.scale(1,-1);
                    tx.translate(-xoffset,-yoffset);
                    g.drawImage(img,tx,null);
                }
                g.dispose();
                g = null;
                com.lowagie.text.Image image = com.lowagie.text.Image.getInstance(img, null);
                PdfPatternPainter pattern = cb.createPattern(width, height);
                image.setAbsolutePosition(0,0);
                pattern.addImage(image);

                if (fill) {
                    if (currentFillGState != 255){
                        currentFillGState = 255;
                        PdfGState gs = fillGState[255];
                        if (gs == null) {
                            gs = new PdfGState();
                            gs.setFillOpacity(1);
                            fillGState[255] = gs;
                        }
                        cb.setGState(gs);
                    }
                    cb.setPatternFill(pattern);
                }
                else {
                    cb.setPatternStroke(pattern);
                }
            } catch (Exception ex) {
                if (fill)
                    cb.setColorFill(Color.gray);
                else
                    cb.setColorStroke(Color.gray);
            }
        }
    }
    
    private synchronized void waitForImage(java.awt.Image image) {
        if (mediaTracker == null)
            mediaTracker = new MediaTracker(new PdfGraphics2D.FakeComponent());
        mediaTracker.addImage(image, 0);
        try {
            mediaTracker.waitForID(0);
        }
        catch (InterruptedException e) {
            // empty on purpose
        }
        mediaTracker.removeImage(image);
    }

    /**
     * Split the given string into substrings depending on if all characters of substring can be
     * displayed with the defined font or not.
     *
     * @param s
     *            given string
     * @return list of substrings that can be displayed by with the defined font or not.
     *
     */
    private List splitIntoSubstringsByVisibility(String s) {
        List stringParts = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        boolean displayableLastChar = true;
        for (int charIndex = 0; charIndex < s.length(); charIndex++) {
            char c = s.charAt(charIndex);
            boolean b = font.canDisplay(c);
            if (charIndex > 0 && displayableLastChar != b) {
                stringParts.add(sb.toString());
                sb.setLength(0);
            }
            displayableLastChar = b;
            sb.append(c);
        }
        stringParts.add(sb.toString());
        return stringParts;
    }

        
    static private class FakeComponent extends Component {

        private static final long serialVersionUID = 6450197945596086638L;
    }

    /**
     * @since 2.0.8
     */
    public static class HyperLinkKey extends RenderingHints.Key
    {
         public static final HyperLinkKey KEY_INSTANCE = new HyperLinkKey(9999);
         public static final Object VALUE_HYPERLINKKEY_OFF = "0";
         
        protected HyperLinkKey(int arg0) {
            super(arg0);
        }
        
        public boolean isCompatibleValue(Object val)
        {
            return true;
        }
        public String toString()
        {
            return "HyperLinkKey";
        }
    }


    /**
     * Wrapper class that helps to draw string with sun.font.CompositeFont (Windows logical
     * fonts).
* If the given font is a sun.font.CompositeFont than try to find some font (an implementation * of sun.font.Font2D) that will display current text. For some symbols that cannot be displayed * with the font from the first slot of the composite font all other font will be checked.
*
* This processing is not necessary only for Mac OS - there isn't used "sun.font.CompositeFont", * but "sun.font.CFont".
*
* Since the sun.* packages are not part of the supported, public interface the * reflection will be used. */ private static class CompositeFontDrawer { static boolean isSupported() { return SUPPORTED; } private static final String GET_MODULE_METHOD_NAME = "getModule"; private static final String IS_OPEN_METHOD_NAME = "isOpen"; private static final String ADD_OPENS_METHOD_NAME = "addOpens"; private static final String COMPOSITE_FONT_CLASS_NAME = "sun.font.CompositeFont"; private static final Class COMPOSITE_FONT_CLASS; private static final String GET_NUM_SLOTS_METHOD_NAME = "getNumSlots"; private static final Method GET_NUM_SLOTS_METHOD; private static final String GET_SLOT_FONT_METHOD_NAME = "getSlotFont"; private static final Method GET_SLOT_FONT_METHOD; private static final String FONT_UTILITIES_CLASS_NAME = "sun.font.FontUtilities"; private static final Class FONT_UTILITIES_CLASS; private static final String GET_FONT2D_METHOD_NAME = "getFont2D"; private static final Method GET_FONT2D_METHOD; private static final String FONT2D_CLASS_NAME = "sun.font.Font2D"; private static final Class FONT2D_CLASS; private static final String CAN_DISPLAY_METHOD_NAME = "canDisplay"; private static final Method CAN_DYSPLAY_METHOD; private static final String GET_FONT_NAME_METHOD_NAME = "getFontName"; private static final Method GET_FONT_NAME_METHOD; private static final boolean SUPPORTED; static { String osName = System.getProperty("os.name", "unknownOS"); boolean macOS = osName.startsWith("Mac"); if (!macOS) { FONT_UTILITIES_CLASS = getClassForName(FONT_UTILITIES_CLASS_NAME); updateModuleToOpenPackage(FONT_UTILITIES_CLASS, "sun.font"); GET_FONT2D_METHOD = getMethod(FONT_UTILITIES_CLASS, GET_FONT2D_METHOD_NAME, Font.class); COMPOSITE_FONT_CLASS = getClassForName(COMPOSITE_FONT_CLASS_NAME); GET_NUM_SLOTS_METHOD = getMethod(COMPOSITE_FONT_CLASS, GET_NUM_SLOTS_METHOD_NAME); GET_SLOT_FONT_METHOD = getMethod(COMPOSITE_FONT_CLASS, GET_SLOT_FONT_METHOD_NAME, int.class); FONT2D_CLASS = getClassForName(FONT2D_CLASS_NAME); CAN_DYSPLAY_METHOD = getMethod(FONT2D_CLASS, CAN_DISPLAY_METHOD_NAME, char.class); GET_FONT_NAME_METHOD = getMethod(FONT2D_CLASS, GET_FONT_NAME_METHOD_NAME, Locale.class); } else { FONT_UTILITIES_CLASS = null; GET_FONT2D_METHOD = null; COMPOSITE_FONT_CLASS = null; GET_NUM_SLOTS_METHOD = null; GET_SLOT_FONT_METHOD = null; FONT2D_CLASS = null; CAN_DYSPLAY_METHOD = null; GET_FONT_NAME_METHOD = null; } SUPPORTED = FONT_UTILITIES_CLASS != null && COMPOSITE_FONT_CLASS != null && FONT2D_CLASS != null && GET_FONT2D_METHOD != null && GET_NUM_SLOTS_METHOD != null && GET_SLOT_FONT_METHOD != null && CAN_DYSPLAY_METHOD != null && GET_FONT_NAME_METHOD != null; } /** * Update module of the given class to open the given * package to the target module if the target module * is opened for the current module.
* This helps to avoid warnings for the --illegal-access=permit. * Actually (java 9-13) "permit" is default mode, but in the future java * releases the default mode will be "deny". It's also important to * add --add-opens for the given package if it's need. */ private static void updateModuleToOpenPackage(Class classInModule, String packageName) { if (classInModule == null || packageName == null) { return; } Method getModuleMethod = getMethod(Class.class, GET_MODULE_METHOD_NAME); if (getModuleMethod == null) { return; } try { Object targetModule = getModuleMethod.invoke(classInModule); if (targetModule == null) { return; } Class moduleClass = targetModule.getClass(); Object callerModule = getModuleMethod.invoke(CompositeFontDrawer.class); Method isOpenMethod = getMethod(moduleClass, IS_OPEN_METHOD_NAME, String.class, moduleClass); if (isOpenMethod == null) { return; } Object isOpened = isOpenMethod.invoke(targetModule, packageName, callerModule); if (isOpened instanceof Boolean && ((Boolean) isOpened)) { Method addOpensMethod = getMethod(moduleClass, ADD_OPENS_METHOD_NAME, String.class, moduleClass); if (callerModule != null) { addOpensMethod.invoke(targetModule, packageName, callerModule); } } } catch (Exception e) { e.printStackTrace(); } } private static Class getClassForName(String className) { try { return Class.forName(className); } catch (Exception e) { return null; } } private static Method getMethod(Class clazz, String methodName, Class... parameterTypes) { if (clazz == null) { return null; } Method method; try { method = clazz.getDeclaredMethod(methodName, parameterTypes); if (!method.isAccessible()) { method.setAccessible(true); } } catch (Exception e) { method = null; } return method; } private final transient StringBuilder sb = new StringBuilder(); /** * Splitted parts of the string. */ private final transient List stringParts = new ArrayList<>(); /** * {@link BaseFont Base fonts} that corresponds to the splitted part of the string */ private final transient List correspondingBaseFontsForParts = new ArrayList<>(); private final transient Map fontFamilyComposite = new HashMap<>(); /** * Check if the given font is a composite font. * * @param font * given font * @return true if the given font is sun.font.CompositeFont. False * otherwise. */ boolean isCompositeFont(Font font) { if (!isSupported() || font == null) { assert false; return false; } String fontFamily = font.getFamily(); if (fontFamily != null && fontFamilyComposite.containsKey(fontFamily)) { return fontFamilyComposite.get(fontFamily); } try { Object result = GET_FONT2D_METHOD.invoke(null, font); boolean composite = result != null && result.getClass() == COMPOSITE_FONT_CLASS; if (fontFamily != null) { fontFamilyComposite.put(fontFamily, composite); } return composite; } catch (Exception e) { return false; } } /** * Draw text with the given font at the specified position. This method splits the string * into parts so that it can be displayed with a matching (font that can display all symbols * of this part of string) slot font.
* If some class/method cannot be found or throw exception the default drawing string * function will be used for a drawing string. * * @param s * given string that should be drawn * @param compositeFont * composite font. This font should be an instance of composite font, otherwise * the default drawing function will be called. * @param x * the x coordinate of the location * @param y * the y coordinate of the location * @param fontConverter * function that convert {@link java.awt.Font font} to the needed * {@link com.lowagie.text.pdf.BaseFont base font} * @param defaultDrawingFunction * default drawing function that will be used for drawing string. * @return width of the drawn string. */ double drawString(String s, Font compositeFont, double x, double y, Function fontConverter, DrawStringFunction defaultDrawingFunction) { String fontFamily = compositeFont.getFamily(); if (!isSupported() || (fontFamily != null && !fontFamilyComposite.get(fontFamily))) { assert false; return defaultDrawingFunction .drawString(s, fontConverter.apply(compositeFont), x, y); } try { splitStringIntoDisplayableParts(s, compositeFont, fontConverter); double width = 0; for (int i = 0; i < stringParts.size(); i++) { String strPart = stringParts.get(i); BaseFont correspondingBaseFont = correspondingBaseFontsForParts.get(i); BaseFont baseFont = correspondingBaseFont == null ? fontConverter.apply(compositeFont) : correspondingBaseFont; width += defaultDrawingFunction.drawString(strPart, baseFont, x + width, y); } return width; } catch (Exception e) { BaseFont baseFont = fontConverter.apply(compositeFont); return defaultDrawingFunction.drawString(s, baseFont, x, y); } } /** * Split string into visible and not visible parts.
* This method split string into substring parts. For each splitted part * correspond found {@link BaseFont base font} from the slots of the composite * font witch can display all characters of the part of string. If no font found * the {@link BaseFont base font} from the own composite font will be used. * * @param s * @param compositeFont * @param fontConverter * @throws IllegalAccessException * @throws IllegalArgumentException * @throws InvocationTargetException */ private void splitStringIntoDisplayableParts(String s, Font compositeFont, Function fontConverter) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { Object result = GET_FONT2D_METHOD.invoke(null, compositeFont); if (result.getClass() != COMPOSITE_FONT_CLASS) { throw new IllegalArgumentException("Given font isn't a composite font."); } sb.setLength(0); stringParts.clear(); correspondingBaseFontsForParts.clear(); BaseFont baseFontFromCompositeFont = fontConverter.apply(compositeFont); BaseFont lastBaseFont = null; Object numSlotsResult = GET_NUM_SLOTS_METHOD.invoke(result); int numSlots = (Integer) numSlotsResult; for (int charIndex = 0; charIndex < s.length(); charIndex++) { char c = s.charAt(charIndex); boolean found = false; for (int slotIndex = 0; slotIndex < numSlots; slotIndex++) { Object phFont = GET_SLOT_FONT_METHOD.invoke(result, slotIndex); if (phFont == null) { continue; } Boolean canBeDysplayedByPhysicalFont = (Boolean) CAN_DYSPLAY_METHOD.invoke(phFont, c); if (canBeDysplayedByPhysicalFont) { Object fontNameResult = GET_FONT_NAME_METHOD.invoke(phFont, (Locale) null); Font font = new Font((String) fontNameResult, compositeFont.getStyle(), compositeFont.getSize()); BaseFont correspondingBaseFont = fontConverter.apply(font); if (correspondingBaseFont != null && correspondingBaseFont.charExists(c)) { if (sb.length() == 0) { correspondingBaseFontsForParts.add(correspondingBaseFont); lastBaseFont = correspondingBaseFont; } else if (!Objects.equals(lastBaseFont, correspondingBaseFont)) { stringParts.add(sb.toString()); sb.setLength(0); correspondingBaseFontsForParts.add(correspondingBaseFont); lastBaseFont = correspondingBaseFont; } sb.append(c); found = true; break; } } } if (!found) { if (sb.length() == 0) { correspondingBaseFontsForParts.add(baseFontFromCompositeFont); lastBaseFont = null; } else if (lastBaseFont != null) { stringParts.add(sb.toString()); sb.setLength(0); correspondingBaseFontsForParts.add(baseFontFromCompositeFont); lastBaseFont = null; } sb.append(c); } } stringParts.add(sb.toString()); sb.setLength(0); } @FunctionalInterface public interface DrawStringFunction { double drawString(String s, BaseFont basicFont, double x, double y); } } }