/*
 * Copyright (c) 1998, 2011, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.awt.windows;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.Transparency;

import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextLayout;

import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Line2D;

import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.WritableRaster;
import java.awt.image.ComponentSampleModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.SampleModel;

import sun.awt.image.ByteComponentRaster;
import sun.awt.image.BytePackedRaster;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;

import java.util.Arrays;

import sun.font.CharToGlyphMapper;
import sun.font.CompositeFont;
import sun.font.Font2D;
import sun.font.FontUtilities;
import sun.font.PhysicalFont;
import sun.font.TrueTypeFont;

import sun.print.PathGraphics;
import sun.print.ProxyGraphics2D;

class WPathGraphics extends PathGraphics {

    /**
     * For a drawing application the initial user space
     * resolution is 72dpi.
     */
    private static final int DEFAULT_USER_RES = 72;

    private static final float MIN_DEVICE_LINEWIDTH = 1.2f;
    private static final float MAX_THINLINE_INCHES = 0.014f;

    /* Note that preferGDITextLayout implies useGDITextLayout.
     * "prefer" is used to override cases where would otherwise
     * choose not to use it. Note that non-layout factors may
     * still mean that GDI cannot be used.
     */
    private static boolean useGDITextLayout = true;
    private static boolean preferGDITextLayout = false;
    static {
        String textLayoutStr =
            (String)java.security.AccessController.doPrivileged(
                   new sun.security.action.GetPropertyAction(
                         "sun.java2d.print.enableGDITextLayout"));

        if (textLayoutStr != null) {
            useGDITextLayout = Boolean.getBoolean(textLayoutStr);
            if (!useGDITextLayout) {
                if (textLayoutStr.equalsIgnoreCase("prefer")) {
                    useGDITextLayout = true;
                    preferGDITextLayout = true;
                }
            }
        }
    }

    WPathGraphics(Graphics2D graphics, PrinterJob printerJob,
                  Printable painter, PageFormat pageFormat, int pageIndex,
                  boolean canRedraw) {
        super(graphics, printerJob, painter, pageFormat, pageIndex, canRedraw);
    }

    /**
     * Creates a new <code>Graphics</code> object that is
     * a copy of this <code>Graphics</code> object.
     * @return     a new graphics context that is a copy of
     *                       this graphics context.
     * @since      JDK1.0
     */
    @Override
    public Graphics create() {

        return new WPathGraphics((Graphics2D) getDelegate().create(),
                                 getPrinterJob(),
                                 getPrintable(),
                                 getPageFormat(),
                                 getPageIndex(),
                                 canDoRedraws());
    }

    /**
     * Strokes the outline of a Shape using the settings of the current
     * graphics state.  The rendering attributes applied include the
     * clip, transform, paint or color, composite and stroke attributes.
     * @param s The shape to be drawn.
     * @see #setStroke
     * @see #setPaint
     * @see java.awt.Graphics#setColor
     * @see #transform
     * @see #setTransform
     * @see #clip
     * @see #setClip
     * @see #setComposite
     */
    @Override
    public void draw(Shape s) {

        Stroke stroke = getStroke();

        /* If the line being drawn is thinner than can be
         * rendered, then change the line width, stroke
         * the shape, and then set the line width back.
         * We can only do this for BasicStroke's.
         */
        if (stroke instanceof BasicStroke) {
            BasicStroke lineStroke;
            BasicStroke minLineStroke = null;
            float deviceLineWidth;
            float lineWidth;
            AffineTransform deviceTransform;
            Point2D.Float penSize;

            /* Get the requested line width in user space.
             */
            lineStroke = (BasicStroke) stroke;
            lineWidth = lineStroke.getLineWidth();
            penSize = new Point2D.Float(lineWidth, lineWidth);

            /* Compute the line width in device coordinates.
             * Work on a point in case there is asymetric scaling
             * between user and device space.
             * Take the absolute value in case there is negative
             * scaling in effect.
             */
            deviceTransform = getTransform();
            deviceTransform.deltaTransform(penSize, penSize);
            deviceLineWidth = Math.min(Math.abs(penSize.x),
                                       Math.abs(penSize.y));

            /* If the requested line is too thin then map our
             * minimum line width back to user space and set
             * a new BasicStroke.
             */
            if (deviceLineWidth < MIN_DEVICE_LINEWIDTH) {

                Point2D.Float minPenSize = new Point2D.Float(
                                                MIN_DEVICE_LINEWIDTH,
                                                MIN_DEVICE_LINEWIDTH);

                try {
                    AffineTransform inverse;
                    float minLineWidth;

                    /* Convert the minimum line width from device
                     * space to user space.
                     */
                    inverse = deviceTransform.createInverse();
                    inverse.deltaTransform(minPenSize, minPenSize);

                    minLineWidth = Math.max(Math.abs(minPenSize.x),
                                            Math.abs(minPenSize.y));

                    /* Use all of the parameters from the current
                     * stroke but change the line width to our
                     * calculated minimum.
                     */
                    minLineStroke = new BasicStroke(minLineWidth,
                                                    lineStroke.getEndCap(),
                                                    lineStroke.getLineJoin(),
                                                    lineStroke.getMiterLimit(),
                                                    lineStroke.getDashArray(),
                                                    lineStroke.getDashPhase());
                    setStroke(minLineStroke);

                } catch (NoninvertibleTransformException e) {
                    /* If we can't invert the matrix there is something
                     * very wrong so don't worry about the minor matter
                     * of a minimum line width.
                     */
                }
            }

            super.draw(s);

            /* If we changed the stroke, put back the old
             * stroke in order to maintain a minimum line
             * width.
             */
            if (minLineStroke != null) {
                setStroke(lineStroke);
            }

        /* The stroke in effect was not a BasicStroke so we
         * will not try to enforce a minimum line width.
         */
        } else {
            super.draw(s);
        }
    }

    /**
     * Draws the text given by the specified string, using this
     * graphics context's current font and color. The baseline of the
     * first character is at position (<i>x</i>,&nbsp;<i>y</i>) in this
     * graphics context's coordinate system.
     * @param       str      the string to be drawn.
     * @param       x        the <i>x</i> coordinate.
     * @param       y        the <i>y</i> coordinate.
     * @see         java.awt.Graphics#drawBytes
     * @see         java.awt.Graphics#drawChars
     * @since       JDK1.0
     */
    @Override
    public void drawString(String str, int x, int y) {
        drawString(str, (float) x, (float) y);
    }

    @Override
     public void drawString(String str, float x, float y) {
         drawString(str, x, y, getFont(), getFontRenderContext(), 0f);
     }

    /* A return value of 0 would mean font not available to GDI, or the
     * it can't be used for this string.
     * A return of 1 means it is suitable, including for composites.
     * We check that the transform in effect is doable with GDI, and that
     * this is a composite font AWT can handle, or a physical font GDI
     * can handle directly. Its possible that some strings may ultimately
     * fail the more stringent tests in drawString but this is rare and
     * also that method will always succeed, as if the font isn't available
     * it will use outlines via a superclass call. Also it is only called for
     * the default render context (as canDrawStringToWidth() will return
     * false. That is why it ignores the frc and width arguments.
     */
    @Override
    protected int platformFontCount(Font font, String str) {

        AffineTransform deviceTransform = getTransform();
        AffineTransform fontTransform = new AffineTransform(deviceTransform);
        fontTransform.concatenate(getFont().getTransform());
        int transformType = fontTransform.getType();

        /* Test if GDI can handle the transform */
        boolean directToGDI = ((transformType !=
                               AffineTransform.TYPE_GENERAL_TRANSFORM)
                               && ((transformType & AffineTransform.TYPE_FLIP)
                                   == 0));

        if (!directToGDI) {
            return 0;
        }

        /* Since all windows fonts are available, and the JRE fonts
         * are also registered. Only the Font.createFont() case is presently
         * unknown to GDI. Those can be registered too, although that
         * code does not exist yet, it can be added too, so we should not
         * fail that case. Just do a quick check whether its a TrueTypeFont
         * - ie not a Type1 font etc, and let drawString() resolve the rest.
         */
        Font2D font2D = FontUtilities.getFont2D(font);
        if (font2D instanceof CompositeFont ||
            font2D instanceof TrueTypeFont) {
            return 1;
        } else {
            return 0;
        }
    }

    private static boolean isXP() {
        String osVersion = System.getProperty("os.version");
        if (osVersion != null) {
            Float version = Float.valueOf(osVersion);
            return (version.floatValue() >= 5.1f);
        } else {
            return false;
        }
    }

    /* In case GDI doesn't handle shaping or BIDI consistently with
     * 2D's TextLayout, we can detect these cases and redelegate up to
     * be drawn via TextLayout, which in is rendered as runs of
     * GlyphVectors, to which we can assign positions for each glyph.
     */
    private boolean strNeedsTextLayout(String str, Font font) {
        char[] chars = str.toCharArray();
        boolean isComplex = FontUtilities.isComplexText(chars, 0, chars.length);
        if (!isComplex) {
            return false;
        } else if (!useGDITextLayout) {
            return true;
        } else {
            if (preferGDITextLayout ||
                (isXP() && FontUtilities.textLayoutIsCompatible(font))) {
                return false;
            } else {
                return true;
            }
        }
    }

    private int getAngle(Point2D.Double pt) {
        /* Get the rotation in 1/10'ths degree (as needed by Windows)
         * so that GDI can draw the text rotated.
         * This calculation is only valid for a uniform scale, no shearing.
         */
        double angle = Math.toDegrees(Math.atan2(pt.y, pt.x));
        if (angle < 0.0) {
            angle+= 360.0;
        }
        /* Windows specifies the rotation anti-clockwise from the x-axis
         * of the device, 2D specifies +ve rotation towards the y-axis
         * Since the 2D y-axis runs from top-to-bottom, windows angle of
         * rotation here is opposite than 2D's, so the rotation needed
         * needs to be recalculated in the opposite direction.
         */
        if (angle != 0.0) {
            angle = 360.0 - angle;
        }
        return (int)Math.round(angle * 10.0);
    }

    private float getAwScale(double scaleFactorX, double scaleFactorY) {

        float awScale = (float)(scaleFactorX/scaleFactorY);
        /* don't let rounding errors be interpreted as non-uniform scale */
        if (awScale > 0.999f && awScale < 1.001f) {
            awScale = 1.0f;
        }
        return awScale;
    }

    /**
     * Renders the text specified by the specified <code>String</code>,
     * using the current <code>Font</code> and <code>Paint</code> attributes
     * in the <code>Graphics2D</code> context.
     * The baseline of the first character is at position
     * (<i>x</i>,&nbsp;<i>y</i>) in the User Space.
     * The rendering attributes applied include the <code>Clip</code>,
     * <code>Transform</code>, <code>Paint</code>, <code>Font</code> and
     * <code>Composite</code> attributes. For characters in script systems
     * such as Hebrew and Arabic, the glyphs can be rendered from right to
     * left, in which case the coordinate supplied is the location of the
     * leftmost character on the baseline.
     * @param s the <code>String</code> to be rendered
     * @param x,&nbsp;y the coordinates where the <code>String</code>
     * should be rendered
     * @see #setPaint
     * @see java.awt.Graphics#setColor
     * @see java.awt.Graphics#setFont
     * @see #setTransform
     * @see #setComposite
     * @see #setClip
     */
    @Override
    public void drawString(String str, float x, float y,
                           Font font, FontRenderContext frc, float targetW) {
        if (str.length() == 0) {
            return;
        }

        if (WPrinterJob.shapeTextProp) {
            super.drawString(str, x, y, font, frc, targetW);
            return;
        }

        /* If the Font has layout attributes we need to delegate to TextLayout.
         * TextLayout renders text as GlyphVectors. We try to print those
         * using printer fonts - ie using Postscript text operators so
         * we may be reinvoked. In that case the "!printingGlyphVector" test
         * prevents us recursing and instead sends us into the body of the
         * method where we can safely ignore layout attributes as those
         * are already handled by TextLayout.
         * Similarly if layout is needed based on the text, then we
         * delegate to TextLayout if possible, or failing that we delegate
         * upwards to filled shapes.
         */
        boolean layoutNeeded = strNeedsTextLayout(str, font);
        if ((font.hasLayoutAttributes() || layoutNeeded)
            && !printingGlyphVector) {
            TextLayout layout = new TextLayout(str, font, frc);
            layout.draw(this, x, y);
            return;
        } else if (layoutNeeded) {
            super.drawString(str, x, y, font, frc, targetW);
            return;
        }

        AffineTransform deviceTransform = getTransform();
        AffineTransform fontTransform = new AffineTransform(deviceTransform);
        fontTransform.concatenate(font.getTransform());
        int transformType = fontTransform.getType();

        /* Use GDI for the text if the graphics transform is something
         * for which we can obtain a suitable GDI font.
         * A flip or shearing transform on the graphics or a transform
         * on the font force us to decompose the text into a shape.
         */
        boolean directToGDI = ((transformType !=
                               AffineTransform.TYPE_GENERAL_TRANSFORM)
                               && ((transformType & AffineTransform.TYPE_FLIP)
                                   == 0));

        WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
        try {
            wPrinterJob.setTextColor((Color)getPaint());
        } catch (ClassCastException e) { // peek should detect such paints.
            directToGDI = false;
        }

        if (!directToGDI) {
            super.drawString(str, x, y, font, frc, targetW);
            return;
        }

        /* Now we have checked everything is OK to go through GDI as text
         * with the exception of testing GDI can find and use the font. That
         * is handled in the textOut() call.
         */

        /* Compute the starting position of the string in
         * device space.
         */
        Point2D.Float userpos = new Point2D.Float(x, y);
        Point2D.Float devpos = new Point2D.Float();

        /* Already have the translate from the deviceTransform,
         * but the font may have a translation component too.
         */
        if (font.isTransformed()) {
            AffineTransform fontTx = font.getTransform();
            float translateX = (float)(fontTx.getTranslateX());
            float translateY = (float)(fontTx.getTranslateY());
            if (Math.abs(translateX) < 0.00001) translateX = 0f;
            if (Math.abs(translateY) < 0.00001) translateY = 0f;
            userpos.x += translateX; userpos.y += translateY;
        }
        deviceTransform.transform(userpos, devpos);

        if (getClip() != null) {
            deviceClip(getClip().getPathIterator(deviceTransform));
        }

        /* Get the font size in device coordinates.
         * The size needed is the font height scaled to device space.
         * Although we have already tested that there is no shear,
         * there may be a non-uniform scale, so the width of the font
         * does not scale equally with the height. That is handled
         * by specifying an 'average width' scale to GDI.
         */
        float fontSize = font.getSize2D();

        Point2D.Double pty = new Point2D.Double(0.0, 1.0);
        fontTransform.deltaTransform(pty, pty);
        double scaleFactorY = Math.sqrt(pty.x*pty.x+pty.y*pty.y);
        float scaledFontSizeY = (float)(fontSize * scaleFactorY);

        Point2D.Double ptx = new Point2D.Double(1.0, 0.0);
        fontTransform.deltaTransform(ptx, ptx);
        double scaleFactorX = Math.sqrt(ptx.x*ptx.x+ptx.y*ptx.y);
        float scaledFontSizeX = (float)(fontSize * scaleFactorX);

        float awScale = getAwScale(scaleFactorX, scaleFactorY);
        int iangle = getAngle(ptx);

        Font2D font2D = FontUtilities.getFont2D(font);
        if (font2D instanceof TrueTypeFont) {
            textOut(str, font, (TrueTypeFont)font2D, frc,
                    scaledFontSizeY, iangle, awScale,
                    deviceTransform, scaleFactorX,
                    x, y, devpos.x, devpos.y, targetW);
        } else if (font2D instanceof CompositeFont) {
            /* Composite fonts are made up of multiple fonts and each
             * substring that uses a particular component font needs to
             * be separately sent to GDI.
             * This works for standard composite fonts, alternate ones,
             * Fonts that are a physical font backed by a standard composite,
             * and with fallback fonts.
             */
            CompositeFont compFont = (CompositeFont)font2D;
            float userx = x, usery = y;
            float devx = devpos.x, devy = devpos.y;
            char[] chars = str.toCharArray();
            int len = chars.length;
            int[] glyphs = new int[len];
            compFont.getMapper().charsToGlyphs(len, chars, glyphs);

            int startChar = 0, endChar = 0, slot = 0;
            while (endChar < len) {

                startChar = endChar;
                slot = glyphs[startChar] >>> 24;

                while (endChar < len && ((glyphs[endChar] >>> 24) == slot)) {
                    endChar++;
                }
                String substr = new String(chars, startChar,endChar-startChar);
                PhysicalFont slotFont = compFont.getSlotFont(slot);
                textOut(substr, font, slotFont, frc,
                        scaledFontSizeY, iangle, awScale,
                        deviceTransform, scaleFactorX,
                        userx, usery, devx, devy, 0f);
                Rectangle2D bds = font.getStringBounds(substr, frc);
                float xAdvance = (float)bds.getWidth();
                userx += xAdvance;
                userpos.x += xAdvance;
                deviceTransform.transform(userpos, devpos);
            }
        } else {
            super.drawString(str, x, y, font, frc, targetW);
        }
    }

    /** return true if the Graphics instance can directly print
     * this glyphvector
     */
    @Override
    protected boolean printGlyphVector(GlyphVector gv, float x, float y) {
        /* We don't want to try to handle per-glyph transforms. GDI can't
         * handle per-glyph rotations, etc. There's no way to express it
         * in a single call, so just bail for this uncommon case.
         */
        if ((gv.getLayoutFlags() & GlyphVector.FLAG_HAS_TRANSFORMS) != 0) {
            return false;
        }

        AffineTransform deviceTransform = getTransform();
        AffineTransform fontTransform = new AffineTransform(deviceTransform);
        Font font = gv.getFont();
        fontTransform.concatenate(font.getTransform());
        int transformType = fontTransform.getType();

        /* Use GDI for the text if the graphics transform is something
         * for which we can obtain a suitable GDI font.
         * A flip or shearing transform on the graphics or a transform
         * on the font force us to decompose the text into a shape.
         */
        boolean directToGDI =
            ((transformType != AffineTransform.TYPE_GENERAL_TRANSFORM) &&
             ((transformType & AffineTransform.TYPE_FLIP) == 0));

        WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
        try {
            wPrinterJob.setTextColor((Color)getPaint());
        } catch (ClassCastException e) { // peek should detect such paints.
            directToGDI = false;
        }

        if (WPrinterJob.shapeTextProp || !directToGDI) {
            return false;
        }
        /* Compute the starting position of the string in
         * device space.
         */
        Point2D.Float userpos = new Point2D.Float(x, y);
        Point2D.Float devpos = new Point2D.Float();

        /* Already have the translate from the deviceTransform,
         * but the font may have a translation component too.
         */
        if (font.isTransformed()) {
            AffineTransform fontTx = font.getTransform();
            float translateX = (float)(fontTx.getTranslateX());
            float translateY = (float)(fontTx.getTranslateY());
            if (Math.abs(translateX) < 0.00001) translateX = 0f;
            if (Math.abs(translateY) < 0.00001) translateY = 0f;
            userpos.x += translateX; userpos.y += translateY;
        }
        deviceTransform.transform(userpos, devpos);

        if (getClip() != null) {
            deviceClip(getClip().getPathIterator(deviceTransform));
        }

        /* Get the font size in device coordinates.
         * The size needed is the font height scaled to device space.
         * Although we have already tested that there is no shear,
         * there may be a non-uniform scale, so the width of the font
         * does not scale equally with the height. That is handled
         * by specifying an 'average width' scale to GDI.
         */
        float fontSize = font.getSize2D();

        Point2D.Double pty = new Point2D.Double(0.0, 1.0);
        fontTransform.deltaTransform(pty, pty);
        double scaleFactorY = Math.sqrt(pty.x*pty.x+pty.y*pty.y);
        float scaledFontSizeY = (float)(fontSize * scaleFactorY);

        Point2D.Double pt = new Point2D.Double(1.0, 0.0);
        fontTransform.deltaTransform(pt, pt);
        double scaleFactorX = Math.sqrt(pt.x*pt.x+pt.y*pt.y);
        float scaledFontSizeX = (float)(fontSize * scaleFactorX);

        float awScale = getAwScale(scaleFactorX, scaleFactorY);
        int iangle = getAngle(pt);

        int numGlyphs = gv.getNumGlyphs();
        int[] glyphCodes = gv.getGlyphCodes(0, numGlyphs, null);
        float[] glyphPos = gv.getGlyphPositions(0, numGlyphs, null);

        /* layout replaces glyphs which have been combined away
         * with 0xfffe or 0xffff. These are supposed to be invisible
         * and we need to handle this here as GDI will interpret it
         * as a missing glyph. We'll do it here by compacting the
         * glyph codes array, but we have to do it in conjunction with
         * compacting the positions/advances arrays too AND updating
         * the number of glyphs ..
         * Note that since the slot number for composites is in the
         * significant byte we need to mask out that for comparison of
         * the invisible glyph.
         */
        int invisibleGlyphCnt = 0;
        for (int gc=0; gc<numGlyphs; gc++) {
            if ((glyphCodes[gc] & 0xffff) >=
                CharToGlyphMapper.INVISIBLE_GLYPHS) {
                invisibleGlyphCnt++;
            }
        }
        if (invisibleGlyphCnt > 0) {
            int visibleGlyphCnt = numGlyphs - invisibleGlyphCnt;
            int[] visibleGlyphCodes = new int[visibleGlyphCnt];
            float[] visiblePositions = new float[visibleGlyphCnt*2];
            int index = 0;
            for (int i=0; i<numGlyphs; i++) {
                if ((glyphCodes[i] & 0xffff)
                    < CharToGlyphMapper.INVISIBLE_GLYPHS) {
                    visibleGlyphCodes[index] = glyphCodes[i];
                    visiblePositions[index*2]   = glyphPos[i*2];
                    visiblePositions[index*2+1] = glyphPos[i*2+1];
                    index++;
                }
            }
            numGlyphs = visibleGlyphCnt;
            glyphCodes = visibleGlyphCodes;
            glyphPos = visiblePositions;
        }

        /* To get GDI to rotate glyphs we need to specify the angle
         * of rotation to GDI when creating the HFONT. This implicitly
         * also rotates the baseline, and this adjusts the X & Y advances
         * of the glyphs accordingly.
         * When we specify the advances, they are in device space, so
         * we don't want any further interpretation applied by GDI, but
         * since as noted the advances are interpreted in the HFONT's
         * coordinate space, our advances would be rotated again.
         * We don't have any way to tell GDI to rotate only the glyphs and
         * not the advances, so we need to account for this in the advances
         * we supply, by supplying unrotated advances.
         * Note that "iangle" is in the opposite direction to 2D's normal
         * direction of rotation, so this rotation inverts the
         * rotation element of the deviceTransform.
         */
        AffineTransform advanceTransform =
            new AffineTransform(deviceTransform);
        advanceTransform.rotate(iangle*Math.PI/1800.0);
        float[] glyphAdvPos = new float[glyphPos.length];

        advanceTransform.transform(glyphPos, 0,         //source
                                   glyphAdvPos, 0,      //destination
                                   glyphPos.length/2);  //num points

        Font2D font2D = FontUtilities.getFont2D(font);
        if (font2D instanceof TrueTypeFont) {
            String family = font2D.getFamilyName(null);
            int style = font.getStyle() | font2D.getStyle();
            if (!wPrinterJob.setFont(family, scaledFontSizeY, style,
                                     iangle, awScale)) {
                return false;
            }
            wPrinterJob.glyphsOut(glyphCodes, devpos.x, devpos.y, glyphAdvPos);

        } else if (font2D instanceof CompositeFont) {
            /* Composite fonts are made up of multiple fonts and each
             * substring that uses a particular component font needs to
             * be separately sent to GDI.
             * This works for standard composite fonts, alternate ones,
             * Fonts that are a physical font backed by a standard composite,
             * and with fallback fonts.
             */
            CompositeFont compFont = (CompositeFont)font2D;
            float userx = x, usery = y;
            float devx = devpos.x, devy = devpos.y;

            int start = 0, end = 0, slot = 0;
            while (end < numGlyphs) {

                start = end;
                slot = glyphCodes[start] >>> 24;

                while (end < numGlyphs && ((glyphCodes[end] >>> 24) == slot)) {
                    end++;
                }
                /* If we can't get the font, bail to outlines.
                 * But we should always be able to get all fonts for
                 * Composites, so this is unlikely, so any overstriking
                 * if only one slot is unavailable is not worth worrying
                 * about.
                 */
                PhysicalFont slotFont = compFont.getSlotFont(slot);
                if (!(slotFont instanceof TrueTypeFont)) {
                    return false;
                }
                String family = slotFont.getFamilyName(null);
                int style = font.getStyle() | slotFont.getStyle();
                if (!wPrinterJob.setFont(family, scaledFontSizeY, style,
                                         iangle, awScale)) {
                    return false;
                }

                int[] glyphs = Arrays.copyOfRange(glyphCodes, start, end);
                float[] posns = Arrays.copyOfRange(glyphAdvPos,
                                                   start*2, end*2);
                if (start != 0) {
                    Point2D.Float p =
                        new Point2D.Float(x+glyphPos[start*2],
                                          y+glyphPos[start*2+1]);
                    deviceTransform.transform(p, p);
                    devx = p.x;
                    devy = p.y;
                }
                wPrinterJob.glyphsOut(glyphs, devx, devy, posns);
            }
        } else {
            return false;
        }
        return true;
    }

    private void textOut(String str,
                          Font font, PhysicalFont font2D,
                          FontRenderContext frc,
                          float deviceSize, int rotation, float awScale,
                          AffineTransform deviceTransform,
                          double scaleFactorX,
                          float userx, float usery,
                          float devx, float devy, float targetW) {

         String family = font2D.getFamilyName(null);
         int style = font.getStyle() | font2D.getStyle();
         WPrinterJob wPrinterJob = (WPrinterJob)getPrinterJob();
         boolean setFont = wPrinterJob.setFont(family, deviceSize, style,
                                               rotation, awScale);
         if (!setFont) {
             super.drawString(str, userx, usery, font, frc, targetW);
             return;
         }

         float[] glyphPos = null;
         if (!okGDIMetrics(str, font, frc, scaleFactorX)) {
             /* If there is a 1:1 char->glyph mapping then char positions
              * are the same as glyph positions and we can tell GDI
              * where to place the glyphs.
              * On drawing we remove control chars so these need to be
              * removed now so the string and positions are the same length.
              * For other cases we need to pass glyph codes to GDI.
              */
             str = wPrinterJob.removeControlChars(str);
             char[] chars = str.toCharArray();
             int len = chars.length;
             GlyphVector gv = null;
             if (!FontUtilities.isComplexText(chars, 0, len)) {
                 gv = font.createGlyphVector(frc, str);
             }
             if (gv == null) {
                 super.drawString(str, userx, usery, font, frc, targetW);
                 return;
             }
             glyphPos = gv.getGlyphPositions(0, len, null);
             Point2D gvAdvPt = gv.getGlyphPosition(gv.getNumGlyphs());

             /* GDI advances must not include device space rotation.
              * See earlier comment in printGlyphVector() for details.
              */
             AffineTransform advanceTransform =
               new AffineTransform(deviceTransform);
             advanceTransform.rotate(rotation*Math.PI/1800.0);
             float[] glyphAdvPos = new float[glyphPos.length];

             advanceTransform.transform(glyphPos, 0,         //source
                                        glyphAdvPos, 0,      //destination
                                        glyphPos.length/2);  //num points
             glyphPos = glyphAdvPos;
         }
         wPrinterJob.textOut(str, devx, devy, glyphPos);
     }

     /* If 2D and GDI agree on the advance of the string we do not
      * need to explicitly assign glyph positions.
      * If we are to use the GDI advance, require it to agree with
      * JDK to a precision of <= 0.2% - ie 1 pixel in 500
      * discrepancy after rounding the 2D advance to the
      * nearest pixel and is greater than one pixel in total.
      * ie strings < 500 pixels in length will be OK so long
      * as they differ by only 1 pixel even though that is > 0.02%
      * The bounds from 2D are in user space so need to
      * be scaled to device space for comparison with GDI.
      * scaleX is the scale from user space to device space needed for this.
      */
     private boolean okGDIMetrics(String str, Font font,
                                  FontRenderContext frc, double scaleX) {

         Rectangle2D bds = font.getStringBounds(str, frc);
         double jdkAdvance = bds.getWidth();
         jdkAdvance = Math.round(jdkAdvance*scaleX);
         int gdiAdvance = ((WPrinterJob)getPrinterJob()).getGDIAdvance(str);
         if (jdkAdvance > 0 && gdiAdvance > 0) {
             double diff = Math.abs(gdiAdvance-jdkAdvance);
             double ratio = gdiAdvance/jdkAdvance;
             if (ratio < 1) {
                 ratio = 1/ratio;
             }
             return diff <= 1 || ratio < 1.002;
         }
         return true;
     }

    /**
     * The various <code>drawImage()</code> methods for
     * <code>WPathGraphics</code> are all decomposed
     * into an invocation of <code>drawImageToPlatform</code>.
     * The portion of the passed in image defined by
     * <code>srcX, srcY, srcWidth, and srcHeight</code>
     * is transformed by the supplied AffineTransform and
     * drawn using GDI to the printer context.
     *
     * @param   img     The image to be drawn.
     * @param   xform   Used to tranform the image before drawing.
     *                  This can be null.
     * @param   bgcolor This color is drawn where the image has transparent
     *                  pixels. If this parameter is null then the
     *                  pixels already in the destination should show
     *                  through.
     * @param   srcX    With srcY this defines the upper-left corner
     *                  of the portion of the image to be drawn.
     *
     * @param   srcY    With srcX this defines the upper-left corner
     *                  of the portion of the image to be drawn.
     * @param   srcWidth    The width of the portion of the image to
     *                      be drawn.
     * @param   srcHeight   The height of the portion of the image to
     *                      be drawn.
     * @param   handlingTransparency if being recursively called to
     *                    print opaque region of transparent image
     */
    protected boolean drawImageToPlatform(Image image, AffineTransform xform,
                                          Color bgcolor,
                                          int srcX, int srcY,
                                          int srcWidth, int srcHeight,
                                          boolean handlingTransparency) {

        BufferedImage img = getBufferedImage(image);
        if (img == null) {
            return true;
        }

        WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();

        /* The full transform to be applied to the image is the
         * caller's transform concatenated on to the transform
         * from user space to device space. If the caller didn't
         * supply a transform then we just act as if they passed
         * in the identify transform.
         */
        AffineTransform fullTransform = getTransform();
        if (xform == null) {
            xform = new AffineTransform();
        }
        fullTransform.concatenate(xform);

        /* Split the full transform into a pair of
         * transforms. The first transform holds effects
         * that GDI (under Win95) can not perform such
         * as rotation and shearing. The second transform
         * is setup to hold only the scaling effects.
         * These transforms are created such that a point,
         * p, in user space, when transformed by 'fullTransform'
         * lands in the same place as when it is transformed
         * by 'rotTransform' and then 'scaleTransform'.
         *
         * The entire image transformation is not in Java in order
         * to minimize the amount of memory needed in the VM. By
         * dividing the transform in two, we rotate and shear
         * the source image in its own space and only go to
         * the, usually, larger, device space when we ask
         * GDI to perform the final scaling.
         * Clamp this to the device scale for better quality printing.
         */
        double[] fullMatrix = new double[6];
        fullTransform.getMatrix(fullMatrix);

        /* Calculate the amount of scaling in the x
         * and y directions. This scaling is computed by
         * transforming a unit vector along each axis
         * and computing the resulting magnitude.
         * The computed values 'scaleX' and 'scaleY'
         * represent the amount of scaling GDI will be asked
         * to perform.
         */
        Point2D.Float unitVectorX = new Point2D.Float(1, 0);
        Point2D.Float unitVectorY = new Point2D.Float(0, 1);
        fullTransform.deltaTransform(unitVectorX, unitVectorX);
        fullTransform.deltaTransform(unitVectorY, unitVectorY);

        Point2D.Float origin = new Point2D.Float(0, 0);
        double scaleX = unitVectorX.distance(origin);
        double scaleY = unitVectorY.distance(origin);

        double devResX = wPrinterJob.getXRes();
        double devResY = wPrinterJob.getYRes();
        double devScaleX = devResX / DEFAULT_USER_RES;
        double devScaleY = devResY / DEFAULT_USER_RES;

        /* check if rotated or sheared */
        int transformType = fullTransform.getType();
        boolean clampScale = ((transformType &
                               (AffineTransform.TYPE_GENERAL_ROTATION |
                                AffineTransform.TYPE_GENERAL_TRANSFORM)) != 0);
        if (clampScale) {
            if (scaleX > devScaleX) scaleX = devScaleX;
            if (scaleY > devScaleY) scaleY = devScaleY;
        }

        /* We do not need to draw anything if either scaling
         * factor is zero.
         */
        if (scaleX != 0 && scaleY != 0) {

            /* Here's the transformation we will do with Java2D,
            */
            AffineTransform rotTransform = new AffineTransform(
                                        fullMatrix[0] / scaleX,  //m00
                                        fullMatrix[1] / scaleY,  //m10
                                        fullMatrix[2] / scaleX,  //m01
                                        fullMatrix[3] / scaleY,  //m11
                                        fullMatrix[4] / scaleX,  //m02
                                        fullMatrix[5] / scaleY); //m12

            /* The scale transform is not used directly: we instead
             * directly multiply by scaleX and scaleY.
             *
             * Conceptually here is what the scaleTransform is:
             *
             * AffineTransform scaleTransform = new AffineTransform(
             *                      scaleX,                     //m00
             *                      0,                          //m10
             *                      0,                          //m01
             *                      scaleY,                     //m11
             *                      0,                          //m02
             *                      0);                         //m12
             */

            /* Convert the image source's rectangle into the rotated
             * and sheared space. Once there, we calculate a rectangle
             * that encloses the resulting shape. It is this rectangle
             * which defines the size of the BufferedImage we need to
             * create to hold the transformed image.
             */
            Rectangle2D.Float srcRect = new Rectangle2D.Float(srcX, srcY,
                                                              srcWidth,
                                                              srcHeight);

            Shape rotShape = rotTransform.createTransformedShape(srcRect);
            Rectangle2D rotBounds = rotShape.getBounds2D();

            /* add a fudge factor as some fp precision problems have
             * been observed which caused pixels to be rounded down and
             * out of the image.
             */
            rotBounds.setRect(rotBounds.getX(), rotBounds.getY(),
                              rotBounds.getWidth()+0.001,
                              rotBounds.getHeight()+0.001);

            int boundsWidth = (int) rotBounds.getWidth();
            int boundsHeight = (int) rotBounds.getHeight();

            if (boundsWidth > 0 && boundsHeight > 0) {

                /* If the image has transparent or semi-transparent
                 * pixels then we'll have the application re-render
                 * the portion of the page covered by the image.
                 * The BufferedImage will be at the image's resolution
                 * to avoid wasting memory. By re-rendering this portion
                 * of a page all compositing is done by Java2D into
                 * the BufferedImage and then that image is copied to
                 * GDI.
                 * However several special cases can be handled otherwise:
                 * - bitmask transparency with a solid background colour
                 * - images which have transparency color models but no
                 * transparent pixels
                 * - images with bitmask transparency and an IndexColorModel
                 * (the common transparent GIF case) can be handled by
                 * rendering just the opaque pixels.
                 */
                boolean drawOpaque = true;
                if (!handlingTransparency && hasTransparentPixels(img)) {
                    drawOpaque = false;
                    if (isBitmaskTransparency(img)) {
                        if (bgcolor == null) {
                            if (drawBitmaskImage(img, xform, bgcolor,
                                                 srcX, srcY,
                                                 srcWidth, srcHeight)) {
                                // image drawn, just return.
                                return true;
                            }
                        } else if (bgcolor.getTransparency()
                                   == Transparency.OPAQUE) {
                            drawOpaque = true;
                        }
                    }
                    if (!canDoRedraws()) {
                        drawOpaque = true;
                    }
                } else {
                    // if there's no transparent pixels there's no need
                    // for a background colour. This can avoid edge artifacts
                    // in rotation cases.
                    bgcolor = null;
                }
                // if src region extends beyond the image, the "opaque" path
                // may blit b/g colour (including white) where it shoudn't.
                if ((srcX+srcWidth > img.getWidth(null) ||
                     srcY+srcHeight > img.getHeight(null))
                    && canDoRedraws()) {
                    drawOpaque = false;
                }
                if (drawOpaque == false) {

                    fullTransform.getMatrix(fullMatrix);
                    AffineTransform tx =
                        new AffineTransform(
                                            fullMatrix[0] / devScaleX,  //m00
                                            fullMatrix[1] / devScaleY,  //m10
                                            fullMatrix[2] / devScaleX,  //m01
                                            fullMatrix[3] / devScaleY,  //m11
                                            fullMatrix[4] / devScaleX,  //m02
                                            fullMatrix[5] / devScaleY); //m12

                    Rectangle2D.Float rect =
                        new Rectangle2D.Float(srcX, srcY, srcWidth, srcHeight);

                    Shape shape = fullTransform.createTransformedShape(rect);
                    // Region isn't user space because its potentially
                    // been rotated for landscape.
                    Rectangle2D region = shape.getBounds2D();

                    region.setRect(region.getX(), region.getY(),
                                   region.getWidth()+0.001,
                                   region.getHeight()+0.001);

                    // Try to limit the amount of memory used to 8Mb, so
                    // if at device resolution this exceeds a certain
                    // image size then scale down the region to fit in
                    // that memory, but never to less than 72 dpi.

                    int w = (int)region.getWidth();
                    int h = (int)region.getHeight();
                    int nbytes = w * h * 3;
                    int maxBytes = 8 * 1024 * 1024;
                    double origDpi = (devResX < devResY) ? devResX : devResY;
                    int dpi = (int)origDpi;
                    double scaleFactor = 1;

                    double maxSFX = w/(double)boundsWidth;
                    double maxSFY = h/(double)boundsHeight;
                    double maxSF = (maxSFX > maxSFY) ? maxSFY : maxSFX;
                    int minDpi = (int)(dpi/maxSF);
                    if (minDpi < DEFAULT_USER_RES) minDpi = DEFAULT_USER_RES;

                    while (nbytes > maxBytes && dpi > minDpi) {
                        scaleFactor *= 2;
                        dpi /= 2;
                        nbytes /= 4;
                    }
                    if (dpi < minDpi) {
                        scaleFactor = (origDpi / minDpi);
                    }

                    region.setRect(region.getX()/scaleFactor,
                                   region.getY()/scaleFactor,
                                   region.getWidth()/scaleFactor,
                                   region.getHeight()/scaleFactor);

                    /*
                     * We need to have the clip as part of the saved state,
                     * either directly, or all the components that are
                     * needed to reconstitute it (image source area,
                     * image transform and current graphics transform).
                     * The clip is described in user space, so we need to
                     * save the current graphics transform anyway so just
                     * save these two.
                     */
                    wPrinterJob.saveState(getTransform(), getClip(),
                                          region, scaleFactor, scaleFactor);
                    return true;
                /* The image can be rendered directly by GDI so we
                 * copy it into a BufferedImage (this takes care of
                 * ColorSpace and BufferedImageOp issues) and then
                 * send that to GDI.
                 */
                } else {
                    /* Create a buffered image big enough to hold the portion
                     * of the source image being printed.
                     * The image format will be 3BYTE_BGR for most cases
                     * except where we can represent the image as a 1, 4 or 8
                     * bits-per-pixel DIB.
                     */
                    int dibType = BufferedImage.TYPE_3BYTE_BGR;
                    IndexColorModel icm = null;

                    ColorModel cm = img.getColorModel();
                    int imgType = img.getType();
                    if (cm instanceof IndexColorModel &&
                        cm.getPixelSize() <= 8 &&
                        (imgType == BufferedImage.TYPE_BYTE_BINARY ||
                         imgType == BufferedImage.TYPE_BYTE_INDEXED)) {
                        icm = (IndexColorModel)cm;
                        dibType = imgType;
                        /* BYTE_BINARY may be 2 bpp which DIB can't handle.
                         * Convert this to 4bpp.
                         */
                        if (imgType == BufferedImage.TYPE_BYTE_BINARY &&
                            cm.getPixelSize() == 2) {

                            int[] rgbs = new int[16];
                            icm.getRGBs(rgbs);
                            boolean transparent =
                                icm.getTransparency() != Transparency.OPAQUE;
                            int transpixel = icm.getTransparentPixel();

                            icm = new IndexColorModel(4, 16,
                                                      rgbs, 0,
                                                      transparent, transpixel,
                                                      DataBuffer.TYPE_BYTE);
                        }
                    }

                    int iw = (int)rotBounds.getWidth();
                    int ih = (int)rotBounds.getHeight();
                    BufferedImage deepImage = null;
                    /* If there is no special transform needed (this is a
                     * simple BLIT) and dibType == img.getType() and we
                     * didn't create a new IndexColorModel AND the whole of
                     * the source image is being drawn (GDI can't handle a
                     * portion of the original source image) then we
                     * don't need to create this intermediate image - GDI
                     * can access the data from the original image.
                     * Since a subimage can be created by calling
                     * BufferedImage.getSubImage() that condition needs to
                     * be accounted for too. This implies inspecting the
                     * data buffer. In the end too many cases are not able
                     * to take advantage of this option until we can teach
                     * the native code to properly navigate the data buffer.
                     * There was a concern that since in native code since we
                     * need to DWORD align and flip to a bottom up DIB that
                     * the "original" image may get perturbed by this.
                     * But in fact we always malloc new memory for the aligned
                     * copy so this isn't a problem.
                     * This points out that we allocate two temporaries copies
                     * of the image : one in Java and one in native. If
                     * we can be smarter about not allocating this one when
                     * not needed, that would seem like a good thing to do,
                     * even if in many cases the ColorModels don't match and
                     * its needed.
                     * Until all of this is resolved newImage is always true.
                     */
                    boolean newImage = true;
                    if (newImage) {
                        if (icm == null) {
                            deepImage = new BufferedImage(iw, ih, dibType);
                        } else {
                            deepImage = new BufferedImage(iw, ih, dibType,icm);
                        }

                        /* Setup a Graphics2D on to the BufferedImage so that
                         * the source image when copied, lands within the
                         * image buffer.
                         */
                        Graphics2D imageGraphics = deepImage.createGraphics();
                        imageGraphics.clipRect(0, 0,
                                               deepImage.getWidth(),
                                               deepImage.getHeight());

                        imageGraphics.translate(-rotBounds.getX(),
                                                -rotBounds.getY());
                        imageGraphics.transform(rotTransform);

                        /* Fill the BufferedImage either with the caller
                         * supplied color, 'bgColor' or, if null, with white.
                         */
                        if (bgcolor == null) {
                            bgcolor = Color.white;
                        }

                        imageGraphics.drawImage(img,
                                                srcX, srcY,
                                                srcX + srcWidth,
                                                srcY + srcHeight,
                                                srcX, srcY,
                                                srcX + srcWidth,
                                                srcY + srcHeight,
                                                bgcolor, null);
                        imageGraphics.dispose();
                    } else {
                        deepImage = img;
                    }

                    /* Scale the bounding rectangle by the scale transform.
                     * Because the scaling transform has only x and y
                     * scaling components it is equivalent to multiply
                     * the x components of the bounding rectangle by
                     * the x scaling factor and to multiply the y components
                     * by the y scaling factor.
                     */
                    Rectangle2D.Float scaledBounds
                            = new Rectangle2D.Float(
                                    (float) (rotBounds.getX() * scaleX),
                                    (float) (rotBounds.getY() * scaleY),
                                    (float) (rotBounds.getWidth() * scaleX),
                                    (float) (rotBounds.getHeight() * scaleY));

                    /* Pull the raster data from the buffered image
                     * and pass it along to GDI.
                     */
                    WritableRaster raster = deepImage.getRaster();
                    byte[] data;
                    if (raster instanceof ByteComponentRaster) {
                        data = ((ByteComponentRaster)raster).getDataStorage();
                    } else if (raster instanceof BytePackedRaster) {
                        data = ((BytePackedRaster)raster).getDataStorage();
                    } else {
                        return false;
                    }

                    int bitsPerPixel = 24;
                    SampleModel sm = deepImage.getSampleModel();
                    if (sm instanceof ComponentSampleModel) {
                        ComponentSampleModel csm = (ComponentSampleModel)sm;
                        bitsPerPixel = csm.getPixelStride() * 8;
                    } else if (sm instanceof MultiPixelPackedSampleModel) {
                        MultiPixelPackedSampleModel mppsm =
                            (MultiPixelPackedSampleModel)sm;
                        bitsPerPixel = mppsm.getPixelBitStride();
                    } else {
                        if (icm != null) {
                            int diw = deepImage.getWidth();
                            int dih = deepImage.getHeight();
                            if (diw > 0 && dih > 0) {
                                bitsPerPixel = data.length*8/diw/dih;
                            }
                        }
                    }

                    /* Because the caller's image has been rotated
                     * and sheared into our BufferedImage and because
                     * we will be handing that BufferedImage directly to
                     * GDI, we need to set an additional clip. This clip
                     * makes sure that only parts of the BufferedImage
                     * that are also part of the caller's image are drawn.
                     */
                    Shape holdClip = getClip();
                    clip(xform.createTransformedShape(srcRect));
                    deviceClip(getClip().getPathIterator(getTransform()));

                    wPrinterJob.drawDIBImage
                        (data, scaledBounds.x, scaledBounds.y,
                         (float)Math.rint(scaledBounds.width+0.5),
                         (float)Math.rint(scaledBounds.height+0.5),
                         0f, 0f,
                         deepImage.getWidth(), deepImage.getHeight(),
                         bitsPerPixel, icm);

                    setClip(holdClip);
                }
            }
        }

        return true;
    }

    /**
     * Have the printing application redraw everything that falls
     * within the page bounds defined by <code>region</code>.
     */
    public void redrawRegion(Rectangle2D region, double scaleX, double scaleY,
                             Shape savedClip, AffineTransform savedTransform)
            throws PrinterException {

        WPrinterJob wPrinterJob = (WPrinterJob)getPrinterJob();
        Printable painter = getPrintable();
        PageFormat pageFormat = getPageFormat();
        int pageIndex = getPageIndex();

        /* Create a buffered image big enough to hold the portion
         * of the source image being printed.
         */
        BufferedImage deepImage = new BufferedImage(
                                        (int) region.getWidth(),
                                        (int) region.getHeight(),
                                        BufferedImage.TYPE_3BYTE_BGR);

        /* Get a graphics for the application to render into.
         * We initialize the buffer to white in order to
         * match the paper and then we shift the BufferedImage
         * so that it covers the area on the page where the
         * caller's Image will be drawn.
         */
        Graphics2D g = deepImage.createGraphics();
        ProxyGraphics2D proxy = new ProxyGraphics2D(g, wPrinterJob);
        proxy.setColor(Color.white);
        proxy.fillRect(0, 0, deepImage.getWidth(), deepImage.getHeight());
        proxy.clipRect(0, 0, deepImage.getWidth(), deepImage.getHeight());

        proxy.translate(-region.getX(), -region.getY());

        /* Calculate the resolution of the source image.
         */
        float sourceResX = (float)(wPrinterJob.getXRes() / scaleX);
        float sourceResY = (float)(wPrinterJob.getYRes() / scaleY);

        /* The application expects to see user space at 72 dpi.
         * so change user space from image source resolution to
         *  72 dpi.
         */
        proxy.scale(sourceResX / DEFAULT_USER_RES,
                    sourceResY / DEFAULT_USER_RES);

        proxy.translate(
            -wPrinterJob.getPhysicalPrintableX(pageFormat.getPaper())
               / wPrinterJob.getXRes() * DEFAULT_USER_RES,
            -wPrinterJob.getPhysicalPrintableY(pageFormat.getPaper())
               / wPrinterJob.getYRes() * DEFAULT_USER_RES);
        /* NB User space now has to be at 72 dpi for this calc to be correct */
        proxy.transform(new AffineTransform(getPageFormat().getMatrix()));
        proxy.setPaint(Color.black);

        painter.print(proxy, pageFormat, pageIndex);

        g.dispose();

        /* We need to set the device clip using saved information.
         * savedClip intersects the user clip with a clip that restricts
         * the GDI rendered area of our BufferedImage to that which
         * may correspond to a rotate or shear.
         * The saved device transform is needed as the current transform
         * is not likely to be the same.
         */
        deviceClip(savedClip.getPathIterator(savedTransform));

        /* Scale the bounding rectangle by the scale transform.
         * Because the scaling transform has only x and y
         * scaling components it is equivalent to multiplying
         * the x components of the bounding rectangle by
         * the x scaling factor and to multiplying the y components
         * by the y scaling factor.
         */
        Rectangle2D.Float scaledBounds
                = new Rectangle2D.Float(
                        (float) (region.getX() * scaleX),
                        (float) (region.getY() * scaleY),
                        (float) (region.getWidth() * scaleX),
                        (float) (region.getHeight() * scaleY));

        /* Pull the raster data from the buffered image
         * and pass it along to GDI.
         */
       ByteComponentRaster tile
                = (ByteComponentRaster)deepImage.getRaster();

        wPrinterJob.drawImage3ByteBGR(tile.getDataStorage(),
                    scaledBounds.x, scaledBounds.y,
                    scaledBounds.width,
                    scaledBounds.height,
                    0f, 0f,
                    deepImage.getWidth(), deepImage.getHeight());

    }

    /*
     * Fill the path defined by <code>pathIter</code>
     * with the specified color.
     * The path is provided in device coordinates.
     */
    protected void deviceFill(PathIterator pathIter, Color color) {

        WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();

        convertToWPath(pathIter);
        wPrinterJob.selectSolidBrush(color);
        wPrinterJob.fillPath();
    }

    /*
     * Set the printer device's clip to be the
     * path defined by <code>pathIter</code>
     * The path is provided in device coordinates.
     */
    protected void deviceClip(PathIterator pathIter) {

        WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();

        convertToWPath(pathIter);
        wPrinterJob.selectClipPath();
    }

    /**
     * Draw the bounding rectangle using transformed coordinates.
     */
     protected void deviceFrameRect(int x, int y, int width, int height,
                                     Color color) {

        AffineTransform deviceTransform = getTransform();

        /* check if rotated or sheared */
        int transformType = deviceTransform.getType();
        boolean usePath = ((transformType &
                           (AffineTransform.TYPE_GENERAL_ROTATION |
                            AffineTransform.TYPE_GENERAL_TRANSFORM)) != 0);

        if (usePath) {
            draw(new Rectangle2D.Float(x, y, width, height));
            return;
        }

        Stroke stroke = getStroke();

        if (stroke instanceof BasicStroke) {
            BasicStroke lineStroke = (BasicStroke) stroke;

            int endCap = lineStroke.getEndCap();
            int lineJoin = lineStroke.getLineJoin();


            /* check for default style and try to optimize it by
             * calling the frameRect native function instead of using paths.
             */
            if ((endCap == BasicStroke.CAP_SQUARE) &&
                (lineJoin == BasicStroke.JOIN_MITER) &&
                (lineStroke.getMiterLimit() ==10.0f)) {

                float lineWidth = lineStroke.getLineWidth();
                Point2D.Float penSize = new Point2D.Float(lineWidth,
                                                          lineWidth);

                deviceTransform.deltaTransform(penSize, penSize);
                float deviceLineWidth = Math.min(Math.abs(penSize.x),
                                                 Math.abs(penSize.y));

                /* transform upper left coordinate */
                Point2D.Float ul_pos = new Point2D.Float(x, y);
                deviceTransform.transform(ul_pos, ul_pos);

                /* transform lower right coordinate */
                Point2D.Float lr_pos = new Point2D.Float(x + width,
                                                         y + height);
                deviceTransform.transform(lr_pos, lr_pos);

                float w = (float) (lr_pos.getX() - ul_pos.getX());
                float h = (float)(lr_pos.getY() - ul_pos.getY());

                WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();

                /* use selectStylePen, if supported */
                if (wPrinterJob.selectStylePen(endCap, lineJoin,
                                           deviceLineWidth, color) == true)  {
                    wPrinterJob.frameRect((float)ul_pos.getX(),
                                          (float)ul_pos.getY(), w, h);
                }
                /* not supported, must be a Win 9x */
                else {

                    double lowerRes = Math.min(wPrinterJob.getXRes(),
                                               wPrinterJob.getYRes());

                    if ((deviceLineWidth/lowerRes) < MAX_THINLINE_INCHES) {
                        /* use the default pen styles for thin pens. */
                        wPrinterJob.selectPen(deviceLineWidth, color);
                        wPrinterJob.frameRect((float)ul_pos.getX(),
                                              (float)ul_pos.getY(), w, h);
                    }
                    else {
                        draw(new Rectangle2D.Float(x, y, width, height));
                    }
                }
            }
            else {
                draw(new Rectangle2D.Float(x, y, width, height));
            }
        }
     }


     /*
      * Fill the rectangle with specified color and using Windows'
      * GDI fillRect function.
      * Boundaries are determined by the given coordinates.
      */
    protected void deviceFillRect(int x, int y, int width, int height,
                                  Color color) {
        /*
         * Transform to device coordinates
         */
        AffineTransform deviceTransform = getTransform();

        /* check if rotated or sheared */
        int transformType = deviceTransform.getType();
        boolean usePath =  ((transformType &
                               (AffineTransform.TYPE_GENERAL_ROTATION |
                                AffineTransform.TYPE_GENERAL_TRANSFORM)) != 0);
        if (usePath) {
            fill(new Rectangle2D.Float(x, y, width, height));
            return;
        }

        Point2D.Float tlc_pos = new Point2D.Float(x, y);
        deviceTransform.transform(tlc_pos, tlc_pos);

        Point2D.Float brc_pos = new Point2D.Float(x+width, y+height);
        deviceTransform.transform(brc_pos, brc_pos);

        float deviceWidth = (float) (brc_pos.getX() - tlc_pos.getX());
        float deviceHeight = (float)(brc_pos.getY() - tlc_pos.getY());

        WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
        wPrinterJob.fillRect((float)tlc_pos.getX(), (float)tlc_pos.getY(),
                             deviceWidth, deviceHeight, color);
    }


    /**
     * Draw a line using a pen created using the specified color
     * and current stroke properties.
     */
    protected void deviceDrawLine(int xBegin, int yBegin, int xEnd, int yEnd,
                                  Color color) {
        Stroke stroke = getStroke();

        if (stroke instanceof BasicStroke) {
            BasicStroke lineStroke = (BasicStroke) stroke;

            if (lineStroke.getDashArray() != null) {
                draw(new Line2D.Float(xBegin, yBegin, xEnd, yEnd));
                return;
            }

            float lineWidth = lineStroke.getLineWidth();
            Point2D.Float penSize = new Point2D.Float(lineWidth, lineWidth);

            AffineTransform deviceTransform = getTransform();
            deviceTransform.deltaTransform(penSize, penSize);

            float deviceLineWidth = Math.min(Math.abs(penSize.x),
                                             Math.abs(penSize.y));

            Point2D.Float begin_pos = new Point2D.Float(xBegin, yBegin);
            deviceTransform.transform(begin_pos, begin_pos);

            Point2D.Float end_pos = new Point2D.Float(xEnd, yEnd);
            deviceTransform.transform(end_pos, end_pos);

            int endCap = lineStroke.getEndCap();
            int lineJoin = lineStroke.getLineJoin();

            /* check if it's a one-pixel line */
            if ((end_pos.getX() == begin_pos.getX())
                && (end_pos.getY() == begin_pos.getY())) {

                /* endCap other than Round will not print!
                 * due to Windows GDI limitation, force it to CAP_ROUND
                 */
                endCap = BasicStroke.CAP_ROUND;
            }


            WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();

            /* call native function that creates pen with style */
            if (wPrinterJob.selectStylePen(endCap, lineJoin,
                                           deviceLineWidth, color)) {
                wPrinterJob.moveTo((float)begin_pos.getX(),
                                   (float)begin_pos.getY());
                wPrinterJob.lineTo((float)end_pos.getX(),
                                   (float)end_pos.getY());
            }
            /* selectStylePen is not supported, must be Win 9X */
            else {

                /* let's see if we can use a a default pen
                 *  if it's round end (Windows' default style)
                 *  or it's vertical/horizontal
                 *  or stroke is too thin.
                 */
                double lowerRes = Math.min(wPrinterJob.getXRes(),
                                           wPrinterJob.getYRes());

                if ((endCap == BasicStroke.CAP_ROUND) ||
                 (((xBegin == xEnd) || (yBegin == yEnd)) &&
                 (deviceLineWidth/lowerRes < MAX_THINLINE_INCHES))) {

                    wPrinterJob.selectPen(deviceLineWidth, color);
                    wPrinterJob.moveTo((float)begin_pos.getX(),
                                       (float)begin_pos.getY());
                    wPrinterJob.lineTo((float)end_pos.getX(),
                                       (float)end_pos.getY());
                }
                else {
                    draw(new Line2D.Float(xBegin, yBegin, xEnd, yEnd));
                }
            }
        }
    }


    /**
     * Given a Java2D <code>PathIterator</code> instance,
     * this method translates that into a Window's path
     * in the printer device context.
     */
    private void convertToWPath(PathIterator pathIter) {

        float[] segment = new float[6];
        int segmentType;

        WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();

        /* Map the PathIterator's fill rule into the Window's
         * polygon fill rule.
         */
        int polyFillRule;
        if (pathIter.getWindingRule() == PathIterator.WIND_EVEN_ODD) {
            polyFillRule = WPrinterJob.POLYFILL_ALTERNATE;
        } else {
            polyFillRule = WPrinterJob.POLYFILL_WINDING;
        }
        wPrinterJob.setPolyFillMode(polyFillRule);

        wPrinterJob.beginPath();

        while (pathIter.isDone() == false) {
            segmentType = pathIter.currentSegment(segment);

            switch (segmentType) {
             case PathIterator.SEG_MOVETO:
                wPrinterJob.moveTo(segment[0], segment[1]);
                break;

             case PathIterator.SEG_LINETO:
                wPrinterJob.lineTo(segment[0], segment[1]);
                break;

            /* Convert the quad path to a bezier.
             */
             case PathIterator.SEG_QUADTO:
                int lastX = wPrinterJob.getPenX();
                int lastY = wPrinterJob.getPenY();
                float c1x = lastX + (segment[0] - lastX) * 2 / 3;
                float c1y = lastY + (segment[1] - lastY) * 2 / 3;
                float c2x = segment[2] - (segment[2] - segment[0]) * 2/ 3;
                float c2y = segment[3] - (segment[3] - segment[1]) * 2/ 3;
                wPrinterJob.polyBezierTo(c1x, c1y,
                                         c2x, c2y,
                                         segment[2], segment[3]);
                break;

             case PathIterator.SEG_CUBICTO:
                wPrinterJob.polyBezierTo(segment[0], segment[1],
                                         segment[2], segment[3],
                                         segment[4], segment[5]);
                break;

             case PathIterator.SEG_CLOSE:
                wPrinterJob.closeFigure();
                break;
            }


            pathIter.next();
        }

        wPrinterJob.endPath();

    }

}
