/* * Copyright 2006-2017 ICEsoft Technologies Canada Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the * License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package org.icepdf.core.pobjects.graphics; import org.icepdf.core.pobjects.fonts.FontFile; import org.icepdf.core.pobjects.graphics.text.GlyphText; import org.icepdf.core.util.Defs; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.Rectangle2D; import java.util.ArrayList; /** * <p>This class represents text which will be rendered to the a graphics context. * This class was created to act as a wrapper for painting text using the Phelphs * font library as well as painting using java.awt.Font.</p> * <br> * <p>Objects of this type are created by the content parser when "TJ" or "Tj" * operands are encountered in a page's content stream. Each TextSprite object * is comprised of a list of CharSprites which can be painted by the Shapes * class at render time.</p> * * @since 2.0 */ public class TextSprite { // ability to turn off optimized drawing for text. private static boolean optimizedDrawingEnabled = Defs.booleanProperty("org.icepdf.core.text.optimized", true); private final static boolean OPTIMIZED_DRAWING_TYPE_3_ENABLED = Defs.booleanProperty("org.icepdf.core.text.optimized.type3", true); // child GlyphText objects private ArrayList<GlyphText> glyphTexts; // text bounds, including all child Glyph sprites, in glyph space // this bound is used during painting to respect painting clip. Rectangle2D.Float bounds; // space reference for where glyph private AffineTransform graphicStateTransform; private AffineTransform tmTransform; // stroke color private Color strokeColor; // the write private int rmode; // Font used to paint text private FontFile font; // font's resource name and size, used by PS writer. private String fontName; private int fontSize; private static final String TYPE_3 = "Type3"; /** * <p>Creates a new TextSprite object.</p> * * @param font font used when painting glyphs. * @param contentLength length of text content. */ public TextSprite(FontFile font, int contentLength, AffineTransform graphicStateTransform, AffineTransform tmTransform) { glyphTexts = new ArrayList<GlyphText>(contentLength); // all glyphs in text share this ctm this.graphicStateTransform = graphicStateTransform; this.tmTransform = tmTransform; this.font = font; if (optimizedDrawingEnabled && !OPTIMIZED_DRAWING_TYPE_3_ENABLED) { optimizedDrawingEnabled = !(font.getFormat() != null && font.getFormat().equals(TYPE_3)); } bounds = new Rectangle2D.Float(); } /** * <p>Adds a new text char to the TextSprite which will pe painted at x, y under * the current CTM</p> * * @param cid cid to paint. * @param unicode unicode representation of cid. * @param x x-coordinate to paint. * @param y y-coordinate to paint. * @param width width of cid from font. */ public GlyphText addText(String cid, String unicode, float x, float y, float width) { // x,y must not chance as it will affect painting of the glyph, // we can change the bounds of glyphBounds as this is what needs to be normalized // to page space // IMPORTANT: where working in Java Coordinates with any of the Font bounds float w = width;//(float)stringBounds.getWidth(); float h = (float) (font.getAscent() + font.getDescent()); double descent = font.getDescent(); double ascent = font.getAscent(); if (h <= 0.0f) { h = (float) (font.getMaxCharBounds().getHeight()); } if (w <= 0.0f) { w = (float) font.getMaxCharBounds().getWidth(); } // zero height will not intersect with clip rectangle and maybe have visibility issues. // we generally get here if the font.getAscent is zero and as a result must compensate. if (h <= 0.0f) { Rectangle2D bounds = font.getEstringBounds(cid, 0, 1); if (bounds != null && bounds.getHeight() > 0) { h = (float) bounds.getHeight(); } else { // match the width, as it will make text selection work a bit better. h = font.getSize(); } if (ascent == 0) { ascent = h; } } Rectangle2D.Float glyphBounds; // irregular negative layout of text, need to create the bbox appropriately. if (w < 0.0f || font.getSize() < 0) { glyphBounds = new Rectangle2D.Float(x + width, y - (float) descent, -w, h); }else{ glyphBounds = new Rectangle2D.Float(x, y - (float) ascent, w, h); } // add bounds to total text bounds. bounds.add(glyphBounds); // create glyph and normalize bounds. GlyphText glyphText = new GlyphText(x, y, glyphBounds, cid, unicode); glyphText.normalizeToUserSpace(graphicStateTransform, tmTransform); glyphTexts.add(glyphText); return glyphText; } /** * Gets the character bounds of each glyph found in the TextSprite. * * @return bounds in PDF coordinates of character bounds */ public ArrayList<GlyphText> getGlyphSprites() { return glyphTexts; } public AffineTransform getGraphicStateTransform() { return graphicStateTransform; } /** * Set the graphic state transform on all child sprites, This is used for * xForm object parsing and text selection. There is no need to do this * outside of the context parser. * * @param graphicStateTransform */ public void setGraphicStateTransform(AffineTransform graphicStateTransform) { this.graphicStateTransform = graphicStateTransform; for (GlyphText sprite : glyphTexts) { sprite.normalizeToUserSpace(this.graphicStateTransform, tmTransform); } } /** * <p>Set the rmode for all the characters being in this object. Rmode can * have the following values:</p> * <ul> * <li>0 - Fill text.</li> * <li>1 - Stroke text. </li> * <li>2 - fill, then stroke text. </li> * <li>3 - Neither fill nor stroke text (invisible). </li> * <li>4 - Fill text and add to path for clipping. </li> * <li>5 - Stroke text and add to path for clipping. </li> * <li>6 - Fill, then stroke text and add to path for clipping. </li> * <li>7 - Add text to path for clipping.</li> * </ul> * * @param rmode valid rmode from 0-7 */ public void setRMode(int rmode) { if (rmode >= 0) { this.rmode = rmode; } } public String toString() { StringBuilder text = new StringBuilder(glyphTexts.size()); for (GlyphText glyphText : glyphTexts) { text.append(glyphText.getUnicode()); } return text.toString(); } public void setStrokeColor(Color color) { strokeColor = color; } /** * Getst the bounds of the text that makes up this sprite. The bounds * are defined PDF space and are relative to the current CTM. * * @return text sprites bounds. */ public Rectangle2D.Float getBounds() { return bounds; } /** * <p>Paints all the character elements in this TextSprite to the graphics * context</p> * * @param g graphics context to which the characters will be painted to. */ public void paint(Graphics g) { Graphics2D g2d = (Graphics2D) g; // draw bounding box. // drawBoundBox(g2d); for (GlyphText glyphText : glyphTexts) { // paint glyph font.drawEstring(g2d, glyphText.getCid(), glyphText.getX(), glyphText.getY(), FontFile.LAYOUT_NONE, rmode, strokeColor); // debug glyph box // draw glyph box // drawGyphBox(g2d, glyphText); } } /** * Gets the glyph outline as an Area. This method is primarily used * for processing text rendering modes 4 - 7. * * @return area representing the glyph outline. */ public Area getGlyphOutline() { Area glyphOutline = null; for (GlyphText glyphText : glyphTexts) { if (glyphOutline != null) { glyphOutline.add(new Area(font.getEstringOutline( glyphText.getCid(), glyphText.getX(), glyphText.getY()))); } else { glyphOutline = new Area(font.getEstringOutline( glyphText.getCid(), glyphText.getX(), glyphText.getY())); } } return glyphOutline; } public FontFile getFont() { return font; } public Color getStrokeColor() { return strokeColor; } public String getFontName() { return fontName; } public void setFontName(String fontName) { this.fontName = fontName; } public int getFontSize() { return fontSize; } public void setFontSize(int fontSize) { this.fontSize = fontSize; } /* private void drawBoundBox(Graphics2D gg) { // draw the characters GeneralPath charOutline; Color oldColor = gg.getColor(); Stroke oldStroke = gg.getStroke(); double scale = gg.getTransform().getScaleX(); scale = 1.0f / scale; if (scale <= 0) { scale = 1; } gg.setStroke(new BasicStroke((float) (scale))); gg.setColor(Color.blue); charOutline = new GeneralPath(bounds); gg.draw(charOutline); gg.setColor(oldColor); gg.setStroke(oldStroke); } */ public void setFont(FontFile font) { this.font = font; } /* private void drawGyphBox(Graphics2D gg, GlyphText glyphSprite) { // draw the characters GeneralPath charOutline; Color oldColor = gg.getColor(); Stroke oldStroke = gg.getStroke(); double scale = gg.getTransform().getScaleX(); scale = 1.0f / scale; if (scale <= 0) { scale = 1; } gg.setStroke(new BasicStroke((float) (scale))); gg.setColor(Color.red); charOutline = new GeneralPath(glyphSprite.getBounds()); gg.draw(charOutline); gg.setColor(oldColor); gg.setStroke(oldStroke); } */ /** * Tests if the interior of the <code>TextSprite</code> bounds intersects the * interior of a specified <code>shape</code>. * * @param shape shape to calculate intersection against * @return true, if <code>TextSprite</code> bounds intersects <code>shape</code>; * otherwise; false. */ public boolean intersects(Shape shape) { // return shape.intersects(bounds.toJava2dCoordinates()); return !(optimizedDrawingEnabled)|| (shape != null && shape.intersects(bounds)); } }