001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030/** 031 * 032 */ 033package org.openimaj.image.typography.general; 034 035import java.awt.Font; 036import java.awt.Graphics; 037import java.awt.Graphics2D; 038import java.awt.font.FontRenderContext; 039import java.awt.font.GlyphVector; 040import java.awt.geom.AffineTransform; 041import java.awt.geom.CubicCurve2D; 042import java.awt.geom.GeneralPath; 043import java.awt.geom.PathIterator; 044import java.awt.geom.QuadCurve2D; 045import java.awt.image.BufferedImage; 046 047import org.openimaj.image.DisplayUtilities; 048import org.openimaj.image.FImage; 049import org.openimaj.image.ImageUtilities; 050import org.openimaj.image.renderer.ImageRenderer; 051import org.openimaj.image.renderer.RenderHints; 052import org.openimaj.image.typography.FontRenderer; 053import org.openimaj.image.typography.FontStyle; 054import org.openimaj.image.typography.FontStyle.HorizontalAlignment; 055import org.openimaj.math.geometry.point.Point2d; 056import org.openimaj.math.geometry.point.Point2dImpl; 057import org.openimaj.math.geometry.shape.Polygon; 058import org.openimaj.math.geometry.shape.Rectangle; 059import org.openimaj.math.geometry.transforms.TransformUtilities; 060 061import Jama.Matrix; 062 063import com.caffeineowl.graphics.bezier.BezierUtils; 064import com.caffeineowl.graphics.bezier.CubicSegmentConsumer; 065import com.caffeineowl.graphics.bezier.QuadSegmentConsumer; 066import com.caffeineowl.graphics.bezier.flatnessalgos.SimpleConvexHullSubdivCriterion; 067 068/** 069 * A font renderer that takes the glyph outline as generated by the Java AWT 070 * Font system and renders it into an OpenIMAJ image using the ImageRenderer 071 * methods. 072 * 073 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 074 * @created 18 Aug 2011 075 * 076 * 077 * @param <T> 078 * The image pixel type 079 */ 080public class GeneralFontRenderer<T> extends FontRenderer<T, GeneralFontStyle<T>> 081{ 082 /** 083 * {@inheritDoc} 084 * 085 * @see org.openimaj.image.typography.FontRenderer#renderText(org.openimaj.image.renderer.ImageRenderer, 086 * java.lang.String, int, int, org.openimaj.image.typography.FontStyle) 087 */ 088 @Override 089 public void renderText(final ImageRenderer<T, ?> renderer, final String text, 090 final int x, final int y, final GeneralFontStyle<T> style) 091 { 092 Polygon[] p = GeneralFontRenderer.getPolygons(text, x, y, style); 093 094 p = alignPolygons(p, style); 095 096 if (style.isOutline()) 097 { 098 for (final Polygon polyOuter : p) 099 { 100 if (polyOuter.nVertices() > 0) 101 renderer.drawPolygon(polyOuter, style.getColour()); 102 for (final Polygon poly : polyOuter.getInnerPolys()) 103 if (poly.nVertices() > 0) 104 renderer.drawPolygon(poly, style.getColour()); 105 } 106 } 107 else 108 { 109 for (final Polygon poly : p) 110 if (poly.nVertices() > 0) 111 renderer.drawPolygonFilled(poly, style.getColour()); 112 } 113 } 114 115 private Polygon[] alignPolygons(Polygon[] p, FontStyle<T> sty) { 116 int minx = Integer.MAX_VALUE, miny = Integer.MAX_VALUE, maxx = -minx, maxy = -miny; 117 118 for (final Polygon polygon : p) { 119 for (final Point2d point2d : polygon) { 120 minx = (int) Math.min(point2d.getX(), minx); 121 miny = (int) Math.min(point2d.getY(), miny); 122 maxx = (int) Math.max(point2d.getX(), maxx); 123 maxy = (int) Math.max(point2d.getY(), maxy); 124 } 125 } 126 127 final Rectangle bb = new Rectangle(minx, miny, maxx - minx, maxy - miny); 128 129 // if we have a non-standard horizontal alignment 130 if ((sty.getHorizontalAlignment() != HorizontalAlignment.HORIZONTAL_LEFT)) { 131 // find the length of the string in pixels ... 132 final float len = bb.width; 133 Matrix trans = null; 134 // if we are center aligned 135 if (sty.getHorizontalAlignment() == HorizontalAlignment.HORIZONTAL_CENTER) { 136 trans = TransformUtilities.translateMatrix(-len / 2, 0); 137 } else { 138 trans = TransformUtilities.translateMatrix(-len, 0); 139 } 140 141 for (int i = 0; i < p.length; i++) { 142 p[i] = p[i].transform(trans); 143 } 144 } 145 146 return p; 147 } 148 149 /** 150 * Returns a list of polygons that represent the letters in the given text. 151 * If the font style is outline, the holes will be delivered as separate 152 * polygons otherwise they will be integrated into the letter polygons. 153 * 154 * @param text 155 * The text to render as a polygon 156 * @param x 157 * The x-coordinate 158 * @param y 159 * The y-coordinate 160 * @param style 161 * The font's style 162 * @return A list of polygons 163 */ 164 public static <T> Polygon[] getPolygons(final String text, final int x, final int y, final GeneralFontStyle<T> style) 165 { 166 return getPolygons(text.toCharArray(), x, y, style); 167 } 168 169 /** 170 * Returns a list of polygons that represent the letters in the given text. 171 * If the font style is outline, the holes will be delivered as separate 172 * polygons otherwise they will be integrated into the letter polygons. 173 * 174 * @param characters 175 * The text to render as a polygon 176 * @param x 177 * The x-coordinate 178 * @param y 179 * The y-coordinate 180 * @param style 181 * The font's style 182 * @return A list of polygons 183 */ 184 public static <T> Polygon[] getPolygons(final char[] characters, 185 final int x, final int y, final GeneralFontStyle<T> style) 186 { 187 final Font f = new Font( 188 style.getFont().getName(), 189 ((GeneralFont) style.getFont()).getType(), 190 style.getFontSize()); 191 192 final FontRenderContext frc = new FontRenderContext( 193 new AffineTransform(), true, true); 194 final GlyphVector g = f.createGlyphVector(frc, characters); 195 196 final Polygon[] output = new Polygon[characters.length]; 197 for (int i = 0; i < characters.length; i++) 198 output[i] = new Polygon(); 199 200 Polygon currentPoly = null; 201 for (int i = 0; i < g.getNumGlyphs(); i++) 202 { 203 final Polygon letterPoly = output[g.getGlyphCharIndex(i)]; 204 205 final GeneralPath s = (GeneralPath) g.getGlyphOutline(i, x, y); 206 final PathIterator pi = s.getPathIterator(new AffineTransform()); 207 208 final float[] ps = new float[6]; 209 float xx = 0, yy = 0; 210 while (!pi.isDone()) 211 { 212 final int t = pi.currentSegment(ps); 213 214 switch (t) 215 { 216 case PathIterator.SEG_MOVETO: { 217 if (currentPoly != null && currentPoly.nVertices() > 0) 218 letterPoly.addInnerPolygon( 219 currentPoly.roundVertices()); 220 currentPoly = new Polygon(); 221 222 // if( letterPoly != null && letterPoly.nVertices() > 0 && 223 // letterPoly.isInside( new Point2dImpl( ps[0], ps[1] ) ) ) 224 // currentPoly.setIsHole( true ); 225 226 currentPoly.addVertex(ps[0], ps[1]); 227 xx = ps[0]; 228 yy = ps[1]; 229 break; 230 } 231 case PathIterator.SEG_LINETO: { 232 currentPoly.addVertex(ps[0], ps[1]); 233 xx = ps[0]; 234 yy = ps[1]; 235 break; 236 } 237 case PathIterator.SEG_QUADTO: { 238 final QuadCurve2D c = new QuadCurve2D.Double( 239 xx, yy, ps[0], ps[1], ps[2], ps[3]); 240 final Polygon p = currentPoly; 241 BezierUtils.adaptiveHalving(c, new SimpleConvexHullSubdivCriterion(), 242 new QuadSegmentConsumer() 243 { 244 @Override 245 public void processSegment(final QuadCurve2D segment, final double startT, 246 final double endT) 247 { 248 if (0.0 == startT) 249 p.addVertex(new Point2dImpl( 250 (float) segment.getX1(), (float) segment.getY1())); 251 252 p.addVertex(new Point2dImpl( 253 (float) segment.getX2(), (float) segment.getY2())); 254 } 255 } 256 ); 257 xx = ps[2]; 258 yy = ps[3]; 259 break; 260 } 261 case PathIterator.SEG_CUBICTO: { 262 final CubicCurve2D c = new CubicCurve2D.Double( 263 xx, yy, ps[0], ps[1], 264 ps[2], ps[3], ps[4], ps[5]); 265 final Polygon p = currentPoly; 266 BezierUtils.adaptiveHalving(c, new SimpleConvexHullSubdivCriterion(), 267 new CubicSegmentConsumer() 268 { 269 @Override 270 public void processSegment(final CubicCurve2D segment, 271 final double startT, final double endT) 272 { 273 if (0.0 == startT) 274 p.addVertex(new Point2dImpl( 275 (float) segment.getX1(), (float) segment.getY1())); 276 277 p.addVertex(new Point2dImpl( 278 (float) segment.getX2(), (float) segment.getY2())); 279 } 280 } 281 ); 282 xx = ps[4]; 283 yy = ps[5]; 284 break; 285 } 286 case PathIterator.SEG_CLOSE: { 287 currentPoly.addVertex(ps[0], ps[1]); 288 letterPoly.addInnerPolygon( 289 currentPoly.roundVertices()); 290 currentPoly = new Polygon(); 291 292 break; 293 } 294 } 295 296 pi.next(); 297 } 298 } 299 300 return output; 301 } 302 303 /** 304 * {@inheritDoc} 305 * 306 * @see org.openimaj.image.typography.FontRenderer#getSize(java.lang.String, 307 * org.openimaj.image.typography.FontStyle) 308 */ 309 @Override 310 public Rectangle getSize(final String string, final GeneralFontStyle<T> style) 311 { 312 final Rectangle bounds = new Rectangle(0, 0, Float.MIN_VALUE, Float.MIN_VALUE); 313 314 final Polygon[] polys = GeneralFontRenderer.getPolygons(string, 0, 0, style); 315 for (final Polygon p : polys) 316 { 317 bounds.x = (float) Math.min(bounds.x, p.minX()); 318 bounds.y = (float) Math.min(bounds.y, p.minY()); 319 bounds.width = (float) Math.max(bounds.width, p.maxX() - bounds.x); 320 bounds.height = (float) Math.max(bounds.height, p.maxY() - bounds.y); 321 } 322 323 return bounds; 324 } 325 326 /** 327 * Just for testing render quality against AWT 328 * 329 * @param args 330 */ 331 public static void main(String[] args) { 332 final FImage tmp = new FImage(800, 800); 333 final int size = 40; 334 tmp.drawText("Hello World!", 20, 20 + size, new GeneralFont("Arial", Font.PLAIN), size); 335 336 final GeneralFontStyle<Float> gfs = new GeneralFontStyle<Float>(new GeneralFont("Arial", Font.PLAIN), 337 tmp.createRenderer(RenderHints.ANTI_ALIASED)); 338 gfs.setFontSize(size); 339 final Polygon[] polys = getPolygons("Hello World!", 20, 20 + 2 * size, gfs); 340 for (final Polygon p : polys) 341 tmp.drawPolygon(p, 1f); 342 343 final Font fnt = new Font("Arial", Font.PLAIN, size); 344 final BufferedImage bimg = ImageUtilities.createBufferedImage(tmp); 345 346 final Graphics g = bimg.getGraphics(); 347 g.setFont(fnt); 348 g.drawString("Hello World!", 20, 20 + 3 * size); 349 350 final GlyphVector gl = fnt.createGlyphVector(new FontRenderContext(null, true, true), 351 "Hello World!"); 352 for (int i = 0; i < gl.getNumGlyphs(); i++) { 353 ((Graphics2D) g).fill(gl.getGlyphOutline(i, 20, 20 + 4 * size)); 354 } 355 356 DisplayUtilities.display(bimg); 357 } 358}