/* * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. * * * * * * * * * * * * * * * * * * * * */ package com.sun.javafx.sg.prism; import com.sun.javafx.font.FontResource; import com.sun.javafx.font.FontStrike; import com.sun.javafx.font.Metrics; import com.sun.javafx.font.PGFont; import com.sun.javafx.geom.BaseBounds; import com.sun.javafx.geom.Path2D; import com.sun.javafx.geom.Point2D; import com.sun.javafx.geom.RectBounds; import com.sun.javafx.geom.RoundRectangle2D; import com.sun.javafx.geom.Shape; import com.sun.javafx.geom.transform.BaseTransform; import com.sun.javafx.scene.text.GlyphList; import com.sun.javafx.text.TextRun; import com.sun.prism.Graphics; import com.sun.prism.paint.Color; public class NGText extends NGShape { static final BaseTransform IDENT = BaseTransform.IDENTITY_TRANSFORM; public NGText() { } private GlyphList[] runs; public void setGlyphs(Object[] glyphs) { this.runs = (GlyphList[])glyphs; geometryChanged(); } private float layoutX, layoutY; public void setLayoutLocation(float x, float y) { layoutX = x; layoutY = y; geometryChanged(); } private PGFont font; public void setFont(Object font) { if (font != null && font.equals(this.font)) { return; } this.font = (PGFont)font; this.fontStrike = null; this.identityStrike = null; geometryChanged(); } private int fontSmoothingType; public void setFontSmoothingType(int fontSmoothingType) { this.fontSmoothingType = fontSmoothingType; geometryChanged(); } private boolean underline; public void setUnderline(boolean underline) { this.underline = underline; geometryChanged(); } private boolean strikethrough; public void setStrikethrough(boolean strikethrough) { this.strikethrough = strikethrough; geometryChanged(); } private Object selectionPaint; private int selectionStart; private int selectionEnd; public void setSelection(int start, int end, Object color) { selectionPaint = color; selectionStart = start; selectionEnd = end; geometryChanged(); } /** * Provide some lucky padding in the case that we are rendering LCD * text since there might be some pixels that lie outside the normally * computed content bounds. */ @Override protected BaseBounds computePadding(BaseBounds region) { float pad = fontSmoothingType == FontResource.AA_LCD ? 2f : 1f; return region.deriveWithNewBounds(region.getMinX() - pad, region.getMinY() - pad, region.getMinZ(), region.getMaxX() + pad, region.getMaxY() + pad, region.getMaxZ()); } private static double EPSILON = 0.01; private FontStrike fontStrike = null; private FontStrike identityStrike = null; private double[] strikeMat = new double[4]; private FontStrike getStrike(BaseTransform xform) { int smoothingType = fontSmoothingType; if (getMode() == Mode.STROKE_FILL) { // When there's a stroke, we want the glyph to be unhinted to match // the stroke. This currently means it must be grayscale. smoothingType = FontResource.AA_GREYSCALE; } if (xform.isIdentity()) { if (identityStrike == null || smoothingType != identityStrike.getAAMode()) { identityStrike = font.getStrike(IDENT, smoothingType); } return identityStrike; } // REMIND: need to enhance this, to take other rendering attributes. if (fontStrike == null || fontStrike.getSize() != font.getSize() || (xform.getMxy() == 0 && strikeMat[1] != 0) || (xform.getMyx() == 0 && strikeMat[2] != 0) || (Math.abs(strikeMat[0] - xform.getMxx()) > EPSILON) || (Math.abs(strikeMat[1] - xform.getMxy()) > EPSILON) || (Math.abs(strikeMat[2] - xform.getMyx()) > EPSILON) || (Math.abs(strikeMat[3] - xform.getMyy()) > EPSILON) || smoothingType != fontStrike.getAAMode()) { fontStrike = font.getStrike(xform, smoothingType); strikeMat[0] = xform.getMxx(); strikeMat[1] = xform.getMxy(); strikeMat[2] = xform.getMyx(); strikeMat[3] = xform.getMyy(); } return fontStrike; } @Override public Shape getShape() { if (runs == null) { return new Path2D(); } FontStrike strike = getStrike(IDENT); Path2D outline = new Path2D(); for (int i = 0; i < runs.length; i++) { GlyphList run = runs[i]; Point2D pt = run.getLocation(); float x = pt.x - layoutX; float y = pt.y - layoutY; BaseTransform t = BaseTransform.getTranslateInstance(x, y); outline.append(strike.getOutline(run, t), false); Metrics metrics = null; if (underline) { metrics = strike.getMetrics(); RoundRectangle2D rect = new RoundRectangle2D(); rect.x = x; rect.y = y + metrics.getUnderLineOffset(); rect.width = run.getWidth(); rect.height = metrics.getUnderLineThickness(); outline.append(rect, false); } if (strikethrough) { if (metrics == null) { metrics = strike.getMetrics(); } RoundRectangle2D rect = new RoundRectangle2D(); rect.x = x; rect.y = y + metrics.getStrikethroughOffset(); rect.width = run.getWidth(); rect.height = metrics.getStrikethroughThickness(); outline.append(rect, false); } } return outline; } private boolean drawingEffect = false; @Override protected void renderEffect(Graphics g) { /* Text as pre-composed image glyphs must be rendered in * device space because otherwise pixelisation effects are * very apparent. * The Effects implementation seems to indicate that it applies * effects in a space with the transforms already applied : * ie PrEffectHelper.effect() says for at least a 2D TX : * // process the effect using the current 2D transform, and then * // render the resulting image in device space (i.e., with identity) * However its apparent that (eg) a rotation is applied twice to * shadow text. As if drawing the "non-shadow" text over the image * with the shadow text overlooks that this is in effect * applying that rotation again. However I don't think its quite * that simple. Also the shadow text is cut-off as if it was * clipped to the original unrotated coordinates. * To work around this if an effect is detected, we will render * as shapes since they are provided in user space. * This is probably a reasonable compromise. * However if no transform is detected we can try to use the normal * image drawing path. If that's causes problems this test can * be removed. */ if (!g.getTransformNoClone().isTranslateOrIdentity()) { drawingEffect = true; } try { super.renderEffect(g); } finally { drawingEffect = false; } } private static int FILL = 1 << 1; private static int SHAPE_FILL = 1 << 2; private static int TEXT = 1 << 3; private static int DECORATION = 1 << 4; @Override protected void renderContent2D(Graphics g, boolean printing) { if (mode == Mode.EMPTY) return; if (runs == null || runs.length == 0) return; BaseTransform tx = g.getTransformNoClone(); FontStrike strike = getStrike(tx); if (strike.getAAMode() == FontResource.AA_LCD || (fillPaint != null && fillPaint.isProportional()) || (drawPaint != null && drawPaint.isProportional())) { /* * This check is only a performance optimization, to prevent * unnecessarily computing bounds. It's a quickly cautious estimate * if we might need to setNodeBounds, graphics does practically no * extra work by setting node bounds. But it's much faster to * setNodeBounds for LCD text rendering and is required for correct * proportional gradient. */ BaseBounds bds = getContentBounds(new RectBounds(), IDENT); g.setNodeBounds((RectBounds)bds); } Color selectionColor = null; if (selectionStart != selectionEnd && selectionPaint instanceof Color) { selectionColor = (Color)selectionPaint; } BaseBounds clipBds = null; if (getClipNode() != null) { // Note: this clip does not including any clip in the ancestors. clipBds = getClippedBounds(new RectBounds(), IDENT); } // FILL or STROKE_FILL if (mode != Mode.STROKE) { g.setPaint(fillPaint); int op = TEXT; op |= strike.drawAsShapes() || drawingEffect ? SHAPE_FILL : FILL; renderText(g, strike, clipBds, selectionColor, op); // Splitting decoration from text rendering is important in order // to group common render states together, for fast performance. if (underline || strikethrough) { op = DECORATION | SHAPE_FILL; renderText(g, strike, clipBds, selectionColor, op); } } // STROKE or STROKE_FILL if (mode != Mode.FILL) { g.setPaint(drawPaint); g.setStroke(drawStroke); int op = TEXT; if (underline || strikethrough) { op |= DECORATION; } renderText(g, strike, clipBds, selectionColor, op); } g.setNodeBounds(null); } private void renderText(Graphics g, FontStrike strike, BaseBounds clipBds, Color selectionColor, int op) { for (int i = 0; i < runs.length; i++) { TextRun run = (TextRun)runs[i]; RectBounds lineBounds = run.getLineBounds(); Point2D pt = run.getLocation(); float x = pt.x - layoutX; float y = pt.y - layoutY; if (clipBds != null) { if (y > clipBds.getMaxY()) break; if (y + lineBounds.getHeight() < clipBds.getMinY()) continue; if (x > clipBds.getMaxX()) continue; if (x + run.getWidth() < clipBds.getMinX()) continue; } y -= lineBounds.getMinY(); if ((op & TEXT) != 0 && run.getGlyphCount() > 0) { if ((op & FILL) != 0) { int start = run.getStart(); g.drawString(run, strike, x, y, selectionColor, selectionStart - start, selectionEnd - start); } else { BaseTransform t = BaseTransform.getTranslateInstance(x, y); if ((op & SHAPE_FILL) != 0) { g.fill(strike.getOutline(run, t)); } else { g.draw(strike.getOutline(run, t)); } } } if ((op & DECORATION) != 0) { Metrics metrics = strike.getMetrics(); if (underline) { float offset = y + metrics.getUnderLineOffset(); float thickness = metrics.getUnderLineThickness(); if ((op & SHAPE_FILL) != 0) { if (thickness <= 1f && g.getTransformNoClone().isTranslateOrIdentity()) { float myt = (float)g.getTransformNoClone().getMyt(); offset = Math.round(offset + myt) - myt; } g.fillRect(x, offset, run.getWidth(), thickness); } else { g.drawRect(x, offset, run.getWidth(), thickness); } } if (strikethrough) { float offset = y + metrics.getStrikethroughOffset(); float thickness = metrics.getStrikethroughThickness(); if ((op & SHAPE_FILL) != 0) { if (thickness <= 1f && g.getTransformNoClone().isTranslateOrIdentity()) { float myt = (float)g.getTransformNoClone().getMyt(); offset = Math.round(offset + myt) - myt; } g.fillRect(x, offset, run.getWidth(), thickness); } else { g.drawRect(x, offset, run.getWidth(), thickness); } } } } } }