| /* Latin.java -- Latin specific glyph handling |
| Copyright (C) 2006 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package gnu.java.awt.font.autofit; |
| |
| import java.awt.geom.AffineTransform; |
| import java.util.HashSet; |
| |
| import gnu.java.awt.font.opentype.OpenTypeFont; |
| import gnu.java.awt.font.opentype.truetype.Fixed; |
| import gnu.java.awt.font.opentype.truetype.Point; |
| import gnu.java.awt.font.opentype.truetype.Zone; |
| |
| /** |
| * Implements Latin specific glyph handling. |
| */ |
| class Latin |
| implements Script, Constants |
| { |
| |
| static final int MAX_WIDTHS = 16; |
| |
| private final static int MAX_TEST_CHARS = 12; |
| |
| /** |
| * The types of the 6 blue zones. |
| */ |
| private static final int CAPITAL_TOP = 0; |
| private static final int CAPITAL_BOTTOM = 1; |
| private static final int SMALL_F_TOP = 2; |
| private static final int SMALL_TOP = 3; |
| private static final int SMALL_BOTTOM = 4; |
| private static final int SMALL_MINOR = 5; |
| static final int BLUE_MAX = 6; |
| |
| /** |
| * The test chars for the blue zones. |
| * |
| * @see #initBlues(LatinMetrics, OpenTypeFont) |
| */ |
| private static final String[] TEST_CHARS = |
| new String[]{"THEZOCQS", "HEZLOCUS", "fijkdbh", |
| "xzroesc", "xzroesc", "pqgjy"}; |
| |
| public void applyHints(GlyphHints hints, Zone outline, ScriptMetrics metrics) |
| { |
| hints.reload(outline); |
| hints.rescale(metrics); |
| if (hints.doHorizontal()) |
| { |
| detectFeatures(hints, DIMENSION_HORZ); |
| } |
| if (hints.doVertical()) |
| { |
| detectFeatures(hints, DIMENSION_VERT); |
| computeBlueEdges(hints, (LatinMetrics) metrics); |
| } |
| // Grid-fit the outline. |
| for (int dim = 0; dim < DIMENSION_MAX; dim++) |
| { |
| if (dim == DIMENSION_HORZ && hints.doHorizontal() |
| || dim == DIMENSION_VERT && hints.doVertical()) |
| { |
| hintEdges(hints, dim); |
| if (hints.doAlignEdgePoints()) |
| hints.alignEdgePoints(dim); |
| if (hints.doAlignStrongPoints()) |
| hints.alignStrongPoints(dim); |
| if (hints.doAlignWeakPoints()) |
| hints.alignWeakPoints(dim); |
| |
| } |
| } |
| // FreeType does a save call here. I guess that's not needed as we operate |
| // on the live glyph data anyway. |
| } |
| |
| private void hintEdges(GlyphHints hints, int dim) |
| { |
| AxisHints axis = hints.axis[dim]; |
| Edge[] edges = axis.edges; |
| int numEdges = axis.numEdges; |
| Edge anchor = null; |
| int hasSerifs = 0; |
| |
| // We begin by aligning all stems relative to the blue zone if |
| // needed -- that's only for horizontal edges. |
| if (dim == DIMENSION_VERT) |
| { |
| for (int e = 0; e < numEdges; e++) |
| { |
| Edge edge = edges[e]; |
| if ((edge.flags & Segment.FLAG_EDGE_DONE) != 0) |
| continue; |
| |
| Width blue = edge.blueEdge; |
| Edge edge1 = null; |
| Edge edge2 = edge.link; |
| if (blue != null) |
| { |
| edge1 = edge; |
| } |
| else if (edge2 != null && edge2.blueEdge != null) |
| { |
| blue = edge2.blueEdge; |
| edge1 = edge2; |
| edge2 = edge; |
| } |
| if (edge1 == null) |
| continue; |
| |
| edge1.pos = blue.fit; |
| edge1.flags |= Segment.FLAG_EDGE_DONE; |
| |
| if (edge2 != null && edge2.blueEdge == null) |
| { |
| alignLinkedEdge(hints, dim, edge1, edge2); |
| edge2.flags |= Segment.FLAG_EDGE_DONE; |
| } |
| if (anchor == null) |
| anchor = edge; |
| } |
| } |
| |
| // Now we will align all stem edges, trying to maintain the |
| // relative order of stems in the glyph. |
| for (int e = 0; e < numEdges; e++) |
| { |
| Edge edge = edges[e]; |
| if ((edge.flags & Segment.FLAG_EDGE_DONE) != 0) |
| continue; |
| Edge edge2 = edge.link; |
| if (edge2 == null) |
| { |
| hasSerifs++; |
| continue; |
| } |
| // Now align the stem. |
| // This should not happen, but it's better to be safe. |
| if (edge2.blueEdge != null || axis.getEdgeIndex(edge2) < e) |
| { |
| alignLinkedEdge(hints, dim, edge2, edge); |
| edge.flags |= Segment.FLAG_EDGE_DONE; |
| continue; |
| } |
| |
| if (anchor == null) |
| { |
| int orgLen = edge2.opos - edge.opos; |
| int curLen = computeStemWidth(hints, dim, orgLen, edge.flags, |
| edge2.flags); |
| int uOff, dOff, orgCenter, curPos1, error1, error2; |
| if (curLen <= 64) // < 1 Pixel. |
| { |
| uOff = 32; |
| dOff = 32; |
| } |
| else |
| { |
| uOff = 38; |
| dOff = 26; |
| } |
| if (curLen < 96) |
| { |
| orgCenter = edge.opos + (orgLen >> 1); |
| curPos1 = Utils.pixRound(orgCenter); |
| error1 = orgCenter - (curPos1 - uOff); |
| if (error1 < 0) |
| error1 = -error1; |
| error2 = orgCenter - (curPos1 + dOff); |
| if (error2 < 0) |
| error2 = -error2; |
| if (error1 < error2) |
| { |
| curPos1 -= uOff; |
| } |
| else |
| { |
| curPos1 += dOff; |
| } |
| edge.pos = curPos1 - curLen / 2; |
| edge2.pos = curPos1 + curLen / 2; |
| } |
| else |
| { |
| edge.pos = Utils.pixRound(edge.opos); |
| } |
| anchor = edge; |
| edge.flags |= Segment.FLAG_EDGE_DONE; |
| alignLinkedEdge(hints, dim, edge, edge2); |
| } |
| else |
| { |
| int aDiff = edge.opos - anchor.opos; |
| int orgPos = anchor.pos + aDiff; |
| int orgLen = edge2.opos - edge.opos; |
| int orgCenter = orgPos + (orgLen >> 1); |
| int curLen = computeStemWidth(hints, dim, orgLen, edge.flags, |
| edge2.flags); |
| //System.err.println("stem width: " + curLen); |
| if (curLen < 96) |
| { |
| int uOff, dOff; |
| int curPos1 = Utils.pixRound(orgCenter); |
| if (curLen <= 64) |
| { |
| uOff = 32; |
| dOff = 32; |
| } |
| else |
| { |
| uOff = 38; |
| dOff = 26; |
| } |
| int delta1 = orgCenter - (curPos1 - uOff); |
| if (delta1 < 0) |
| delta1 = -delta1; |
| int delta2 = orgCenter - (curPos1 + dOff); |
| if (delta2 < 0) |
| delta2 = -delta2; |
| if (delta1 < delta2) |
| { |
| curPos1 -= uOff; |
| } |
| else |
| { |
| curPos1 += dOff; |
| } |
| edge.pos = curPos1 - curLen / 2; |
| edge2.pos = curPos1 + curLen / 2; |
| } |
| else |
| { |
| orgPos = anchor.pos + (edge.opos - anchor.opos); |
| orgLen = edge2.opos - edge.opos; |
| orgCenter = orgPos + (orgLen >> 1); |
| curLen = computeStemWidth(hints, dim, orgLen, edge.flags, |
| edge2.flags); |
| int curPos1 = Utils.pixRound(orgPos); |
| int delta1 = curPos1 + (curLen >> 1) - orgCenter; |
| if (delta1 < 0) |
| delta1 = -delta1; |
| int curPos2 = Utils.pixRound(orgPos + orgLen) - curLen; |
| int delta2 = curPos2 + (curLen >> 1) - orgCenter; |
| if (delta2 < 0) |
| delta2 = -delta2; |
| edge.pos = (delta1 < delta2) ? curPos1 : curPos2; |
| edge2.pos = edge.pos + curLen; |
| } |
| edge.flags |= Segment.FLAG_EDGE_DONE; |
| edge2.flags |= Segment.FLAG_EDGE_DONE; |
| |
| if (e > 0 && edge.pos < edges[e - 1].pos) |
| { |
| edge.pos = edges[e - 1].pos; |
| } |
| } |
| } |
| // TODO: Implement the lowercase m symmetry thing. |
| |
| // Now we hint the remaining edges (serifs and singles) in order |
| // to complete our processing. |
| if (hasSerifs > 0 || anchor == null) |
| { |
| for (int e = 0; e < numEdges; e++) |
| { |
| Edge edge = edges[e]; |
| if ((edge.flags & Segment.FLAG_EDGE_DONE) != 0) |
| continue; |
| if (edge.serif != null) |
| { |
| alignSerifEdge(hints, edge.serif, edge); |
| } |
| else if (anchor == null) |
| { |
| edge.pos = Utils.pixRound(edge.opos); |
| anchor = edge; |
| } |
| else |
| { |
| edge.pos = anchor.pos |
| + Utils.pixRound(edge.opos - anchor.opos); |
| } |
| edge.flags |= Segment.FLAG_EDGE_DONE; |
| |
| if (e > 0 && edge.pos < edges[e - 1].pos) |
| { |
| edge.pos = edges[e - 1].pos; |
| } |
| if (e + 1 < numEdges |
| && (edges[e + 1].flags & Segment.FLAG_EDGE_DONE) != 0 |
| && edge.pos > edges[e + 1].pos) |
| { |
| edge.pos = edges[e + 1].pos; |
| } |
| } |
| } |
| |
| // Debug: print all hinted edges. |
| // System.err.println("hinted edges: " ); |
| // for (int i = 0; i < numEdges; i++) |
| // { |
| // System.err.println("edge#" + i + ": " + edges[i]); |
| // } |
| } |
| |
| private void alignSerifEdge(GlyphHints hints, Edge base, Edge serif) |
| { |
| serif.pos = base.pos + (serif.opos - base.opos); |
| } |
| |
| private int computeStemWidth(GlyphHints hints, int dim, int width, |
| int baseFlags, int stemFlags) |
| { |
| LatinMetrics metrics = (LatinMetrics) hints.metrics; |
| LatinAxis axis = metrics.axis[dim]; |
| int dist = width; |
| int sign = 0; |
| boolean vertical = dim == DIMENSION_VERT; |
| if (! doStemAdjust(hints)) |
| return width; |
| if (dist < 0) |
| { |
| dist = -width; |
| sign = 1; |
| } |
| if ((vertical && ! doVertSnap(hints)) || ! vertical && ! doHorzSnap(hints)) |
| { |
| // Smooth hinting process. Very lightly quantize the stem width. |
| // Leave the widths of serifs alone. |
| if ((stemFlags & Segment.FLAG_EDGE_SERIF) != 0 && vertical |
| && dist < 3 * 64) |
| { |
| return doneWidth(dist, sign); |
| } |
| else if ((baseFlags & Segment.FLAG_EDGE_ROUND) != 0) |
| { |
| if (dist < 80) |
| dist = 64; |
| } |
| else if (dist < 56) |
| { |
| dist = 56; |
| } |
| if (axis.widthCount > 0) |
| { |
| int delta; |
| if (axis.widthCount > 0) |
| { |
| delta = dist - axis.widths[0].cur; |
| if (delta < 0) |
| { |
| delta = -delta; |
| } |
| if (delta < 40) |
| { |
| dist = axis.widths[0].cur; |
| if (dist < 48) |
| dist = 48; |
| return doneWidth(dist, sign); |
| } |
| } |
| if (dist < 3 * 64) // < 3 pixels. |
| { |
| delta = dist & 63; |
| dist &= -64; |
| if (delta < 10) |
| dist += delta; |
| else if (delta < 32) |
| dist += 10; |
| else if (delta < 54) |
| dist += 54; |
| else |
| dist += delta; |
| |
| } |
| else |
| { |
| dist = (dist + 32) & ~63; |
| } |
| } |
| } |
| else |
| { |
| // Strong hinting process: Snap the stem width to integer pixels. |
| dist = snapWidth(axis.widths, axis.widthCount, dist); |
| if (vertical) |
| { |
| // In the case of vertical hinting, always round |
| // the stem heights to integer pixels. |
| if (dist >= 64) |
| dist = (dist + 16) & ~63; |
| else |
| dist = 64; |
| } |
| else |
| { |
| if (doMono(hints)) |
| { |
| // Monochrome horizontal hinting: Snap widths to integer pixels |
| // with a different threshold. |
| if (dist < 64) |
| dist = 64; |
| else |
| dist = (dist + 32) & ~63; |
| } |
| else |
| { |
| // For anti-aliased hinting, we adopt a more subtle |
| // approach: We strengthen small stems, round those stems |
| // whose size is between 1 and 2 pixels to an integer, |
| // otherwise nothing. |
| if (dist < 48) |
| dist = (dist + 64) >> 1; |
| else if (dist < 128) |
| dist = (dist + 22) & ~63; |
| else |
| // Round otherwise to prevent color fringes in LCD mode. |
| dist = (dist + 32) & ~63; |
| } |
| } |
| } |
| return doneWidth(dist, sign); |
| } |
| |
| private boolean doMono(GlyphHints hints) |
| { |
| return true; |
| } |
| |
| private int snapWidth(Width[] widths, int count, int width) |
| { |
| int best = 64 + 32 + 2; |
| int reference = width; |
| for (int n = 0; n < count; n++) |
| { |
| int w = widths[n].cur; |
| int dist = width - w; |
| if (dist < 0) |
| dist = -dist; |
| if (dist < best) |
| { |
| best = dist; |
| reference = w; |
| } |
| } |
| int scaled = Utils.pixRound(reference); |
| if (width >= reference) |
| { |
| if (width < scaled + 48) |
| width = reference; |
| } |
| else |
| { |
| if (width > scaled + 48) |
| width = reference; |
| } |
| return width; |
| } |
| |
| private int doneWidth(int w, int s) |
| { |
| if (s == 1) |
| w = -w; |
| return w; |
| } |
| |
| private boolean doVertSnap(GlyphHints hints) |
| { |
| // TODO Auto-generated method stub |
| return true; |
| } |
| |
| private boolean doHorzSnap(GlyphHints hints) |
| { |
| // TODO Auto-generated method stub |
| return true; |
| } |
| |
| private boolean doStemAdjust(GlyphHints hints) |
| { |
| // TODO Auto-generated method stub |
| return true; |
| } |
| |
| private void alignLinkedEdge(GlyphHints hints, int dim, Edge base, Edge stem) |
| { |
| int dist = stem.opos - base.opos; |
| int fitted = computeStemWidth(hints, dim, dist, base.flags, stem.flags); |
| stem.pos = base.pos + fitted; |
| } |
| |
| public void doneMetrics(ScriptMetrics metrics) |
| { |
| // TODO Auto-generated method stub |
| |
| } |
| |
| /** |
| * Initializes the <code>hints</code> object. |
| * |
| * @param hints the hints to initialize |
| * @param metrics the metrics to use |
| */ |
| public void initHints(GlyphHints hints, ScriptMetrics metrics) |
| { |
| hints.rescale(metrics); |
| LatinMetrics lm = (LatinMetrics) metrics; |
| hints.xScale = lm.axis[DIMENSION_HORZ].scale; |
| hints.xDelta = lm.axis[DIMENSION_HORZ].delta; |
| hints.yScale = lm.axis[DIMENSION_VERT].scale; |
| hints.yDelta = lm.axis[DIMENSION_VERT].delta; |
| // TODO: Set the scaler and other flags. |
| } |
| |
| /** |
| * Initializes the script metrics. |
| * |
| * @param metrics the script metrics to initialize |
| * @param face the font |
| */ |
| public void initMetrics(ScriptMetrics metrics, OpenTypeFont face) |
| { |
| assert metrics instanceof LatinMetrics; |
| LatinMetrics lm = (LatinMetrics) metrics; |
| lm.unitsPerEm = face.unitsPerEm; |
| |
| // TODO: Check for latin charmap. |
| |
| initWidths(lm, face, 'o'); |
| initBlues(lm, face); |
| } |
| |
| public void scaleMetrics(ScriptMetrics metrics, HintScaler scaler) |
| { |
| LatinMetrics lm = (LatinMetrics) metrics; |
| lm.scaler.renderMode = scaler.renderMode; |
| lm.scaler.face = scaler.face; |
| scaleMetricsDim(lm, scaler, DIMENSION_HORZ); |
| scaleMetricsDim(lm, scaler, DIMENSION_VERT); |
| } |
| |
| private void scaleMetricsDim(LatinMetrics lm, HintScaler scaler, int dim) |
| { |
| int scale; |
| int delta; |
| if (dim == DIMENSION_HORZ) |
| { |
| scale = scaler.xScale; |
| delta = scaler.xDelta; |
| } |
| else |
| { |
| scale = scaler.yScale; |
| delta = scaler.yDelta; |
| } |
| LatinAxis axis = lm.axis[dim]; |
| if (axis.orgScale == scale && axis.orgDelta == delta) |
| // No change, no need to adjust. |
| return; |
| axis.orgScale = scale; |
| axis.orgDelta = delta; |
| |
| // Correct X and Y scale to optimize the alignment of the top small |
| // letters to the pixel grid. |
| LatinAxis axis2 = lm.axis[DIMENSION_VERT]; |
| LatinBlue blue = null; |
| // for (int nn = 0; nn < axis2.blueCount; nn++) |
| // { |
| // if ((axis2.blues[nn].flags & LatinBlue.FLAG_ADJUSTMENT) != 0) |
| // { |
| // blue = axis2.blues[nn]; |
| // break; |
| // } |
| // } |
| // if (blue != null) |
| // { |
| // int scaled = Fixed.mul16(blue.shoot.org, scaler.yScale); |
| // int fitted = Utils.pixRound(scaled); |
| // if (scaled != fitted) |
| // { |
| // if (dim == DIMENSION_HORZ) |
| // { |
| // if (fitted < scaled) |
| // { |
| // scale -= scale / 50; |
| // } |
| // } |
| // else |
| // { |
| // scale = Utils.mulDiv(scale, fitted, scaled); |
| // } |
| // } |
| // } |
| axis.scale = scale; |
| axis.delta = delta; |
| if (dim == DIMENSION_HORZ) |
| { |
| lm.scaler.xScale = scale; |
| lm.scaler.xDelta = delta; |
| } |
| else |
| { |
| lm.scaler.yScale = scale; |
| lm.scaler.yDelta = delta; |
| } |
| // Scale the standard widths. |
| for (int nn = 0; nn < axis.widthCount; nn++) |
| { |
| Width w = axis.widths[nn]; |
| w.cur = Fixed.mul16(w.org, scale); |
| w.fit = w.cur; |
| } |
| // Scale blue zones. |
| if (dim == DIMENSION_VERT) |
| { |
| for (int nn = 0; nn < axis.blueCount; nn++) |
| { |
| blue = axis.blues[nn]; |
| blue.ref.cur = Fixed.mul16(blue.ref.org, scale) + delta; |
| blue.ref.fit = blue.ref.cur; |
| blue.shoot.cur = Fixed.mul16(blue.ref.org, scale) + delta; |
| blue.flags &= ~LatinBlue.FLAG_BLUE_ACTIVE; |
| // A blue zone is only active if it is less than 3/4 pixels tall. |
| int dist = Fixed.mul16(blue.ref.org - blue.shoot.org, scale); |
| if (dist <= 48 && dist >= -48) |
| { |
| int delta1 = blue.shoot.org - blue.ref.org; |
| int delta2 = delta1; |
| if (delta1 < 0) |
| delta2 = -delta2; |
| delta2 = Fixed.mul16(delta2, scale); |
| if (delta2 < 32) |
| delta2 = 0; |
| else if (delta2 < 64) |
| delta2 = 32 + (((delta2 - 32) + 16) & ~31); |
| else |
| delta2 = Utils.pixRound(delta2); |
| if (delta1 < 0) |
| delta2 = -delta2; |
| blue.ref.fit = Utils.pixRound(blue.ref.cur); |
| blue.shoot.fit = blue.ref.fit + delta2; |
| blue.flags |= LatinBlue.FLAG_BLUE_ACTIVE; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Determines the standard stem widths. |
| * |
| * @param metrics the metrics to use |
| * @param face the font face |
| * @param ch the character that is used for getting the widths |
| */ |
| private void initWidths(LatinMetrics metrics, OpenTypeFont face, char ch) |
| { |
| GlyphHints hints = new GlyphHints(); |
| metrics.axis[DIMENSION_HORZ].widthCount = 0; |
| metrics.axis[DIMENSION_VERT].widthCount = 0; |
| int glyphIndex = face.getGlyph(ch); |
| Zone outline = face.getRawGlyphOutline(glyphIndex, IDENTITY); |
| LatinMetrics dummy = new LatinMetrics(); |
| HintScaler scaler = dummy.scaler; |
| dummy.unitsPerEm = metrics.unitsPerEm; |
| scaler.xScale = scaler.yScale = 10000; |
| scaler.xDelta = scaler.yDelta = 0; |
| scaler.face = face; |
| hints.rescale(dummy); |
| hints.reload(outline); |
| for (int dim = 0; dim < DIMENSION_MAX; dim++) |
| { |
| LatinAxis axis = metrics.axis[dim]; |
| AxisHints axHints = hints.axis[dim]; |
| int numWidths = 0; |
| computeSegments(hints, dim); |
| linkSegments(hints, dim); |
| Segment[] segs = axHints.segments; |
| HashSet<Segment> touched = new HashSet<Segment>(); |
| for (int i = 0; i < segs.length; i++) |
| { |
| Segment seg = segs[i]; |
| Segment link = seg.link; |
| if (link != null && link.link == seg && ! touched.contains(link)) |
| { |
| int dist = Math.abs(seg.pos - link.pos); |
| if (numWidths < MAX_WIDTHS) |
| axis.widths[numWidths++] = new Width(dist); |
| } |
| touched.add(seg); |
| } |
| Utils.sort(numWidths, axis.widths); |
| axis.widthCount = numWidths; |
| } |
| for (int dim = 0; dim < DIMENSION_MAX; dim++) |
| { |
| LatinAxis axis = metrics.axis[dim]; |
| int stdw = axis.widthCount > 0 ? axis.widths[0].org |
| : constant(metrics, 50); |
| axis.edgeDistanceTreshold= stdw / 5; |
| } |
| } |
| |
| void linkSegments(GlyphHints hints, int dim) |
| { |
| AxisHints axis = hints.axis[dim]; |
| Segment[] segments = axis.segments; |
| int numSegs = axis.numSegments; |
| int majorDir = axis.majorDir; |
| int lenThreshold = constant((LatinMetrics) hints.metrics, 8); |
| lenThreshold = Math.min(1, lenThreshold); |
| int lenScore = constant((LatinMetrics) hints.metrics, 3000); |
| for (int i1 = 0; i1 < numSegs; i1++) |
| { |
| Segment seg1 = segments[i1]; |
| // The fake segments are introduced to hint the metrics. |
| // Never link them to anything. |
| if (seg1.first == seg1.last || seg1.dir != majorDir) |
| continue; |
| for (int i2 = 0; i2 < numSegs; i2++) |
| { |
| Segment seg2 = segments[i2]; |
| if (seg2 != seg1 && seg1.dir + seg2.dir == 0) |
| { |
| int pos1 = seg1.pos; |
| int pos2 = seg2.pos; |
| // The vertical coords are swapped compared to how FT handles |
| // this. |
| int dist = dim == DIMENSION_VERT ? pos1 - pos2 : pos2 - pos1; |
| if (dist >= 0) |
| { |
| int min = seg1.minPos; |
| int max = seg1.maxPos; |
| int len, score; |
| if (min < seg2.minPos) |
| min = seg2.minPos; |
| if (max > seg2.maxPos) |
| max = seg2.maxPos; |
| len = max - min; |
| if (len > lenThreshold) |
| { |
| score = dist + lenScore / len; |
| if (score < seg1.score) |
| { |
| seg1.score = score; |
| seg1.link = seg2; |
| } |
| if (score < seg2.score) |
| { |
| seg2.score = score; |
| seg2.link = seg1; |
| } |
| } |
| } |
| } |
| } |
| } |
| for (int i1 = 0; i1 < numSegs; i1++) |
| { |
| Segment seg1 = segments[i1]; |
| Segment seg2 = seg1.link; |
| if (seg2 != null) |
| { |
| seg2.numLinked++; |
| if (seg2.link != seg1) |
| { |
| seg1.link = null; |
| seg1.serif = seg2.link; |
| } |
| } |
| // Uncomment to show all segments. |
| // System.err.println("segment#" + i1 + ": " + seg1); |
| } |
| } |
| |
| /** |
| * Initializes the blue zones of the font. |
| * |
| * @param metrics the metrics to use |
| * @param face the font face to analyze |
| */ |
| private void initBlues(LatinMetrics metrics, OpenTypeFont face) |
| { |
| int[] flats = new int[MAX_TEST_CHARS]; |
| int[] rounds = new int[MAX_TEST_CHARS]; |
| int numFlats; |
| int numRounds; |
| LatinBlue blue; |
| LatinAxis axis = metrics.axis[DIMENSION_VERT]; |
| // We compute the blues simply by loading each character in the test |
| // strings, then compute its topmost or bottommost points. |
| for (int bb = 0; bb < BLUE_MAX; bb++) |
| { |
| String p = TEST_CHARS[bb]; |
| int blueRef; |
| int blueShoot; |
| numFlats = 0; |
| numRounds = 0; |
| for (int i = 0; i < p.length(); i++) |
| { |
| // Load the character. |
| int glyphIndex = face.getGlyph(p.charAt(i)); |
| Zone glyph = |
| face.getRawGlyphOutline(glyphIndex, IDENTITY); |
| |
| // Now compute the min and max points. |
| int numPoints = glyph.getSize() - 4; // 4 phantom points. |
| Point[] points = glyph.getPoints(); |
| Point point = points[0]; |
| int extremum = 0; |
| int index = 1; |
| if (isTopBlue(bb)) |
| { |
| for (; index < numPoints; index++) |
| { |
| point = points[index]; |
| // We have the vertical direction swapped. The higher |
| // points have smaller (negative) Y. |
| if (point.getOrigY() < points[extremum].getOrigY()) |
| extremum = index; |
| } |
| } |
| else |
| { |
| for (; index < numPoints; index++) |
| { |
| point = points[index]; |
| // We have the vertical direction swapped. The higher |
| // points have smaller (negative) Y. |
| if (point.getOrigY() > points[extremum].getOrigY()) |
| extremum = index; |
| } |
| } |
| // Debug, prints out the maxima. |
| // System.err.println("extremum for " + bb + " / "+ p.charAt(i) |
| // + ": " + points[extremum]); |
| |
| // Now determine if the point is part of a straight or round |
| // segment. |
| boolean round; |
| int idx = extremum; |
| int first, last, prev, next, end; |
| int dist; |
| last = -1; |
| first = 0; |
| for (int n = 0; n < glyph.getNumContours(); n++) |
| { |
| end = glyph.getContourEnd(n); |
| // System.err.println("contour end for " + n + ": " + end); |
| if (end >= idx) |
| { |
| last = end; |
| break; |
| } |
| first = end + 1; |
| } |
| // Should never happen. |
| assert last >= 0; |
| |
| // Now look for the previous and next points that are not on the |
| // same Y coordinate. Threshold the 'closeness'. |
| prev = idx; |
| next = prev; |
| do |
| { |
| if (prev > first) |
| prev--; |
| else |
| prev = last; |
| dist = points[prev].getOrigY() - points[extremum].getOrigY(); |
| if (dist < -5 || dist > 5) |
| break; |
| } while (prev != idx); |
| do |
| { |
| if (next < last) |
| next++; |
| else |
| next = first; |
| dist = points[next].getOrigY() - points[extremum].getOrigY(); |
| if (dist < -5 || dist > 5) |
| break; |
| } while (next != idx); |
| round = points[prev].isControlPoint() |
| || points[next].isControlPoint(); |
| |
| if (round) |
| { |
| rounds[numRounds++] = points[extremum].getOrigY(); |
| // System.err.println("new round extremum: " + bb + ": " |
| // + points[extremum].getOrigY()); |
| } |
| else |
| { |
| flats[numFlats++] = points[extremum].getOrigY(); |
| // System.err.println("new flat extremum: " + bb + ": " |
| // + points[extremum].getOrigY()); |
| } |
| } |
| // We have computed the contents of the rounds and flats tables. |
| // Now determine the reference and overshoot position of the blues -- |
| // we simply take the median after a simple sort. |
| Utils.sort(numRounds, rounds); |
| Utils.sort(numFlats, flats); |
| blue = axis.blues[axis.blueCount] = new LatinBlue(); |
| axis.blueCount++; |
| if (numFlats == 0) |
| { |
| blue.ref = blue.shoot = new Width(rounds[numRounds / 2]); |
| } |
| else if (numRounds == 0) |
| { |
| blue.ref = blue.shoot = new Width(flats[numFlats / 2]); |
| } |
| else |
| { |
| blue.ref = new Width(flats[numFlats / 2]); |
| blue.shoot = new Width(rounds[numRounds / 2]); |
| } |
| // There are sometimes problems: if the overshoot position of top |
| // zones is under its reference position, or the opposite for bottom |
| // zones. We must check everything there and correct problems. |
| if (blue.shoot != blue.ref) |
| { |
| int ref = blue.ref.org; |
| int shoot = blue.shoot.org; |
| // Inversed vertical coordinates! |
| boolean overRef = shoot < ref; |
| if (isTopBlue(bb) ^ overRef) |
| { |
| blue.shoot = blue.ref = new Width((shoot + ref) / 2); |
| } |
| } |
| blue.flags = 0; |
| if (isTopBlue(bb)) |
| blue.flags |= LatinBlue.FLAG_TOP; |
| // The following flag is used later to adjust y and x scales in |
| // order to optimize the pixel grid alignment of the top small |
| // letters. |
| if (bb == SMALL_TOP) |
| { |
| blue.flags |= LatinBlue.FLAG_ADJUSTMENT; |
| } |
| // Debug: print out the blue zones. |
| // System.err.println("blue zone #" + bb + ": " + blue); |
| } |
| } |
| |
| private static final AffineTransform IDENTITY = new AffineTransform(); |
| |
| private int constant(LatinMetrics metrics, int c) |
| { |
| return c * (metrics.unitsPerEm / 2048); |
| } |
| |
| private void computeSegments(GlyphHints hints, int dim) |
| { |
| Point[] points = hints.points; |
| if (dim == DIMENSION_HORZ) |
| { |
| for (int i = 0; i < hints.numPoints; i++) |
| { |
| points[i].setU(points[i].getOrigX()); |
| points[i].setV(points[i].getOrigY()); |
| } |
| } |
| else |
| { |
| for (int i = 0; i < hints.numPoints; i++) |
| { |
| points[i].setU(points[i].getOrigY()); |
| points[i].setV(points[i].getOrigX()); |
| } |
| } |
| // Now look at each contour. |
| AxisHints axis = hints.axis[dim]; |
| int majorDir = Math.abs(axis.majorDir); |
| int segmentDir = majorDir; |
| Point[] contours = hints.contours; |
| int numContours = hints.numContours; |
| Segment segment = null; |
| for (int i = 0; i < numContours; i++) |
| { |
| int minPos = 32000; |
| int maxPos = -32000; |
| |
| Point point = contours[i]; |
| Point last = point.getPrev(); |
| if (point == last) // Skip singletons. |
| continue; |
| if (Math.abs(last.getOutDir()) == majorDir |
| && Math.abs(point.getOutDir()) == majorDir) |
| { |
| // We are already on an edge. Locate its start. |
| last = point; |
| while (true) |
| { |
| point = point.getPrev(); |
| if (Math.abs(point.getOutDir()) != majorDir) |
| { |
| point = point.getNext(); |
| break; |
| } |
| if (point == last) |
| break; |
| } |
| } |
| last = point; |
| boolean passed = false; |
| boolean onEdge = false; |
| while (true) |
| { |
| int u, v; |
| if (onEdge) |
| { |
| u = point.getU(); |
| if (u < minPos) |
| minPos = u; |
| if (u > maxPos) |
| maxPos = u; |
| if (point.getOutDir() != segmentDir || point == last) |
| { |
| // Leaving an edge. Record new segment. |
| segment.last = point; |
| // (minPos + maxPos) / 2. |
| segment.pos = (minPos + maxPos) >> 1; |
| if (segment.first.isControlPoint() |
| || point.isControlPoint()) |
| segment.flags |= Segment.FLAG_EDGE_ROUND; |
| minPos = maxPos = point.getV(); |
| v = segment.first.getV(); |
| if (v < minPos) |
| minPos = v; |
| if (v > maxPos) |
| maxPos = v; |
| segment.minPos = minPos; |
| segment.maxPos = maxPos; |
| onEdge = false; |
| segment = null; |
| } |
| } |
| if (point == last) |
| { |
| if (passed) |
| break; |
| passed = true; |
| } |
| if (! onEdge && Math.abs(point.getOutDir()) == majorDir) |
| { |
| // This is the start of a new segment. |
| segmentDir = point.getOutDir(); |
| segment = axis.newSegment(); |
| segment.dir = segmentDir; |
| segment.flags = Segment.FLAG_EDGE_NORMAL; |
| minPos = maxPos = point.getU(); |
| segment.first = point; |
| segment.last = point; |
| segment.contour = contours[i]; |
| segment.score = 32000; |
| segment.len = 0; |
| segment.link = null; |
| onEdge = true; |
| } |
| point = point.getNext(); |
| } |
| } |
| |
| } |
| |
| private boolean isTopBlue(int b) |
| { |
| return b == CAPITAL_TOP || b == SMALL_F_TOP || b == SMALL_TOP; |
| } |
| |
| private void detectFeatures(GlyphHints hints, int dim) |
| { |
| computeSegments(hints, dim); |
| linkSegments(hints, dim); |
| computeEdges(hints, dim); |
| } |
| |
| private void computeEdges(GlyphHints hints, int dim) |
| { |
| AxisHints axis = hints.axis[dim]; |
| LatinAxis laxis = ((LatinMetrics) hints.metrics).axis[dim]; |
| Segment[] segments = axis.segments; |
| int numSegments = axis.numSegments; |
| Segment seg; |
| int upDir; |
| int scale; |
| int edgeDistanceThreshold; |
| axis.numEdges = 0; |
| scale = dim == DIMENSION_HORZ ? hints.xScale : hints.yScale; |
| upDir = dim == DIMENSION_HORZ ? DIR_UP : DIR_RIGHT; |
| |
| // We will begin by generating a sorted table of edges for the |
| // current direction. To do so, we simply scan each segment and try |
| // to find an edge in our table that corresponds to its position. |
| // |
| // If no edge is found, we create one and insert a new edge in the |
| // sorted table. Otherwise, we simply add the segment to the egde's |
| // list which will be processed in the second step to compute the |
| // edge's properties. |
| // |
| // Note that the edge table is sorted along the segment/edge |
| // position. |
| |
| edgeDistanceThreshold = Fixed.mul16(laxis.edgeDistanceTreshold, scale); |
| if (edgeDistanceThreshold > 64 / 4) |
| edgeDistanceThreshold = 64 / 4; |
| edgeDistanceThreshold = Fixed.div16(edgeDistanceThreshold, scale); |
| for (int i = 0; i < numSegments; i++) |
| { |
| seg = segments[i]; |
| Edge found = null; |
| for (int ee = 0; ee < axis.numEdges; ee++) |
| { |
| Edge edge = axis.edges[ee]; |
| int dist = seg.pos - edge.fpos; |
| if (dist < 0) |
| dist = -dist; |
| if (dist < edgeDistanceThreshold) |
| { |
| found = edge; |
| break; |
| } |
| } |
| if (found == null) |
| { |
| // Insert new edge in the list and sort according to |
| // the position. |
| Edge edge = axis.newEdge(seg.pos); |
| edge.first = seg; |
| edge.last = seg; |
| edge.fpos = seg.pos; |
| edge.opos = edge.pos = Fixed.mul16(seg.pos, scale); |
| seg.edgeNext = seg; |
| seg.edge = edge; |
| } |
| else |
| { |
| seg.edgeNext = found.first; |
| found.last.edgeNext = seg; |
| found.last = seg; |
| seg.edge = found; |
| } |
| } |
| // Good. We will now compute each edge's properties according to |
| // segments found on its position. Basically these are: |
| // - Edge's main direction. |
| // - Stem edge, serif edge, or both (which defaults to stem edge). |
| // - Rounded edge, straight or both (which defaults to straight). |
| // - Link for edge. |
| |
| // Now, compute each edge properties. |
| for (int e = 0; e < axis.numEdges; e++) |
| { |
| Edge edge = axis.edges[e]; |
| // Does it contain round segments? |
| int isRound = 0; |
| // Does it contain straight segments? |
| int isStraight = 0; |
| // Number of upward segments. |
| int ups = 0; |
| // Number of downward segments. |
| int downs = 0; |
| |
| seg = edge.first; |
| do |
| { |
| // Check for roundness of segment. |
| if ((seg.flags & Segment.FLAG_EDGE_ROUND) != 0) |
| isRound++; |
| else |
| isStraight++; |
| |
| // Check for segment direction. |
| if (seg.dir == upDir) |
| ups += seg.maxPos - seg.minPos; |
| else |
| downs += seg.maxPos - seg.minPos; |
| |
| // Check for links. If seg.serif is set, then seg.link must |
| // be ignored. |
| boolean isSerif = seg.serif != null && seg.serif.edge != edge; |
| if (seg.link != null || isSerif) |
| { |
| Edge edge2 = edge.link; |
| Segment seg2 = seg.link; |
| if (isSerif) |
| { |
| seg2 = seg.serif; |
| edge2 = edge.serif; |
| } |
| if (edge2 != null) |
| { |
| int edgeDelta = edge.fpos - edge2.fpos; |
| if (edgeDelta < 0) |
| edgeDelta = -edgeDelta; |
| int segDelta = seg.pos - seg2.pos; |
| if (segDelta < 0) |
| segDelta = -segDelta; |
| if (segDelta < edgeDelta) |
| edge2 = seg2.edge; |
| } |
| else |
| { |
| edge2 = seg2.edge; |
| } |
| if (isSerif) |
| { |
| edge.serif = edge2; |
| edge2.flags |= Segment.FLAG_EDGE_SERIF; |
| } |
| else |
| { |
| edge.link = edge2; |
| } |
| } |
| seg = seg.edgeNext; |
| } while (seg != edge.first); |
| edge.flags = Segment.FLAG_EDGE_NORMAL; |
| if (isRound > 0 && isRound > isStraight) |
| edge.flags |= Segment.FLAG_EDGE_ROUND; |
| |
| // Set the edge's main direction. |
| edge.dir = DIR_NONE; |
| if (ups > downs) |
| edge.dir = upDir; |
| else if (ups < downs) |
| edge.dir = -upDir; |
| else if (ups == downs) |
| edge.dir = 0; |
| |
| // Gets rid of serif if link is set. This gets rid of many |
| // unpleasant artifacts. |
| if (edge.serif != null && edge.link != null) |
| { |
| edge.serif = null; |
| } |
| |
| // Debug: Print out all edges. |
| // System.err.println("edge# " + e + ": " + edge); |
| } |
| } |
| |
| private void computeBlueEdges(GlyphHints hints, LatinMetrics metrics) |
| { |
| AxisHints axis = hints.axis[DIMENSION_VERT]; |
| Edge[] edges = axis.edges; |
| int numEdges = axis.numEdges; |
| LatinAxis latin = metrics.axis[DIMENSION_VERT]; |
| int scale = latin.scale; |
| |
| // Compute which blue zones are active. I.e. have their scaled |
| // size < 3/4 pixels. |
| |
| // For each horizontal edge search the blue zone that is closest. |
| for (int e = 0; e < numEdges; e++) |
| { |
| Edge edge = edges[e]; |
| // System.err.println("checking edge: " + edge); |
| Width bestBlue = null; |
| int bestDist = Fixed.mul16(metrics.unitsPerEm / 40, scale); |
| |
| if (bestDist > 64 / 2) |
| bestDist = 64 / 2; |
| for (int bb = 0; bb < BLUE_MAX; bb++) |
| { |
| LatinBlue blue = latin.blues[bb]; |
| // System.err.println("checking blue: " + blue); |
| // Skip inactive blue zones, i.e. those that are too small. |
| if ((blue.flags & LatinBlue.FLAG_BLUE_ACTIVE) == 0) |
| continue; |
| // If it is a top zone, check for right edges. If it is a bottom |
| // zone, check for left edges. |
| boolean isTopBlue = (blue.flags & LatinBlue.FLAG_TOP) != 0; |
| boolean isMajorDir = edge.dir == axis.majorDir; |
| |
| // If it is a top zone, the edge must be against the major |
| // direction. If it is a bottom zone it must be in the major |
| // direction. |
| if (isTopBlue ^ isMajorDir) |
| { |
| int dist = edge.fpos - blue.ref.org; |
| if (dist < 0) |
| dist = -dist; |
| dist = Fixed.mul16(dist, scale); |
| if (dist < bestDist) |
| { |
| bestDist = dist; |
| bestBlue = blue.ref; |
| } |
| |
| // Now, compare it to the overshoot position if the edge is |
| // rounded, and if the edge is over the reference position of |
| // a top zone, or under the reference position of a bottom |
| // zone. |
| if ((edge.flags & Segment.FLAG_EDGE_ROUND) != 0 && dist != 0) |
| { |
| // Inversed vertical coordinates! |
| boolean isUnderRef = edge.fpos > blue.ref.org; |
| if (isTopBlue ^ isUnderRef) |
| { |
| blue = latin.blues[bb]; // Needed? |
| dist = edge.fpos - blue.shoot.org; |
| if (dist < 0) |
| dist = -dist; |
| dist = Fixed.mul16(dist, scale); |
| if (dist < bestDist) |
| { |
| bestDist = dist; |
| bestBlue = blue.shoot; |
| } |
| } |
| } |
| |
| } |
| } |
| if (bestBlue != null) |
| { |
| edge.blueEdge = bestBlue; |
| // Debug: Print out the blue edges. |
| // System.err.println("blue edge for: " + edge + ": " + bestBlue); |
| } |
| } |
| } |
| } |