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}