0N/A/*
3909N/A * Copyright (c) 1998, 2011, Oracle and/or its affiliates. All rights reserved.
0N/A * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
0N/A *
0N/A * This code is free software; you can redistribute it and/or modify it
0N/A * under the terms of the GNU General Public License version 2 only, as
2362N/A * published by the Free Software Foundation. Oracle designates this
0N/A * particular file as subject to the "Classpath" exception as provided
2362N/A * by Oracle in the LICENSE file that accompanied this code.
0N/A *
0N/A * This code is distributed in the hope that it will be useful, but WITHOUT
0N/A * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0N/A * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
0N/A * version 2 for more details (a copy is included in the LICENSE file that
0N/A * accompanied this code).
0N/A *
0N/A * You should have received a copy of the GNU General Public License version
0N/A * 2 along with this work; if not, write to the Free Software Foundation,
0N/A * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
0N/A *
2362N/A * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
2362N/A * or visit www.oracle.com if you need additional information or have any
2362N/A * questions.
0N/A */
0N/A
0N/Apackage sun.awt.windows;
0N/A
0N/Aimport java.awt.BasicStroke;
0N/Aimport java.awt.Color;
0N/Aimport java.awt.Font;
0N/Aimport java.awt.Graphics;
0N/Aimport java.awt.Graphics2D;
0N/Aimport java.awt.Image;
0N/Aimport java.awt.Shape;
0N/Aimport java.awt.Stroke;
0N/Aimport java.awt.Transparency;
0N/A
0N/Aimport java.awt.font.FontRenderContext;
0N/Aimport java.awt.font.GlyphVector;
0N/Aimport java.awt.font.TextLayout;
0N/A
0N/Aimport java.awt.geom.AffineTransform;
0N/Aimport java.awt.geom.NoninvertibleTransformException;
0N/Aimport java.awt.geom.PathIterator;
0N/Aimport java.awt.geom.Point2D;
0N/Aimport java.awt.geom.Rectangle2D;
0N/Aimport java.awt.geom.Line2D;
0N/A
0N/Aimport java.awt.image.BufferedImage;
0N/Aimport java.awt.image.ColorModel;
0N/Aimport java.awt.image.DataBuffer;
0N/Aimport java.awt.image.IndexColorModel;
0N/Aimport java.awt.image.WritableRaster;
3535N/Aimport java.awt.image.ComponentSampleModel;
3535N/Aimport java.awt.image.MultiPixelPackedSampleModel;
3535N/Aimport java.awt.image.SampleModel;
3535N/A
0N/Aimport sun.awt.image.ByteComponentRaster;
0N/Aimport sun.awt.image.BytePackedRaster;
0N/Aimport java.awt.print.PageFormat;
0N/Aimport java.awt.print.Printable;
0N/Aimport java.awt.print.PrinterException;
0N/Aimport java.awt.print.PrinterJob;
0N/A
0N/Aimport java.util.Arrays;
0N/A
0N/Aimport sun.font.CharToGlyphMapper;
0N/Aimport sun.font.CompositeFont;
0N/Aimport sun.font.Font2D;
1686N/Aimport sun.font.FontUtilities;
0N/Aimport sun.font.PhysicalFont;
0N/Aimport sun.font.TrueTypeFont;
0N/A
0N/Aimport sun.print.PathGraphics;
0N/Aimport sun.print.ProxyGraphics2D;
0N/A
0N/Aclass WPathGraphics extends PathGraphics {
0N/A
0N/A /**
0N/A * For a drawing application the initial user space
0N/A * resolution is 72dpi.
0N/A */
0N/A private static final int DEFAULT_USER_RES = 72;
0N/A
0N/A private static final float MIN_DEVICE_LINEWIDTH = 1.2f;
0N/A private static final float MAX_THINLINE_INCHES = 0.014f;
0N/A
0N/A /* Note that preferGDITextLayout implies useGDITextLayout.
0N/A * "prefer" is used to override cases where would otherwise
0N/A * choose not to use it. Note that non-layout factors may
0N/A * still mean that GDI cannot be used.
0N/A */
0N/A private static boolean useGDITextLayout = true;
0N/A private static boolean preferGDITextLayout = false;
0N/A static {
0N/A String textLayoutStr =
0N/A (String)java.security.AccessController.doPrivileged(
0N/A new sun.security.action.GetPropertyAction(
0N/A "sun.java2d.print.enableGDITextLayout"));
0N/A
0N/A if (textLayoutStr != null) {
0N/A useGDITextLayout = Boolean.getBoolean(textLayoutStr);
0N/A if (!useGDITextLayout) {
0N/A if (textLayoutStr.equalsIgnoreCase("prefer")) {
0N/A useGDITextLayout = true;
0N/A preferGDITextLayout = true;
0N/A }
0N/A }
0N/A }
0N/A }
0N/A
0N/A WPathGraphics(Graphics2D graphics, PrinterJob printerJob,
0N/A Printable painter, PageFormat pageFormat, int pageIndex,
0N/A boolean canRedraw) {
0N/A super(graphics, printerJob, painter, pageFormat, pageIndex, canRedraw);
0N/A }
0N/A
0N/A /**
0N/A * Creates a new <code>Graphics</code> object that is
0N/A * a copy of this <code>Graphics</code> object.
0N/A * @return a new graphics context that is a copy of
0N/A * this graphics context.
0N/A * @since JDK1.0
0N/A */
1686N/A @Override
0N/A public Graphics create() {
0N/A
0N/A return new WPathGraphics((Graphics2D) getDelegate().create(),
0N/A getPrinterJob(),
0N/A getPrintable(),
0N/A getPageFormat(),
0N/A getPageIndex(),
0N/A canDoRedraws());
0N/A }
0N/A
0N/A /**
0N/A * Strokes the outline of a Shape using the settings of the current
0N/A * graphics state. The rendering attributes applied include the
0N/A * clip, transform, paint or color, composite and stroke attributes.
0N/A * @param s The shape to be drawn.
0N/A * @see #setStroke
0N/A * @see #setPaint
0N/A * @see java.awt.Graphics#setColor
0N/A * @see #transform
0N/A * @see #setTransform
0N/A * @see #clip
0N/A * @see #setClip
0N/A * @see #setComposite
0N/A */
1686N/A @Override
0N/A public void draw(Shape s) {
0N/A
0N/A Stroke stroke = getStroke();
0N/A
0N/A /* If the line being drawn is thinner than can be
0N/A * rendered, then change the line width, stroke
0N/A * the shape, and then set the line width back.
0N/A * We can only do this for BasicStroke's.
0N/A */
0N/A if (stroke instanceof BasicStroke) {
0N/A BasicStroke lineStroke;
0N/A BasicStroke minLineStroke = null;
0N/A float deviceLineWidth;
0N/A float lineWidth;
0N/A AffineTransform deviceTransform;
0N/A Point2D.Float penSize;
0N/A
0N/A /* Get the requested line width in user space.
0N/A */
0N/A lineStroke = (BasicStroke) stroke;
0N/A lineWidth = lineStroke.getLineWidth();
0N/A penSize = new Point2D.Float(lineWidth, lineWidth);
0N/A
0N/A /* Compute the line width in device coordinates.
0N/A * Work on a point in case there is asymetric scaling
0N/A * between user and device space.
0N/A * Take the absolute value in case there is negative
0N/A * scaling in effect.
0N/A */
0N/A deviceTransform = getTransform();
0N/A deviceTransform.deltaTransform(penSize, penSize);
0N/A deviceLineWidth = Math.min(Math.abs(penSize.x),
0N/A Math.abs(penSize.y));
0N/A
0N/A /* If the requested line is too thin then map our
0N/A * minimum line width back to user space and set
0N/A * a new BasicStroke.
0N/A */
0N/A if (deviceLineWidth < MIN_DEVICE_LINEWIDTH) {
0N/A
0N/A Point2D.Float minPenSize = new Point2D.Float(
0N/A MIN_DEVICE_LINEWIDTH,
0N/A MIN_DEVICE_LINEWIDTH);
0N/A
0N/A try {
0N/A AffineTransform inverse;
0N/A float minLineWidth;
0N/A
0N/A /* Convert the minimum line width from device
0N/A * space to user space.
0N/A */
0N/A inverse = deviceTransform.createInverse();
0N/A inverse.deltaTransform(minPenSize, minPenSize);
0N/A
0N/A minLineWidth = Math.max(Math.abs(minPenSize.x),
0N/A Math.abs(minPenSize.y));
0N/A
0N/A /* Use all of the parameters from the current
0N/A * stroke but change the line width to our
0N/A * calculated minimum.
0N/A */
0N/A minLineStroke = new BasicStroke(minLineWidth,
0N/A lineStroke.getEndCap(),
0N/A lineStroke.getLineJoin(),
0N/A lineStroke.getMiterLimit(),
0N/A lineStroke.getDashArray(),
0N/A lineStroke.getDashPhase());
0N/A setStroke(minLineStroke);
0N/A
0N/A } catch (NoninvertibleTransformException e) {
0N/A /* If we can't invert the matrix there is something
0N/A * very wrong so don't worry about the minor matter
0N/A * of a minimum line width.
0N/A */
0N/A }
0N/A }
0N/A
0N/A super.draw(s);
0N/A
0N/A /* If we changed the stroke, put back the old
0N/A * stroke in order to maintain a minimum line
0N/A * width.
0N/A */
0N/A if (minLineStroke != null) {
0N/A setStroke(lineStroke);
0N/A }
0N/A
0N/A /* The stroke in effect was not a BasicStroke so we
0N/A * will not try to enforce a minimum line width.
0N/A */
0N/A } else {
0N/A super.draw(s);
0N/A }
0N/A }
0N/A
0N/A /**
0N/A * Draws the text given by the specified string, using this
0N/A * graphics context's current font and color. The baseline of the
0N/A * first character is at position (<i>x</i>,&nbsp;<i>y</i>) in this
0N/A * graphics context's coordinate system.
0N/A * @param str the string to be drawn.
0N/A * @param x the <i>x</i> coordinate.
0N/A * @param y the <i>y</i> coordinate.
0N/A * @see java.awt.Graphics#drawBytes
0N/A * @see java.awt.Graphics#drawChars
0N/A * @since JDK1.0
0N/A */
1686N/A @Override
0N/A public void drawString(String str, int x, int y) {
0N/A drawString(str, (float) x, (float) y);
0N/A }
0N/A
1686N/A @Override
0N/A public void drawString(String str, float x, float y) {
0N/A drawString(str, x, y, getFont(), getFontRenderContext(), 0f);
0N/A }
0N/A
0N/A /* A return value of 0 would mean font not available to GDI, or the
0N/A * it can't be used for this string.
0N/A * A return of 1 means it is suitable, including for composites.
0N/A * We check that the transform in effect is doable with GDI, and that
0N/A * this is a composite font AWT can handle, or a physical font GDI
0N/A * can handle directly. Its possible that some strings may ultimately
0N/A * fail the more stringent tests in drawString but this is rare and
0N/A * also that method will always succeed, as if the font isn't available
0N/A * it will use outlines via a superclass call. Also it is only called for
0N/A * the default render context (as canDrawStringToWidth() will return
0N/A * false. That is why it ignores the frc and width arguments.
0N/A */
1686N/A @Override
0N/A protected int platformFontCount(Font font, String str) {
0N/A
0N/A AffineTransform deviceTransform = getTransform();
0N/A AffineTransform fontTransform = new AffineTransform(deviceTransform);
0N/A fontTransform.concatenate(getFont().getTransform());
0N/A int transformType = fontTransform.getType();
0N/A
0N/A /* Test if GDI can handle the transform */
0N/A boolean directToGDI = ((transformType !=
0N/A AffineTransform.TYPE_GENERAL_TRANSFORM)
0N/A && ((transformType & AffineTransform.TYPE_FLIP)
0N/A == 0));
0N/A
0N/A if (!directToGDI) {
0N/A return 0;
0N/A }
0N/A
0N/A /* Since all windows fonts are available, and the JRE fonts
0N/A * are also registered. Only the Font.createFont() case is presently
0N/A * unknown to GDI. Those can be registered too, although that
0N/A * code does not exist yet, it can be added too, so we should not
0N/A * fail that case. Just do a quick check whether its a TrueTypeFont
0N/A * - ie not a Type1 font etc, and let drawString() resolve the rest.
0N/A */
1686N/A Font2D font2D = FontUtilities.getFont2D(font);
0N/A if (font2D instanceof CompositeFont ||
0N/A font2D instanceof TrueTypeFont) {
0N/A return 1;
0N/A } else {
0N/A return 0;
0N/A }
0N/A }
0N/A
0N/A private static boolean isXP() {
0N/A String osVersion = System.getProperty("os.version");
0N/A if (osVersion != null) {
0N/A Float version = Float.valueOf(osVersion);
0N/A return (version.floatValue() >= 5.1f);
0N/A } else {
0N/A return false;
0N/A }
0N/A }
0N/A
0N/A /* In case GDI doesn't handle shaping or BIDI consistently with
0N/A * 2D's TextLayout, we can detect these cases and redelegate up to
0N/A * be drawn via TextLayout, which in is rendered as runs of
0N/A * GlyphVectors, to which we can assign positions for each glyph.
0N/A */
0N/A private boolean strNeedsTextLayout(String str, Font font) {
0N/A char[] chars = str.toCharArray();
1686N/A boolean isComplex = FontUtilities.isComplexText(chars, 0, chars.length);
0N/A if (!isComplex) {
0N/A return false;
0N/A } else if (!useGDITextLayout) {
0N/A return true;
0N/A } else {
0N/A if (preferGDITextLayout ||
1686N/A (isXP() && FontUtilities.textLayoutIsCompatible(font))) {
0N/A return false;
0N/A } else {
0N/A return true;
0N/A }
0N/A }
0N/A }
0N/A
0N/A private int getAngle(Point2D.Double pt) {
0N/A /* Get the rotation in 1/10'ths degree (as needed by Windows)
0N/A * so that GDI can draw the text rotated.
0N/A * This calculation is only valid for a uniform scale, no shearing.
0N/A */
0N/A double angle = Math.toDegrees(Math.atan2(pt.y, pt.x));
0N/A if (angle < 0.0) {
0N/A angle+= 360.0;
0N/A }
0N/A /* Windows specifies the rotation anti-clockwise from the x-axis
0N/A * of the device, 2D specifies +ve rotation towards the y-axis
0N/A * Since the 2D y-axis runs from top-to-bottom, windows angle of
0N/A * rotation here is opposite than 2D's, so the rotation needed
0N/A * needs to be recalculated in the opposite direction.
0N/A */
0N/A if (angle != 0.0) {
0N/A angle = 360.0 - angle;
0N/A }
0N/A return (int)Math.round(angle * 10.0);
0N/A }
0N/A
0N/A private float getAwScale(double scaleFactorX, double scaleFactorY) {
0N/A
0N/A float awScale = (float)(scaleFactorX/scaleFactorY);
0N/A /* don't let rounding errors be interpreted as non-uniform scale */
0N/A if (awScale > 0.999f && awScale < 1.001f) {
0N/A awScale = 1.0f;
0N/A }
0N/A return awScale;
0N/A }
0N/A
0N/A /**
0N/A * Renders the text specified by the specified <code>String</code>,
0N/A * using the current <code>Font</code> and <code>Paint</code> attributes
0N/A * in the <code>Graphics2D</code> context.
0N/A * The baseline of the first character is at position
0N/A * (<i>x</i>,&nbsp;<i>y</i>) in the User Space.
0N/A * The rendering attributes applied include the <code>Clip</code>,
0N/A * <code>Transform</code>, <code>Paint</code>, <code>Font</code> and
0N/A * <code>Composite</code> attributes. For characters in script systems
0N/A * such as Hebrew and Arabic, the glyphs can be rendered from right to
0N/A * left, in which case the coordinate supplied is the location of the
0N/A * leftmost character on the baseline.
0N/A * @param s the <code>String</code> to be rendered
0N/A * @param x,&nbsp;y the coordinates where the <code>String</code>
0N/A * should be rendered
0N/A * @see #setPaint
0N/A * @see java.awt.Graphics#setColor
0N/A * @see java.awt.Graphics#setFont
0N/A * @see #setTransform
0N/A * @see #setComposite
0N/A * @see #setClip
0N/A */
1686N/A @Override
0N/A public void drawString(String str, float x, float y,
0N/A Font font, FontRenderContext frc, float targetW) {
0N/A if (str.length() == 0) {
0N/A return;
0N/A }
0N/A
0N/A if (WPrinterJob.shapeTextProp) {
0N/A super.drawString(str, x, y, font, frc, targetW);
0N/A return;
0N/A }
0N/A
0N/A /* If the Font has layout attributes we need to delegate to TextLayout.
0N/A * TextLayout renders text as GlyphVectors. We try to print those
0N/A * using printer fonts - ie using Postscript text operators so
0N/A * we may be reinvoked. In that case the "!printingGlyphVector" test
0N/A * prevents us recursing and instead sends us into the body of the
0N/A * method where we can safely ignore layout attributes as those
0N/A * are already handled by TextLayout.
0N/A * Similarly if layout is needed based on the text, then we
0N/A * delegate to TextLayout if possible, or failing that we delegate
0N/A * upwards to filled shapes.
0N/A */
0N/A boolean layoutNeeded = strNeedsTextLayout(str, font);
0N/A if ((font.hasLayoutAttributes() || layoutNeeded)
0N/A && !printingGlyphVector) {
0N/A TextLayout layout = new TextLayout(str, font, frc);
0N/A layout.draw(this, x, y);
0N/A return;
0N/A } else if (layoutNeeded) {
0N/A super.drawString(str, x, y, font, frc, targetW);
0N/A return;
0N/A }
0N/A
0N/A AffineTransform deviceTransform = getTransform();
0N/A AffineTransform fontTransform = new AffineTransform(deviceTransform);
0N/A fontTransform.concatenate(font.getTransform());
0N/A int transformType = fontTransform.getType();
0N/A
0N/A /* Use GDI for the text if the graphics transform is something
0N/A * for which we can obtain a suitable GDI font.
0N/A * A flip or shearing transform on the graphics or a transform
0N/A * on the font force us to decompose the text into a shape.
0N/A */
0N/A boolean directToGDI = ((transformType !=
0N/A AffineTransform.TYPE_GENERAL_TRANSFORM)
0N/A && ((transformType & AffineTransform.TYPE_FLIP)
0N/A == 0));
0N/A
0N/A WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
0N/A try {
0N/A wPrinterJob.setTextColor((Color)getPaint());
0N/A } catch (ClassCastException e) { // peek should detect such paints.
0N/A directToGDI = false;
0N/A }
0N/A
0N/A if (!directToGDI) {
0N/A super.drawString(str, x, y, font, frc, targetW);
0N/A return;
0N/A }
0N/A
0N/A /* Now we have checked everything is OK to go through GDI as text
0N/A * with the exception of testing GDI can find and use the font. That
0N/A * is handled in the textOut() call.
0N/A */
0N/A
0N/A /* Compute the starting position of the string in
0N/A * device space.
0N/A */
0N/A Point2D.Float userpos = new Point2D.Float(x, y);
0N/A Point2D.Float devpos = new Point2D.Float();
0N/A
0N/A /* Already have the translate from the deviceTransform,
0N/A * but the font may have a translation component too.
0N/A */
0N/A if (font.isTransformed()) {
0N/A AffineTransform fontTx = font.getTransform();
0N/A float translateX = (float)(fontTx.getTranslateX());
0N/A float translateY = (float)(fontTx.getTranslateY());
0N/A if (Math.abs(translateX) < 0.00001) translateX = 0f;
0N/A if (Math.abs(translateY) < 0.00001) translateY = 0f;
0N/A userpos.x += translateX; userpos.y += translateY;
0N/A }
0N/A deviceTransform.transform(userpos, devpos);
0N/A
0N/A if (getClip() != null) {
0N/A deviceClip(getClip().getPathIterator(deviceTransform));
0N/A }
0N/A
0N/A /* Get the font size in device coordinates.
0N/A * The size needed is the font height scaled to device space.
0N/A * Although we have already tested that there is no shear,
0N/A * there may be a non-uniform scale, so the width of the font
0N/A * does not scale equally with the height. That is handled
0N/A * by specifying an 'average width' scale to GDI.
0N/A */
0N/A float fontSize = font.getSize2D();
0N/A
0N/A Point2D.Double pty = new Point2D.Double(0.0, 1.0);
0N/A fontTransform.deltaTransform(pty, pty);
0N/A double scaleFactorY = Math.sqrt(pty.x*pty.x+pty.y*pty.y);
0N/A float scaledFontSizeY = (float)(fontSize * scaleFactorY);
0N/A
0N/A Point2D.Double ptx = new Point2D.Double(1.0, 0.0);
0N/A fontTransform.deltaTransform(ptx, ptx);
0N/A double scaleFactorX = Math.sqrt(ptx.x*ptx.x+ptx.y*ptx.y);
0N/A float scaledFontSizeX = (float)(fontSize * scaleFactorX);
0N/A
0N/A float awScale = getAwScale(scaleFactorX, scaleFactorY);
0N/A int iangle = getAngle(ptx);
0N/A
1686N/A Font2D font2D = FontUtilities.getFont2D(font);
0N/A if (font2D instanceof TrueTypeFont) {
0N/A textOut(str, font, (TrueTypeFont)font2D, frc,
0N/A scaledFontSizeY, iangle, awScale,
0N/A deviceTransform, scaleFactorX,
0N/A x, y, devpos.x, devpos.y, targetW);
0N/A } else if (font2D instanceof CompositeFont) {
0N/A /* Composite fonts are made up of multiple fonts and each
0N/A * substring that uses a particular component font needs to
0N/A * be separately sent to GDI.
0N/A * This works for standard composite fonts, alternate ones,
0N/A * Fonts that are a physical font backed by a standard composite,
0N/A * and with fallback fonts.
0N/A */
0N/A CompositeFont compFont = (CompositeFont)font2D;
0N/A float userx = x, usery = y;
0N/A float devx = devpos.x, devy = devpos.y;
0N/A char[] chars = str.toCharArray();
0N/A int len = chars.length;
0N/A int[] glyphs = new int[len];
0N/A compFont.getMapper().charsToGlyphs(len, chars, glyphs);
0N/A
0N/A int startChar = 0, endChar = 0, slot = 0;
0N/A while (endChar < len) {
0N/A
0N/A startChar = endChar;
0N/A slot = glyphs[startChar] >>> 24;
0N/A
0N/A while (endChar < len && ((glyphs[endChar] >>> 24) == slot)) {
0N/A endChar++;
0N/A }
0N/A String substr = new String(chars, startChar,endChar-startChar);
0N/A PhysicalFont slotFont = compFont.getSlotFont(slot);
0N/A textOut(substr, font, slotFont, frc,
0N/A scaledFontSizeY, iangle, awScale,
0N/A deviceTransform, scaleFactorX,
0N/A userx, usery, devx, devy, 0f);
0N/A Rectangle2D bds = font.getStringBounds(substr, frc);
0N/A float xAdvance = (float)bds.getWidth();
0N/A userx += xAdvance;
0N/A userpos.x += xAdvance;
0N/A deviceTransform.transform(userpos, devpos);
6110N/A devx = devpos.x;
6110N/A devy = devpos.y;
0N/A }
0N/A } else {
0N/A super.drawString(str, x, y, font, frc, targetW);
0N/A }
0N/A }
0N/A
0N/A /** return true if the Graphics instance can directly print
0N/A * this glyphvector
0N/A */
1686N/A @Override
0N/A protected boolean printGlyphVector(GlyphVector gv, float x, float y) {
0N/A /* We don't want to try to handle per-glyph transforms. GDI can't
0N/A * handle per-glyph rotations, etc. There's no way to express it
0N/A * in a single call, so just bail for this uncommon case.
0N/A */
0N/A if ((gv.getLayoutFlags() & GlyphVector.FLAG_HAS_TRANSFORMS) != 0) {
0N/A return false;
0N/A }
0N/A
0N/A AffineTransform deviceTransform = getTransform();
0N/A AffineTransform fontTransform = new AffineTransform(deviceTransform);
0N/A Font font = gv.getFont();
0N/A fontTransform.concatenate(font.getTransform());
0N/A int transformType = fontTransform.getType();
0N/A
0N/A /* Use GDI for the text if the graphics transform is something
0N/A * for which we can obtain a suitable GDI font.
0N/A * A flip or shearing transform on the graphics or a transform
0N/A * on the font force us to decompose the text into a shape.
0N/A */
0N/A boolean directToGDI =
0N/A ((transformType != AffineTransform.TYPE_GENERAL_TRANSFORM) &&
0N/A ((transformType & AffineTransform.TYPE_FLIP) == 0));
0N/A
0N/A WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
0N/A try {
0N/A wPrinterJob.setTextColor((Color)getPaint());
0N/A } catch (ClassCastException e) { // peek should detect such paints.
0N/A directToGDI = false;
0N/A }
0N/A
0N/A if (WPrinterJob.shapeTextProp || !directToGDI) {
0N/A return false;
0N/A }
0N/A /* Compute the starting position of the string in
0N/A * device space.
0N/A */
0N/A Point2D.Float userpos = new Point2D.Float(x, y);
0N/A Point2D.Float devpos = new Point2D.Float();
0N/A
0N/A /* Already have the translate from the deviceTransform,
0N/A * but the font may have a translation component too.
0N/A */
0N/A if (font.isTransformed()) {
0N/A AffineTransform fontTx = font.getTransform();
0N/A float translateX = (float)(fontTx.getTranslateX());
0N/A float translateY = (float)(fontTx.getTranslateY());
0N/A if (Math.abs(translateX) < 0.00001) translateX = 0f;
0N/A if (Math.abs(translateY) < 0.00001) translateY = 0f;
0N/A userpos.x += translateX; userpos.y += translateY;
0N/A }
0N/A deviceTransform.transform(userpos, devpos);
0N/A
0N/A if (getClip() != null) {
0N/A deviceClip(getClip().getPathIterator(deviceTransform));
0N/A }
0N/A
0N/A /* Get the font size in device coordinates.
0N/A * The size needed is the font height scaled to device space.
0N/A * Although we have already tested that there is no shear,
0N/A * there may be a non-uniform scale, so the width of the font
0N/A * does not scale equally with the height. That is handled
0N/A * by specifying an 'average width' scale to GDI.
0N/A */
0N/A float fontSize = font.getSize2D();
0N/A
0N/A Point2D.Double pty = new Point2D.Double(0.0, 1.0);
0N/A fontTransform.deltaTransform(pty, pty);
0N/A double scaleFactorY = Math.sqrt(pty.x*pty.x+pty.y*pty.y);
0N/A float scaledFontSizeY = (float)(fontSize * scaleFactorY);
0N/A
0N/A Point2D.Double pt = new Point2D.Double(1.0, 0.0);
0N/A fontTransform.deltaTransform(pt, pt);
0N/A double scaleFactorX = Math.sqrt(pt.x*pt.x+pt.y*pt.y);
0N/A float scaledFontSizeX = (float)(fontSize * scaleFactorX);
0N/A
0N/A float awScale = getAwScale(scaleFactorX, scaleFactorY);
0N/A int iangle = getAngle(pt);
0N/A
0N/A int numGlyphs = gv.getNumGlyphs();
0N/A int[] glyphCodes = gv.getGlyphCodes(0, numGlyphs, null);
0N/A float[] glyphPos = gv.getGlyphPositions(0, numGlyphs, null);
0N/A
0N/A /* layout replaces glyphs which have been combined away
0N/A * with 0xfffe or 0xffff. These are supposed to be invisible
0N/A * and we need to handle this here as GDI will interpret it
0N/A * as a missing glyph. We'll do it here by compacting the
0N/A * glyph codes array, but we have to do it in conjunction with
0N/A * compacting the positions/advances arrays too AND updating
0N/A * the number of glyphs ..
0N/A * Note that since the slot number for composites is in the
0N/A * significant byte we need to mask out that for comparison of
0N/A * the invisible glyph.
0N/A */
0N/A int invisibleGlyphCnt = 0;
0N/A for (int gc=0; gc<numGlyphs; gc++) {
0N/A if ((glyphCodes[gc] & 0xffff) >=
0N/A CharToGlyphMapper.INVISIBLE_GLYPHS) {
0N/A invisibleGlyphCnt++;
0N/A }
0N/A }
0N/A if (invisibleGlyphCnt > 0) {
0N/A int visibleGlyphCnt = numGlyphs - invisibleGlyphCnt;
0N/A int[] visibleGlyphCodes = new int[visibleGlyphCnt];
0N/A float[] visiblePositions = new float[visibleGlyphCnt*2];
0N/A int index = 0;
0N/A for (int i=0; i<numGlyphs; i++) {
0N/A if ((glyphCodes[i] & 0xffff)
0N/A < CharToGlyphMapper.INVISIBLE_GLYPHS) {
0N/A visibleGlyphCodes[index] = glyphCodes[i];
0N/A visiblePositions[index*2] = glyphPos[i*2];
0N/A visiblePositions[index*2+1] = glyphPos[i*2+1];
0N/A index++;
0N/A }
0N/A }
0N/A numGlyphs = visibleGlyphCnt;
0N/A glyphCodes = visibleGlyphCodes;
0N/A glyphPos = visiblePositions;
0N/A }
0N/A
0N/A /* To get GDI to rotate glyphs we need to specify the angle
0N/A * of rotation to GDI when creating the HFONT. This implicitly
0N/A * also rotates the baseline, and this adjusts the X & Y advances
0N/A * of the glyphs accordingly.
0N/A * When we specify the advances, they are in device space, so
0N/A * we don't want any further interpretation applied by GDI, but
0N/A * since as noted the advances are interpreted in the HFONT's
0N/A * coordinate space, our advances would be rotated again.
0N/A * We don't have any way to tell GDI to rotate only the glyphs and
0N/A * not the advances, so we need to account for this in the advances
0N/A * we supply, by supplying unrotated advances.
0N/A * Note that "iangle" is in the opposite direction to 2D's normal
0N/A * direction of rotation, so this rotation inverts the
0N/A * rotation element of the deviceTransform.
0N/A */
0N/A AffineTransform advanceTransform =
0N/A new AffineTransform(deviceTransform);
0N/A advanceTransform.rotate(iangle*Math.PI/1800.0);
0N/A float[] glyphAdvPos = new float[glyphPos.length];
0N/A
0N/A advanceTransform.transform(glyphPos, 0, //source
0N/A glyphAdvPos, 0, //destination
0N/A glyphPos.length/2); //num points
0N/A
1686N/A Font2D font2D = FontUtilities.getFont2D(font);
0N/A if (font2D instanceof TrueTypeFont) {
0N/A String family = font2D.getFamilyName(null);
0N/A int style = font.getStyle() | font2D.getStyle();
0N/A if (!wPrinterJob.setFont(family, scaledFontSizeY, style,
0N/A iangle, awScale)) {
0N/A return false;
0N/A }
0N/A wPrinterJob.glyphsOut(glyphCodes, devpos.x, devpos.y, glyphAdvPos);
0N/A
0N/A } else if (font2D instanceof CompositeFont) {
0N/A /* Composite fonts are made up of multiple fonts and each
0N/A * substring that uses a particular component font needs to
0N/A * be separately sent to GDI.
0N/A * This works for standard composite fonts, alternate ones,
0N/A * Fonts that are a physical font backed by a standard composite,
0N/A * and with fallback fonts.
0N/A */
0N/A CompositeFont compFont = (CompositeFont)font2D;
0N/A float userx = x, usery = y;
0N/A float devx = devpos.x, devy = devpos.y;
0N/A
0N/A int start = 0, end = 0, slot = 0;
0N/A while (end < numGlyphs) {
0N/A
0N/A start = end;
0N/A slot = glyphCodes[start] >>> 24;
0N/A
0N/A while (end < numGlyphs && ((glyphCodes[end] >>> 24) == slot)) {
0N/A end++;
0N/A }
0N/A /* If we can't get the font, bail to outlines.
0N/A * But we should always be able to get all fonts for
0N/A * Composites, so this is unlikely, so any overstriking
0N/A * if only one slot is unavailable is not worth worrying
0N/A * about.
0N/A */
0N/A PhysicalFont slotFont = compFont.getSlotFont(slot);
0N/A if (!(slotFont instanceof TrueTypeFont)) {
0N/A return false;
0N/A }
0N/A String family = slotFont.getFamilyName(null);
0N/A int style = font.getStyle() | slotFont.getStyle();
0N/A if (!wPrinterJob.setFont(family, scaledFontSizeY, style,
0N/A iangle, awScale)) {
0N/A return false;
0N/A }
0N/A
0N/A int[] glyphs = Arrays.copyOfRange(glyphCodes, start, end);
0N/A float[] posns = Arrays.copyOfRange(glyphAdvPos,
0N/A start*2, end*2);
0N/A if (start != 0) {
0N/A Point2D.Float p =
0N/A new Point2D.Float(x+glyphPos[start*2],
0N/A y+glyphPos[start*2+1]);
0N/A deviceTransform.transform(p, p);
0N/A devx = p.x;
0N/A devy = p.y;
0N/A }
0N/A wPrinterJob.glyphsOut(glyphs, devx, devy, posns);
0N/A }
0N/A } else {
0N/A return false;
0N/A }
0N/A return true;
0N/A }
0N/A
0N/A private void textOut(String str,
0N/A Font font, PhysicalFont font2D,
0N/A FontRenderContext frc,
0N/A float deviceSize, int rotation, float awScale,
0N/A AffineTransform deviceTransform,
0N/A double scaleFactorX,
0N/A float userx, float usery,
0N/A float devx, float devy, float targetW) {
0N/A
0N/A String family = font2D.getFamilyName(null);
0N/A int style = font.getStyle() | font2D.getStyle();
0N/A WPrinterJob wPrinterJob = (WPrinterJob)getPrinterJob();
0N/A boolean setFont = wPrinterJob.setFont(family, deviceSize, style,
0N/A rotation, awScale);
0N/A if (!setFont) {
0N/A super.drawString(str, userx, usery, font, frc, targetW);
0N/A return;
0N/A }
0N/A
0N/A float[] glyphPos = null;
0N/A if (!okGDIMetrics(str, font, frc, scaleFactorX)) {
0N/A /* If there is a 1:1 char->glyph mapping then char positions
0N/A * are the same as glyph positions and we can tell GDI
0N/A * where to place the glyphs.
0N/A * On drawing we remove control chars so these need to be
0N/A * removed now so the string and positions are the same length.
0N/A * For other cases we need to pass glyph codes to GDI.
0N/A */
0N/A str = wPrinterJob.removeControlChars(str);
0N/A char[] chars = str.toCharArray();
0N/A int len = chars.length;
0N/A GlyphVector gv = null;
1686N/A if (!FontUtilities.isComplexText(chars, 0, len)) {
0N/A gv = font.createGlyphVector(frc, str);
0N/A }
0N/A if (gv == null) {
0N/A super.drawString(str, userx, usery, font, frc, targetW);
0N/A return;
0N/A }
0N/A glyphPos = gv.getGlyphPositions(0, len, null);
0N/A Point2D gvAdvPt = gv.getGlyphPosition(gv.getNumGlyphs());
0N/A
0N/A /* GDI advances must not include device space rotation.
0N/A * See earlier comment in printGlyphVector() for details.
0N/A */
0N/A AffineTransform advanceTransform =
0N/A new AffineTransform(deviceTransform);
0N/A advanceTransform.rotate(rotation*Math.PI/1800.0);
0N/A float[] glyphAdvPos = new float[glyphPos.length];
0N/A
0N/A advanceTransform.transform(glyphPos, 0, //source
0N/A glyphAdvPos, 0, //destination
0N/A glyphPos.length/2); //num points
0N/A glyphPos = glyphAdvPos;
0N/A }
0N/A wPrinterJob.textOut(str, devx, devy, glyphPos);
0N/A }
0N/A
0N/A /* If 2D and GDI agree on the advance of the string we do not
0N/A * need to explicitly assign glyph positions.
0N/A * If we are to use the GDI advance, require it to agree with
0N/A * JDK to a precision of <= 0.2% - ie 1 pixel in 500
0N/A * discrepancy after rounding the 2D advance to the
0N/A * nearest pixel and is greater than one pixel in total.
0N/A * ie strings < 500 pixels in length will be OK so long
0N/A * as they differ by only 1 pixel even though that is > 0.02%
0N/A * The bounds from 2D are in user space so need to
0N/A * be scaled to device space for comparison with GDI.
0N/A * scaleX is the scale from user space to device space needed for this.
0N/A */
0N/A private boolean okGDIMetrics(String str, Font font,
0N/A FontRenderContext frc, double scaleX) {
0N/A
0N/A Rectangle2D bds = font.getStringBounds(str, frc);
0N/A double jdkAdvance = bds.getWidth();
0N/A jdkAdvance = Math.round(jdkAdvance*scaleX);
0N/A int gdiAdvance = ((WPrinterJob)getPrinterJob()).getGDIAdvance(str);
0N/A if (jdkAdvance > 0 && gdiAdvance > 0) {
0N/A double diff = Math.abs(gdiAdvance-jdkAdvance);
0N/A double ratio = gdiAdvance/jdkAdvance;
0N/A if (ratio < 1) {
0N/A ratio = 1/ratio;
0N/A }
0N/A return diff <= 1 || ratio < 1.002;
0N/A }
0N/A return true;
0N/A }
0N/A
0N/A /**
0N/A * The various <code>drawImage()</code> methods for
0N/A * <code>WPathGraphics</code> are all decomposed
0N/A * into an invocation of <code>drawImageToPlatform</code>.
0N/A * The portion of the passed in image defined by
0N/A * <code>srcX, srcY, srcWidth, and srcHeight</code>
0N/A * is transformed by the supplied AffineTransform and
0N/A * drawn using GDI to the printer context.
0N/A *
0N/A * @param img The image to be drawn.
0N/A * @param xform Used to tranform the image before drawing.
0N/A * This can be null.
0N/A * @param bgcolor This color is drawn where the image has transparent
0N/A * pixels. If this parameter is null then the
0N/A * pixels already in the destination should show
0N/A * through.
0N/A * @param srcX With srcY this defines the upper-left corner
0N/A * of the portion of the image to be drawn.
0N/A *
0N/A * @param srcY With srcX this defines the upper-left corner
0N/A * of the portion of the image to be drawn.
0N/A * @param srcWidth The width of the portion of the image to
0N/A * be drawn.
0N/A * @param srcHeight The height of the portion of the image to
0N/A * be drawn.
0N/A * @param handlingTransparency if being recursively called to
0N/A * print opaque region of transparent image
0N/A */
0N/A protected boolean drawImageToPlatform(Image image, AffineTransform xform,
0N/A Color bgcolor,
0N/A int srcX, int srcY,
0N/A int srcWidth, int srcHeight,
0N/A boolean handlingTransparency) {
0N/A
0N/A BufferedImage img = getBufferedImage(image);
0N/A if (img == null) {
0N/A return true;
0N/A }
0N/A
0N/A WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
0N/A
0N/A /* The full transform to be applied to the image is the
0N/A * caller's transform concatenated on to the transform
0N/A * from user space to device space. If the caller didn't
0N/A * supply a transform then we just act as if they passed
0N/A * in the identify transform.
0N/A */
0N/A AffineTransform fullTransform = getTransform();
0N/A if (xform == null) {
0N/A xform = new AffineTransform();
0N/A }
0N/A fullTransform.concatenate(xform);
0N/A
0N/A /* Split the full transform into a pair of
0N/A * transforms. The first transform holds effects
0N/A * that GDI (under Win95) can not perform such
0N/A * as rotation and shearing. The second transform
0N/A * is setup to hold only the scaling effects.
0N/A * These transforms are created such that a point,
0N/A * p, in user space, when transformed by 'fullTransform'
0N/A * lands in the same place as when it is transformed
0N/A * by 'rotTransform' and then 'scaleTransform'.
0N/A *
0N/A * The entire image transformation is not in Java in order
0N/A * to minimize the amount of memory needed in the VM. By
0N/A * dividing the transform in two, we rotate and shear
0N/A * the source image in its own space and only go to
0N/A * the, usually, larger, device space when we ask
0N/A * GDI to perform the final scaling.
0N/A * Clamp this to the device scale for better quality printing.
0N/A */
0N/A double[] fullMatrix = new double[6];
0N/A fullTransform.getMatrix(fullMatrix);
0N/A
0N/A /* Calculate the amount of scaling in the x
0N/A * and y directions. This scaling is computed by
0N/A * transforming a unit vector along each axis
0N/A * and computing the resulting magnitude.
0N/A * The computed values 'scaleX' and 'scaleY'
0N/A * represent the amount of scaling GDI will be asked
0N/A * to perform.
0N/A */
0N/A Point2D.Float unitVectorX = new Point2D.Float(1, 0);
0N/A Point2D.Float unitVectorY = new Point2D.Float(0, 1);
0N/A fullTransform.deltaTransform(unitVectorX, unitVectorX);
0N/A fullTransform.deltaTransform(unitVectorY, unitVectorY);
0N/A
0N/A Point2D.Float origin = new Point2D.Float(0, 0);
0N/A double scaleX = unitVectorX.distance(origin);
0N/A double scaleY = unitVectorY.distance(origin);
0N/A
0N/A double devResX = wPrinterJob.getXRes();
0N/A double devResY = wPrinterJob.getYRes();
0N/A double devScaleX = devResX / DEFAULT_USER_RES;
0N/A double devScaleY = devResY / DEFAULT_USER_RES;
304N/A
304N/A /* check if rotated or sheared */
304N/A int transformType = fullTransform.getType();
304N/A boolean clampScale = ((transformType &
304N/A (AffineTransform.TYPE_GENERAL_ROTATION |
304N/A AffineTransform.TYPE_GENERAL_TRANSFORM)) != 0);
304N/A if (clampScale) {
304N/A if (scaleX > devScaleX) scaleX = devScaleX;
304N/A if (scaleY > devScaleY) scaleY = devScaleY;
304N/A }
0N/A
0N/A /* We do not need to draw anything if either scaling
0N/A * factor is zero.
0N/A */
0N/A if (scaleX != 0 && scaleY != 0) {
0N/A
0N/A /* Here's the transformation we will do with Java2D,
0N/A */
0N/A AffineTransform rotTransform = new AffineTransform(
0N/A fullMatrix[0] / scaleX, //m00
0N/A fullMatrix[1] / scaleY, //m10
0N/A fullMatrix[2] / scaleX, //m01
0N/A fullMatrix[3] / scaleY, //m11
0N/A fullMatrix[4] / scaleX, //m02
0N/A fullMatrix[5] / scaleY); //m12
0N/A
0N/A /* The scale transform is not used directly: we instead
0N/A * directly multiply by scaleX and scaleY.
0N/A *
0N/A * Conceptually here is what the scaleTransform is:
0N/A *
0N/A * AffineTransform scaleTransform = new AffineTransform(
0N/A * scaleX, //m00
0N/A * 0, //m10
0N/A * 0, //m01
0N/A * scaleY, //m11
0N/A * 0, //m02
0N/A * 0); //m12
0N/A */
0N/A
0N/A /* Convert the image source's rectangle into the rotated
0N/A * and sheared space. Once there, we calculate a rectangle
0N/A * that encloses the resulting shape. It is this rectangle
0N/A * which defines the size of the BufferedImage we need to
0N/A * create to hold the transformed image.
0N/A */
0N/A Rectangle2D.Float srcRect = new Rectangle2D.Float(srcX, srcY,
0N/A srcWidth,
0N/A srcHeight);
0N/A
0N/A Shape rotShape = rotTransform.createTransformedShape(srcRect);
0N/A Rectangle2D rotBounds = rotShape.getBounds2D();
0N/A
0N/A /* add a fudge factor as some fp precision problems have
0N/A * been observed which caused pixels to be rounded down and
0N/A * out of the image.
0N/A */
0N/A rotBounds.setRect(rotBounds.getX(), rotBounds.getY(),
0N/A rotBounds.getWidth()+0.001,
0N/A rotBounds.getHeight()+0.001);
0N/A
0N/A int boundsWidth = (int) rotBounds.getWidth();
0N/A int boundsHeight = (int) rotBounds.getHeight();
0N/A
0N/A if (boundsWidth > 0 && boundsHeight > 0) {
0N/A
0N/A /* If the image has transparent or semi-transparent
0N/A * pixels then we'll have the application re-render
0N/A * the portion of the page covered by the image.
0N/A * The BufferedImage will be at the image's resolution
0N/A * to avoid wasting memory. By re-rendering this portion
0N/A * of a page all compositing is done by Java2D into
0N/A * the BufferedImage and then that image is copied to
0N/A * GDI.
0N/A * However several special cases can be handled otherwise:
0N/A * - bitmask transparency with a solid background colour
0N/A * - images which have transparency color models but no
0N/A * transparent pixels
0N/A * - images with bitmask transparency and an IndexColorModel
0N/A * (the common transparent GIF case) can be handled by
0N/A * rendering just the opaque pixels.
0N/A */
0N/A boolean drawOpaque = true;
0N/A if (!handlingTransparency && hasTransparentPixels(img)) {
0N/A drawOpaque = false;
0N/A if (isBitmaskTransparency(img)) {
0N/A if (bgcolor == null) {
0N/A if (drawBitmaskImage(img, xform, bgcolor,
0N/A srcX, srcY,
0N/A srcWidth, srcHeight)) {
0N/A // image drawn, just return.
0N/A return true;
0N/A }
0N/A } else if (bgcolor.getTransparency()
0N/A == Transparency.OPAQUE) {
0N/A drawOpaque = true;
0N/A }
0N/A }
0N/A if (!canDoRedraws()) {
0N/A drawOpaque = true;
0N/A }
0N/A } else {
0N/A // if there's no transparent pixels there's no need
0N/A // for a background colour. This can avoid edge artifacts
0N/A // in rotation cases.
0N/A bgcolor = null;
0N/A }
0N/A // if src region extends beyond the image, the "opaque" path
0N/A // may blit b/g colour (including white) where it shoudn't.
0N/A if ((srcX+srcWidth > img.getWidth(null) ||
0N/A srcY+srcHeight > img.getHeight(null))
0N/A && canDoRedraws()) {
0N/A drawOpaque = false;
0N/A }
0N/A if (drawOpaque == false) {
0N/A
0N/A fullTransform.getMatrix(fullMatrix);
0N/A AffineTransform tx =
0N/A new AffineTransform(
0N/A fullMatrix[0] / devScaleX, //m00
0N/A fullMatrix[1] / devScaleY, //m10
0N/A fullMatrix[2] / devScaleX, //m01
0N/A fullMatrix[3] / devScaleY, //m11
0N/A fullMatrix[4] / devScaleX, //m02
0N/A fullMatrix[5] / devScaleY); //m12
0N/A
0N/A Rectangle2D.Float rect =
0N/A new Rectangle2D.Float(srcX, srcY, srcWidth, srcHeight);
0N/A
0N/A Shape shape = fullTransform.createTransformedShape(rect);
0N/A // Region isn't user space because its potentially
0N/A // been rotated for landscape.
0N/A Rectangle2D region = shape.getBounds2D();
0N/A
0N/A region.setRect(region.getX(), region.getY(),
0N/A region.getWidth()+0.001,
0N/A region.getHeight()+0.001);
0N/A
0N/A // Try to limit the amount of memory used to 8Mb, so
0N/A // if at device resolution this exceeds a certain
0N/A // image size then scale down the region to fit in
0N/A // that memory, but never to less than 72 dpi.
0N/A
0N/A int w = (int)region.getWidth();
0N/A int h = (int)region.getHeight();
0N/A int nbytes = w * h * 3;
0N/A int maxBytes = 8 * 1024 * 1024;
0N/A double origDpi = (devResX < devResY) ? devResX : devResY;
0N/A int dpi = (int)origDpi;
0N/A double scaleFactor = 1;
0N/A
0N/A double maxSFX = w/(double)boundsWidth;
0N/A double maxSFY = h/(double)boundsHeight;
0N/A double maxSF = (maxSFX > maxSFY) ? maxSFY : maxSFX;
0N/A int minDpi = (int)(dpi/maxSF);
0N/A if (minDpi < DEFAULT_USER_RES) minDpi = DEFAULT_USER_RES;
0N/A
0N/A while (nbytes > maxBytes && dpi > minDpi) {
0N/A scaleFactor *= 2;
0N/A dpi /= 2;
0N/A nbytes /= 4;
0N/A }
0N/A if (dpi < minDpi) {
0N/A scaleFactor = (origDpi / minDpi);
0N/A }
0N/A
0N/A region.setRect(region.getX()/scaleFactor,
0N/A region.getY()/scaleFactor,
0N/A region.getWidth()/scaleFactor,
0N/A region.getHeight()/scaleFactor);
0N/A
0N/A /*
0N/A * We need to have the clip as part of the saved state,
0N/A * either directly, or all the components that are
0N/A * needed to reconstitute it (image source area,
0N/A * image transform and current graphics transform).
0N/A * The clip is described in user space, so we need to
0N/A * save the current graphics transform anyway so just
0N/A * save these two.
0N/A */
0N/A wPrinterJob.saveState(getTransform(), getClip(),
0N/A region, scaleFactor, scaleFactor);
0N/A return true;
0N/A /* The image can be rendered directly by GDI so we
0N/A * copy it into a BufferedImage (this takes care of
0N/A * ColorSpace and BufferedImageOp issues) and then
0N/A * send that to GDI.
0N/A */
0N/A } else {
0N/A /* Create a buffered image big enough to hold the portion
0N/A * of the source image being printed.
0N/A * The image format will be 3BYTE_BGR for most cases
0N/A * except where we can represent the image as a 1, 4 or 8
0N/A * bits-per-pixel DIB.
0N/A */
0N/A int dibType = BufferedImage.TYPE_3BYTE_BGR;
0N/A IndexColorModel icm = null;
0N/A
0N/A ColorModel cm = img.getColorModel();
0N/A int imgType = img.getType();
0N/A if (cm instanceof IndexColorModel &&
0N/A cm.getPixelSize() <= 8 &&
0N/A (imgType == BufferedImage.TYPE_BYTE_BINARY ||
0N/A imgType == BufferedImage.TYPE_BYTE_INDEXED)) {
0N/A icm = (IndexColorModel)cm;
0N/A dibType = imgType;
0N/A /* BYTE_BINARY may be 2 bpp which DIB can't handle.
0N/A * Convert this to 4bpp.
0N/A */
0N/A if (imgType == BufferedImage.TYPE_BYTE_BINARY &&
0N/A cm.getPixelSize() == 2) {
0N/A
0N/A int[] rgbs = new int[16];
0N/A icm.getRGBs(rgbs);
0N/A boolean transparent =
0N/A icm.getTransparency() != Transparency.OPAQUE;
0N/A int transpixel = icm.getTransparentPixel();
0N/A
0N/A icm = new IndexColorModel(4, 16,
0N/A rgbs, 0,
0N/A transparent, transpixel,
0N/A DataBuffer.TYPE_BYTE);
0N/A }
0N/A }
0N/A
0N/A int iw = (int)rotBounds.getWidth();
0N/A int ih = (int)rotBounds.getHeight();
0N/A BufferedImage deepImage = null;
0N/A /* If there is no special transform needed (this is a
0N/A * simple BLIT) and dibType == img.getType() and we
0N/A * didn't create a new IndexColorModel AND the whole of
0N/A * the source image is being drawn (GDI can't handle a
0N/A * portion of the original source image) then we
0N/A * don't need to create this intermediate image - GDI
0N/A * can access the data from the original image.
0N/A * Since a subimage can be created by calling
0N/A * BufferedImage.getSubImage() that condition needs to
0N/A * be accounted for too. This implies inspecting the
0N/A * data buffer. In the end too many cases are not able
0N/A * to take advantage of this option until we can teach
0N/A * the native code to properly navigate the data buffer.
0N/A * There was a concern that since in native code since we
0N/A * need to DWORD align and flip to a bottom up DIB that
0N/A * the "original" image may get perturbed by this.
0N/A * But in fact we always malloc new memory for the aligned
0N/A * copy so this isn't a problem.
0N/A * This points out that we allocate two temporaries copies
0N/A * of the image : one in Java and one in native. If
0N/A * we can be smarter about not allocating this one when
0N/A * not needed, that would seem like a good thing to do,
0N/A * even if in many cases the ColorModels don't match and
0N/A * its needed.
0N/A * Until all of this is resolved newImage is always true.
0N/A */
0N/A boolean newImage = true;
0N/A if (newImage) {
0N/A if (icm == null) {
0N/A deepImage = new BufferedImage(iw, ih, dibType);
0N/A } else {
0N/A deepImage = new BufferedImage(iw, ih, dibType,icm);
0N/A }
0N/A
0N/A /* Setup a Graphics2D on to the BufferedImage so that
0N/A * the source image when copied, lands within the
0N/A * image buffer.
0N/A */
0N/A Graphics2D imageGraphics = deepImage.createGraphics();
0N/A imageGraphics.clipRect(0, 0,
0N/A deepImage.getWidth(),
0N/A deepImage.getHeight());
0N/A
0N/A imageGraphics.translate(-rotBounds.getX(),
0N/A -rotBounds.getY());
0N/A imageGraphics.transform(rotTransform);
0N/A
0N/A /* Fill the BufferedImage either with the caller
0N/A * supplied color, 'bgColor' or, if null, with white.
0N/A */
0N/A if (bgcolor == null) {
0N/A bgcolor = Color.white;
0N/A }
0N/A
0N/A imageGraphics.drawImage(img,
0N/A srcX, srcY,
0N/A srcX + srcWidth,
0N/A srcY + srcHeight,
0N/A srcX, srcY,
0N/A srcX + srcWidth,
0N/A srcY + srcHeight,
0N/A bgcolor, null);
0N/A imageGraphics.dispose();
0N/A } else {
0N/A deepImage = img;
0N/A }
0N/A
0N/A /* Scale the bounding rectangle by the scale transform.
0N/A * Because the scaling transform has only x and y
0N/A * scaling components it is equivalent to multiply
0N/A * the x components of the bounding rectangle by
0N/A * the x scaling factor and to multiply the y components
0N/A * by the y scaling factor.
0N/A */
0N/A Rectangle2D.Float scaledBounds
0N/A = new Rectangle2D.Float(
0N/A (float) (rotBounds.getX() * scaleX),
0N/A (float) (rotBounds.getY() * scaleY),
0N/A (float) (rotBounds.getWidth() * scaleX),
0N/A (float) (rotBounds.getHeight() * scaleY));
0N/A
0N/A /* Pull the raster data from the buffered image
0N/A * and pass it along to GDI.
0N/A */
0N/A WritableRaster raster = deepImage.getRaster();
0N/A byte[] data;
0N/A if (raster instanceof ByteComponentRaster) {
0N/A data = ((ByteComponentRaster)raster).getDataStorage();
0N/A } else if (raster instanceof BytePackedRaster) {
0N/A data = ((BytePackedRaster)raster).getDataStorage();
0N/A } else {
0N/A return false;
0N/A }
0N/A
3535N/A int bitsPerPixel = 24;
3535N/A SampleModel sm = deepImage.getSampleModel();
3535N/A if (sm instanceof ComponentSampleModel) {
3535N/A ComponentSampleModel csm = (ComponentSampleModel)sm;
3535N/A bitsPerPixel = csm.getPixelStride() * 8;
3535N/A } else if (sm instanceof MultiPixelPackedSampleModel) {
3535N/A MultiPixelPackedSampleModel mppsm =
3535N/A (MultiPixelPackedSampleModel)sm;
3535N/A bitsPerPixel = mppsm.getPixelBitStride();
3535N/A } else {
3535N/A if (icm != null) {
3535N/A int diw = deepImage.getWidth();
3535N/A int dih = deepImage.getHeight();
3535N/A if (diw > 0 && dih > 0) {
3535N/A bitsPerPixel = data.length*8/diw/dih;
3535N/A }
3535N/A }
3535N/A }
3535N/A
0N/A /* Because the caller's image has been rotated
0N/A * and sheared into our BufferedImage and because
0N/A * we will be handing that BufferedImage directly to
0N/A * GDI, we need to set an additional clip. This clip
0N/A * makes sure that only parts of the BufferedImage
0N/A * that are also part of the caller's image are drawn.
0N/A */
0N/A Shape holdClip = getClip();
0N/A clip(xform.createTransformedShape(srcRect));
0N/A deviceClip(getClip().getPathIterator(getTransform()));
0N/A
0N/A wPrinterJob.drawDIBImage
0N/A (data, scaledBounds.x, scaledBounds.y,
0N/A (float)Math.rint(scaledBounds.width+0.5),
0N/A (float)Math.rint(scaledBounds.height+0.5),
0N/A 0f, 0f,
0N/A deepImage.getWidth(), deepImage.getHeight(),
3535N/A bitsPerPixel, icm);
0N/A
0N/A setClip(holdClip);
0N/A }
0N/A }
0N/A }
0N/A
0N/A return true;
0N/A }
0N/A
0N/A /**
0N/A * Have the printing application redraw everything that falls
0N/A * within the page bounds defined by <code>region</code>.
0N/A */
0N/A public void redrawRegion(Rectangle2D region, double scaleX, double scaleY,
0N/A Shape savedClip, AffineTransform savedTransform)
0N/A throws PrinterException {
0N/A
0N/A WPrinterJob wPrinterJob = (WPrinterJob)getPrinterJob();
0N/A Printable painter = getPrintable();
0N/A PageFormat pageFormat = getPageFormat();
0N/A int pageIndex = getPageIndex();
0N/A
0N/A /* Create a buffered image big enough to hold the portion
0N/A * of the source image being printed.
0N/A */
0N/A BufferedImage deepImage = new BufferedImage(
0N/A (int) region.getWidth(),
0N/A (int) region.getHeight(),
0N/A BufferedImage.TYPE_3BYTE_BGR);
0N/A
0N/A /* Get a graphics for the application to render into.
0N/A * We initialize the buffer to white in order to
0N/A * match the paper and then we shift the BufferedImage
0N/A * so that it covers the area on the page where the
0N/A * caller's Image will be drawn.
0N/A */
0N/A Graphics2D g = deepImage.createGraphics();
0N/A ProxyGraphics2D proxy = new ProxyGraphics2D(g, wPrinterJob);
0N/A proxy.setColor(Color.white);
0N/A proxy.fillRect(0, 0, deepImage.getWidth(), deepImage.getHeight());
0N/A proxy.clipRect(0, 0, deepImage.getWidth(), deepImage.getHeight());
0N/A
0N/A proxy.translate(-region.getX(), -region.getY());
0N/A
0N/A /* Calculate the resolution of the source image.
0N/A */
0N/A float sourceResX = (float)(wPrinterJob.getXRes() / scaleX);
0N/A float sourceResY = (float)(wPrinterJob.getYRes() / scaleY);
0N/A
0N/A /* The application expects to see user space at 72 dpi.
0N/A * so change user space from image source resolution to
0N/A * 72 dpi.
0N/A */
0N/A proxy.scale(sourceResX / DEFAULT_USER_RES,
0N/A sourceResY / DEFAULT_USER_RES);
0N/A
0N/A proxy.translate(
0N/A -wPrinterJob.getPhysicalPrintableX(pageFormat.getPaper())
0N/A / wPrinterJob.getXRes() * DEFAULT_USER_RES,
0N/A -wPrinterJob.getPhysicalPrintableY(pageFormat.getPaper())
0N/A / wPrinterJob.getYRes() * DEFAULT_USER_RES);
0N/A /* NB User space now has to be at 72 dpi for this calc to be correct */
0N/A proxy.transform(new AffineTransform(getPageFormat().getMatrix()));
0N/A proxy.setPaint(Color.black);
0N/A
0N/A painter.print(proxy, pageFormat, pageIndex);
0N/A
0N/A g.dispose();
0N/A
0N/A /* We need to set the device clip using saved information.
0N/A * savedClip intersects the user clip with a clip that restricts
0N/A * the GDI rendered area of our BufferedImage to that which
0N/A * may correspond to a rotate or shear.
0N/A * The saved device transform is needed as the current transform
0N/A * is not likely to be the same.
0N/A */
0N/A deviceClip(savedClip.getPathIterator(savedTransform));
0N/A
0N/A /* Scale the bounding rectangle by the scale transform.
0N/A * Because the scaling transform has only x and y
0N/A * scaling components it is equivalent to multiplying
0N/A * the x components of the bounding rectangle by
0N/A * the x scaling factor and to multiplying the y components
0N/A * by the y scaling factor.
0N/A */
0N/A Rectangle2D.Float scaledBounds
0N/A = new Rectangle2D.Float(
0N/A (float) (region.getX() * scaleX),
0N/A (float) (region.getY() * scaleY),
0N/A (float) (region.getWidth() * scaleX),
0N/A (float) (region.getHeight() * scaleY));
0N/A
0N/A /* Pull the raster data from the buffered image
0N/A * and pass it along to GDI.
0N/A */
0N/A ByteComponentRaster tile
0N/A = (ByteComponentRaster)deepImage.getRaster();
0N/A
0N/A wPrinterJob.drawImage3ByteBGR(tile.getDataStorage(),
0N/A scaledBounds.x, scaledBounds.y,
0N/A scaledBounds.width,
0N/A scaledBounds.height,
0N/A 0f, 0f,
0N/A deepImage.getWidth(), deepImage.getHeight());
0N/A
0N/A }
0N/A
0N/A /*
0N/A * Fill the path defined by <code>pathIter</code>
0N/A * with the specified color.
0N/A * The path is provided in device coordinates.
0N/A */
0N/A protected void deviceFill(PathIterator pathIter, Color color) {
0N/A
0N/A WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
0N/A
0N/A convertToWPath(pathIter);
0N/A wPrinterJob.selectSolidBrush(color);
0N/A wPrinterJob.fillPath();
0N/A }
0N/A
0N/A /*
0N/A * Set the printer device's clip to be the
0N/A * path defined by <code>pathIter</code>
0N/A * The path is provided in device coordinates.
0N/A */
0N/A protected void deviceClip(PathIterator pathIter) {
0N/A
0N/A WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
0N/A
0N/A convertToWPath(pathIter);
0N/A wPrinterJob.selectClipPath();
0N/A }
0N/A
0N/A /**
0N/A * Draw the bounding rectangle using transformed coordinates.
0N/A */
0N/A protected void deviceFrameRect(int x, int y, int width, int height,
0N/A Color color) {
0N/A
0N/A AffineTransform deviceTransform = getTransform();
0N/A
0N/A /* check if rotated or sheared */
0N/A int transformType = deviceTransform.getType();
0N/A boolean usePath = ((transformType &
0N/A (AffineTransform.TYPE_GENERAL_ROTATION |
0N/A AffineTransform.TYPE_GENERAL_TRANSFORM)) != 0);
0N/A
0N/A if (usePath) {
0N/A draw(new Rectangle2D.Float(x, y, width, height));
0N/A return;
0N/A }
0N/A
0N/A Stroke stroke = getStroke();
0N/A
0N/A if (stroke instanceof BasicStroke) {
0N/A BasicStroke lineStroke = (BasicStroke) stroke;
0N/A
0N/A int endCap = lineStroke.getEndCap();
0N/A int lineJoin = lineStroke.getLineJoin();
0N/A
0N/A
0N/A /* check for default style and try to optimize it by
0N/A * calling the frameRect native function instead of using paths.
0N/A */
0N/A if ((endCap == BasicStroke.CAP_SQUARE) &&
0N/A (lineJoin == BasicStroke.JOIN_MITER) &&
0N/A (lineStroke.getMiterLimit() ==10.0f)) {
0N/A
0N/A float lineWidth = lineStroke.getLineWidth();
0N/A Point2D.Float penSize = new Point2D.Float(lineWidth,
0N/A lineWidth);
0N/A
0N/A deviceTransform.deltaTransform(penSize, penSize);
0N/A float deviceLineWidth = Math.min(Math.abs(penSize.x),
0N/A Math.abs(penSize.y));
0N/A
0N/A /* transform upper left coordinate */
0N/A Point2D.Float ul_pos = new Point2D.Float(x, y);
0N/A deviceTransform.transform(ul_pos, ul_pos);
0N/A
0N/A /* transform lower right coordinate */
0N/A Point2D.Float lr_pos = new Point2D.Float(x + width,
0N/A y + height);
0N/A deviceTransform.transform(lr_pos, lr_pos);
0N/A
0N/A float w = (float) (lr_pos.getX() - ul_pos.getX());
0N/A float h = (float)(lr_pos.getY() - ul_pos.getY());
0N/A
0N/A WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
0N/A
0N/A /* use selectStylePen, if supported */
0N/A if (wPrinterJob.selectStylePen(endCap, lineJoin,
0N/A deviceLineWidth, color) == true) {
0N/A wPrinterJob.frameRect((float)ul_pos.getX(),
0N/A (float)ul_pos.getY(), w, h);
0N/A }
0N/A /* not supported, must be a Win 9x */
0N/A else {
0N/A
0N/A double lowerRes = Math.min(wPrinterJob.getXRes(),
0N/A wPrinterJob.getYRes());
0N/A
0N/A if ((deviceLineWidth/lowerRes) < MAX_THINLINE_INCHES) {
0N/A /* use the default pen styles for thin pens. */
0N/A wPrinterJob.selectPen(deviceLineWidth, color);
0N/A wPrinterJob.frameRect((float)ul_pos.getX(),
0N/A (float)ul_pos.getY(), w, h);
0N/A }
0N/A else {
0N/A draw(new Rectangle2D.Float(x, y, width, height));
0N/A }
0N/A }
0N/A }
0N/A else {
0N/A draw(new Rectangle2D.Float(x, y, width, height));
0N/A }
0N/A }
0N/A }
0N/A
0N/A
0N/A /*
0N/A * Fill the rectangle with specified color and using Windows'
0N/A * GDI fillRect function.
0N/A * Boundaries are determined by the given coordinates.
0N/A */
0N/A protected void deviceFillRect(int x, int y, int width, int height,
0N/A Color color) {
0N/A /*
0N/A * Transform to device coordinates
0N/A */
0N/A AffineTransform deviceTransform = getTransform();
0N/A
0N/A /* check if rotated or sheared */
0N/A int transformType = deviceTransform.getType();
0N/A boolean usePath = ((transformType &
0N/A (AffineTransform.TYPE_GENERAL_ROTATION |
0N/A AffineTransform.TYPE_GENERAL_TRANSFORM)) != 0);
0N/A if (usePath) {
0N/A fill(new Rectangle2D.Float(x, y, width, height));
0N/A return;
0N/A }
0N/A
0N/A Point2D.Float tlc_pos = new Point2D.Float(x, y);
0N/A deviceTransform.transform(tlc_pos, tlc_pos);
0N/A
0N/A Point2D.Float brc_pos = new Point2D.Float(x+width, y+height);
0N/A deviceTransform.transform(brc_pos, brc_pos);
0N/A
0N/A float deviceWidth = (float) (brc_pos.getX() - tlc_pos.getX());
0N/A float deviceHeight = (float)(brc_pos.getY() - tlc_pos.getY());
0N/A
0N/A WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
0N/A wPrinterJob.fillRect((float)tlc_pos.getX(), (float)tlc_pos.getY(),
0N/A deviceWidth, deviceHeight, color);
0N/A }
0N/A
0N/A
0N/A /**
0N/A * Draw a line using a pen created using the specified color
0N/A * and current stroke properties.
0N/A */
0N/A protected void deviceDrawLine(int xBegin, int yBegin, int xEnd, int yEnd,
0N/A Color color) {
0N/A Stroke stroke = getStroke();
0N/A
0N/A if (stroke instanceof BasicStroke) {
0N/A BasicStroke lineStroke = (BasicStroke) stroke;
0N/A
0N/A if (lineStroke.getDashArray() != null) {
0N/A draw(new Line2D.Float(xBegin, yBegin, xEnd, yEnd));
0N/A return;
0N/A }
0N/A
0N/A float lineWidth = lineStroke.getLineWidth();
0N/A Point2D.Float penSize = new Point2D.Float(lineWidth, lineWidth);
0N/A
0N/A AffineTransform deviceTransform = getTransform();
0N/A deviceTransform.deltaTransform(penSize, penSize);
0N/A
0N/A float deviceLineWidth = Math.min(Math.abs(penSize.x),
0N/A Math.abs(penSize.y));
0N/A
0N/A Point2D.Float begin_pos = new Point2D.Float(xBegin, yBegin);
0N/A deviceTransform.transform(begin_pos, begin_pos);
0N/A
0N/A Point2D.Float end_pos = new Point2D.Float(xEnd, yEnd);
0N/A deviceTransform.transform(end_pos, end_pos);
0N/A
0N/A int endCap = lineStroke.getEndCap();
0N/A int lineJoin = lineStroke.getLineJoin();
0N/A
0N/A /* check if it's a one-pixel line */
0N/A if ((end_pos.getX() == begin_pos.getX())
0N/A && (end_pos.getY() == begin_pos.getY())) {
0N/A
0N/A /* endCap other than Round will not print!
0N/A * due to Windows GDI limitation, force it to CAP_ROUND
0N/A */
0N/A endCap = BasicStroke.CAP_ROUND;
0N/A }
0N/A
0N/A
0N/A WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
0N/A
0N/A /* call native function that creates pen with style */
0N/A if (wPrinterJob.selectStylePen(endCap, lineJoin,
0N/A deviceLineWidth, color)) {
0N/A wPrinterJob.moveTo((float)begin_pos.getX(),
0N/A (float)begin_pos.getY());
0N/A wPrinterJob.lineTo((float)end_pos.getX(),
0N/A (float)end_pos.getY());
0N/A }
0N/A /* selectStylePen is not supported, must be Win 9X */
0N/A else {
0N/A
0N/A /* let's see if we can use a a default pen
0N/A * if it's round end (Windows' default style)
0N/A * or it's vertical/horizontal
0N/A * or stroke is too thin.
0N/A */
0N/A double lowerRes = Math.min(wPrinterJob.getXRes(),
0N/A wPrinterJob.getYRes());
0N/A
0N/A if ((endCap == BasicStroke.CAP_ROUND) ||
0N/A (((xBegin == xEnd) || (yBegin == yEnd)) &&
0N/A (deviceLineWidth/lowerRes < MAX_THINLINE_INCHES))) {
0N/A
0N/A wPrinterJob.selectPen(deviceLineWidth, color);
0N/A wPrinterJob.moveTo((float)begin_pos.getX(),
0N/A (float)begin_pos.getY());
0N/A wPrinterJob.lineTo((float)end_pos.getX(),
0N/A (float)end_pos.getY());
0N/A }
0N/A else {
0N/A draw(new Line2D.Float(xBegin, yBegin, xEnd, yEnd));
0N/A }
0N/A }
0N/A }
0N/A }
0N/A
0N/A
0N/A /**
0N/A * Given a Java2D <code>PathIterator</code> instance,
0N/A * this method translates that into a Window's path
0N/A * in the printer device context.
0N/A */
0N/A private void convertToWPath(PathIterator pathIter) {
0N/A
0N/A float[] segment = new float[6];
0N/A int segmentType;
0N/A
0N/A WPrinterJob wPrinterJob = (WPrinterJob) getPrinterJob();
0N/A
0N/A /* Map the PathIterator's fill rule into the Window's
0N/A * polygon fill rule.
0N/A */
0N/A int polyFillRule;
0N/A if (pathIter.getWindingRule() == PathIterator.WIND_EVEN_ODD) {
0N/A polyFillRule = WPrinterJob.POLYFILL_ALTERNATE;
0N/A } else {
0N/A polyFillRule = WPrinterJob.POLYFILL_WINDING;
0N/A }
0N/A wPrinterJob.setPolyFillMode(polyFillRule);
0N/A
0N/A wPrinterJob.beginPath();
0N/A
0N/A while (pathIter.isDone() == false) {
0N/A segmentType = pathIter.currentSegment(segment);
0N/A
0N/A switch (segmentType) {
0N/A case PathIterator.SEG_MOVETO:
0N/A wPrinterJob.moveTo(segment[0], segment[1]);
0N/A break;
0N/A
0N/A case PathIterator.SEG_LINETO:
0N/A wPrinterJob.lineTo(segment[0], segment[1]);
0N/A break;
0N/A
0N/A /* Convert the quad path to a bezier.
0N/A */
0N/A case PathIterator.SEG_QUADTO:
0N/A int lastX = wPrinterJob.getPenX();
0N/A int lastY = wPrinterJob.getPenY();
0N/A float c1x = lastX + (segment[0] - lastX) * 2 / 3;
0N/A float c1y = lastY + (segment[1] - lastY) * 2 / 3;
0N/A float c2x = segment[2] - (segment[2] - segment[0]) * 2/ 3;
0N/A float c2y = segment[3] - (segment[3] - segment[1]) * 2/ 3;
0N/A wPrinterJob.polyBezierTo(c1x, c1y,
0N/A c2x, c2y,
0N/A segment[2], segment[3]);
0N/A break;
0N/A
0N/A case PathIterator.SEG_CUBICTO:
0N/A wPrinterJob.polyBezierTo(segment[0], segment[1],
0N/A segment[2], segment[3],
0N/A segment[4], segment[5]);
0N/A break;
0N/A
0N/A case PathIterator.SEG_CLOSE:
0N/A wPrinterJob.closeFigure();
0N/A break;
0N/A }
0N/A
0N/A
0N/A pathIter.next();
0N/A }
0N/A
0N/A wPrinterJob.endPath();
0N/A
0N/A }
0N/A
0N/A}