All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.fontengine.font.StemFinder Maven / Gradle / Ivy

There is a newer version: 2024.11.18598.20241113T125352Z-241000
Show newest version
/*
*
*	File: StemFinder.java
*
*
*	ADOBE CONFIDENTIAL
*	___________________
*
*	Copyright 2004-2005 Adobe Systems Incorporated
*	All Rights Reserved.
*
*	NOTICE: All information contained herein is, and remains the property of
*	Adobe Systems Incorporated and its suppliers, if any. The intellectual
*	and technical concepts contained herein are proprietary to Adobe Systems
*	Incorporated and its suppliers and may be covered by U.S. and Foreign
*	Patents, patents in process, and are protected by trade secret or
*	copyright law. Dissemination of this information or reproduction of this
*	material is strictly forbidden unless prior written permission is obtained
*	from Adobe Systems Incorporated.
*
*/
package com.adobe.fontengine.font;

import java.util.List;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Comparator;
import java.util.Collections;
import java.util.Iterator;

import com.adobe.fontengine.font.Point;

/**
 * Given unhinted cubic Beziers, derive a primary stem value.
 * 
 * The returned stem is scaled to 1000 ppem.
 * 
 * This class tries to find either horizontal or vertical primary
 * stem values. The terminology in this description is geared toward
 * finding vertical stems, but the algorithm also works for horizontal
 * stems.
 * 
 * Here is the algorithm:
 * 1) Each curve is approximated as a series of 3 lines between
 * the 4 control points.
 * 2) For each line, the direction is examined to determine
 * whether it is on the left or the right of the fill. They are then added
 * to the list of "lefts" or "rights."
 * 3) If 2 lines are continuations of each other, they are merged to form one
 * longer line.
 * 4) We walk the list of rights. If a right is too angled to be
 * considered a stem, it is thrown out. 
 * 5) Repeat step 5 looking at the list of lefts.
 * 6) The lists of lefts and rights are sorted left-to-right (based on their
 * right most point) then top-to-bottom (based on their
 * top-most point).
 * 7) Not implemented: An attempt to deal "correctly" with shadow/outline fonts: if there is
 * a small counter inside a fill and one side of the fill is small, throw out the
 * lines that make up the counter, so the stem goes from the far left to the far right.
 * This step is only taken if 'hintsForFauxing' is true.
 * 8) If a left and right edge overlap enough and the width between them is a "reasonable"
 * stem value, add the width to a running average. If no "good stems" are found, retry looking
 * for any "stems".
 * 
 * 

Concurrency

* * Instances of this class are not threadsafe. If they are to be used in * multiple threads, the client must ensure it is done safely. * */ final public class StemFinder implements OutlineConsumer { private final boolean findVerticalStem; private final boolean hintsForFauxing; private List leftEdges = new LinkedList(); private List rightEdges = new LinkedList(); private List removedEdges = new ArrayList(); private double currentX; private double currentY; private Matrix currentMatrix; // matrix associated with currentX/Y private Matrix lastMatrixSet; private final static Matrix thousand = new Matrix(1000,0,0,1000,0,0); private static class EdgeComparator implements Comparator { private EdgeComparator() {} static final EdgeComparator comparator = new EdgeComparator(); public int compare(Object o1, Object o2) { Edge e1 = (Edge)o1; Edge e2 = (Edge)o2; if (e1.endStemDir < e2.endStemDir || (e1.endStemDir == e2.endStemDir && e1.endOppositeDir < e2.endOppositeDir)) return -1; if (e1.endOppositeDir > e2.endOppositeDir || e1.endStemDir > e2.endStemDir) return 1; return 0; } } private static class Edge { // these are all of the "tweaking" parameters for this algorithm. private final static int SHORT_STEM = 130; private final static int SHORT_OVERLAP = 50; final static double ANGLE_VARIANCE_ALLOWED = .22; // ~12 degrees final static double MAX_WIDTH_PERCENTAGE = 0.8; final static double MAX_STEM = 450; final static int MAX_STEM_VARIANCE = 60; final static int MIN_LENGTH_FOR_STEM_OVERRIDE = 100; // All points in the edge are scaled to 1000 ppem. // for vertical stems, the "stem dir" is the x direction // and the "opposite dir" is the y direction. // for horizontal stems, the "stem dir" is the y direction // and the "opposite dir" is the x direction. // "start" is bottom/left. "end" is top/right. double startStemDir; double startOppositeDir; double endStemDir; double endOppositeDir; // x = my + c (where x == stem direction, y = opposite direction) final double m; final double c; final boolean positiveAngle; Edge(double startStemDir, double startOppositeDir, double endStemDir, double endOppositeDir, boolean positiveAngle) { this.startStemDir = startStemDir; this.startOppositeDir = startOppositeDir; this.endStemDir = endStemDir; this.endOppositeDir = endOppositeDir; // assumes that we have already checked that startOppositeDir != endOppositeDir this.m = (startStemDir - endStemDir)/ (startOppositeDir - endOppositeDir); this.c = startStemDir - startOppositeDir *((startStemDir - endStemDir)/(startOppositeDir - endOppositeDir)); this.positiveAngle = positiveAngle; } private static boolean almostEqual(double e1, double e2) { return Math.abs(e1 - e2) < 0.0001; } private boolean endpointsMeet(Edge e) { // given that 2 edges have the same formula, if they touch at an "y" then the lines are continuations return (almostEqual(startOppositeDir, e.endOppositeDir) || almostEqual(endOppositeDir, e.startOppositeDir)); } private boolean sameFormula(Edge e) { return (almostEqual(m, e.m) && almostEqual(c, e.c)); } double getStemPosGivenOppPos(double opposite) { return m * opposite + c; } boolean continuation(Edge e) { if (sameFormula(e) && endpointsMeet(e)) { if (e.endOppositeDir > this.endOppositeDir) this.endOppositeDir = e.endOppositeDir; if (e.startOppositeDir < this.startOppositeDir) this.startOppositeDir = e.startOppositeDir; if (e.endStemDir > this.endStemDir) this.endStemDir = e.endStemDir; if (e.startStemDir < this.startStemDir) this.startStemDir = e.startStemDir; return true; } return false; } static boolean isShortOverlap(double endOppositeDir, double startOppositeDir) { return (endOppositeDir - startOppositeDir < SHORT_OVERLAP); } static boolean isShort(double endOppositeDir, double startOppositeDir) { return (endOppositeDir - startOppositeDir < SHORT_STEM); } } /** * @param findVerticalStem The stem finder should look for a vertical stem. * @param hintsForFauxing The stem being sought will be used in fauxing. */ public StemFinder(boolean findVerticalStem, boolean hintsForFauxing) { this.findVerticalStem = findVerticalStem; this.hintsForFauxing = hintsForFauxing; } private void addEdge(double ss, double os, double se, double oe, boolean positiveAngle, List edgeList) { edgeList.add(new Edge(ss, os, se, oe, positiveAngle)); } /** * Reset this object so it can be used to compute another stem. */ public void reset() { leftEdges.clear(); rightEdges.clear(); removedEdges.clear(); } /* (non-Javadoc) * @see com.adobe.fontengine.font.OutlineConsumer#setMatrix(com.adobe.fontengine.font.Matrix) */ public void setMatrix(Matrix newMatrix) { lastMatrixSet = newMatrix.multiply(thousand); } /* (non-Javadoc) * @see com.adobe.fontengine.font.OutlineConsumer#moveto(double, double) */ public void moveto(double x, double y) { currentX = x; currentY = y; currentMatrix = lastMatrixSet; } /* (non-Javadoc) * @see com.adobe.fontengine.font.OutlineConsumer#lineto(double, double) */ public void lineto(double x, double y) { addLine(currentX, currentY, x, y); currentX = x; currentY = y; currentMatrix = lastMatrixSet; } private void addLine(double x1, double y1, double x2, double y2) { x1 = currentMatrix.applyToXYGetX(x1,y1); y1 = currentMatrix.applyToXYGetY(x1, y1); x2 = lastMatrixSet.applyToXYGetX(x2,y2); y2 = lastMatrixSet.applyToXYGetY(x2, y2); // throw out horizontal lines (vertical if doing horizontal stems) if ((findVerticalStem && y1 == y2)|| (!findVerticalStem && x1 == x2)) return; if (findVerticalStem) { // right edge if (y1 < y2) if (x1 < x2) addEdge(x1, y1, x2, y2, false, rightEdges); else addEdge(x2, y1, x1, y2, true, rightEdges); // left edge else if (x1 < x2) addEdge(x1, y2, x2, y1,true, leftEdges); else addEdge(x2, y2, x1, y1, false, leftEdges); } else { if (x1 < x2) if (y1 < y2) addEdge(y1, x1, y2, x2, false, rightEdges); else addEdge(y2, x1, y1, x2, true, leftEdges); else if (y1 < y2) addEdge(y1, x2, y2, x1, true, rightEdges); else addEdge(y2, x2, y1, x1, false, leftEdges); } } public void curveto (double x1, double y1, double x2, double y2) { curveto (Math.round ((currentX + 2*x1)/3.0), Math.round ((currentY + 2*y1)/3.0), Math.round ((2*x1 + x2)/3.0), Math.round ((2*y1 + y2)/3.0), x2, y2); } public void curveto(double x2, double y2, double x3, double y3, double x4, double y4) { addLine(currentX, currentY, x2, y2); addLine(x2, y2, x3, y3); addLine(x3, y3, x4, y4); currentX = x4; currentY = y4; currentMatrix = lastMatrixSet; } /** * If 2 edges are extensions of each other, merge them into one edge. * @param edges a List of Edges, all of which point in the same direction. */ private void mergeLines(List edges) { int index; for (index = 0; index < edges.size(); index++) { Edge e1 = (Edge)edges.get(index); ListIterator iter = edges.listIterator(); while (iter.hasNext()) { int nextIndex = iter.nextIndex(); Edge e2 = (Edge)iter.next(); if (e1 == e2) continue; if (e1.continuation(e2)) { if (nextIndex < index) index--; iter.remove(); } } } } private void mergeLines() { mergeLines(leftEdges); mergeLines(rightEdges); } private void removeInnerCounters() { // XXX_lmb not clear that this will work or improve things... } /** * Remove edges that have too great of a slope to be considered stems * @param edges A list of Edges, all of which point in the same direction * @param italicAngle The italicAngle of the font if vertical stems are being computed. * @param addToRejectedList If true, any removed edges will be added to this.removedEdges */ private void removeAngledLines(List edges, double italicAngle, boolean addToRejectedList) { ListIterator iter = edges.listIterator(); double allowedTan; // find the "ideal" angle that stems will be at. if (findVerticalStem) allowedTan = Math.tan(Math.toRadians(italicAngle)); else allowedTan = 0.15; while (iter.hasNext()) { Edge e = (Edge)iter.next(); double actualTan; boolean rightDirection; // find the angle of this edge. if (this.findVerticalStem) { actualTan = -(e.endStemDir - e.startStemDir)/(e.endOppositeDir - e.startOppositeDir); rightDirection = italicAngle == 0 ? true : italicAngle > 0 ? e.positiveAngle : !e.positiveAngle; } else { actualTan = (e.endOppositeDir - e.startOppositeDir)/(e.endStemDir - e.startStemDir); rightDirection = true; } // if the angle is too out of whack with the italic angle, remove it. if (!rightDirection || actualTan < allowedTan - Edge.ANGLE_VARIANCE_ALLOWED || actualTan > allowedTan + Edge.ANGLE_VARIANCE_ALLOWED) { if (addToRejectedList) { removedEdges.add(e); } iter.remove(); } } } private void removeIrrelevantLines(double italicAngle) { removeAngledLines(leftEdges, italicAngle, false); removeAngledLines(rightEdges, italicAngle, true); if (hintsForFauxing) removeInnerCounters(); } /** * is a random point on e contained between left and right, top and bottom. * If both ends of e are on an edge of the box, return true. If one end of * e is in the box, return true. Otherwise, return false. */ private boolean partiallyContained(Edge e, Edge left, Edge right, double top, double bottom) { double x1, y1; double x2, y2; double x,y; int i; boolean firstOnEdge = false; if (findVerticalStem) { x1 = e.startStemDir; y1 = e.positiveAngle ? e.endOppositeDir: e.startOppositeDir; x2 = e.endStemDir; y2 = e.positiveAngle ? e.startOppositeDir: e.endOppositeDir; } else { // XXX_lmb fix this whole routine for horizontal stems x1 = y1 = x2 = y2 = 0; } // try both ends. if neither are in the region, return false. for (i = 0, x = x1, y = y1; i < 2; i++, x = x2, y = y2) { double leftX, rightX; if (y > top || y < bottom) { continue; } leftX = left.getStemPosGivenOppPos(y); rightX = right.getStemPosGivenOppPos(y); if ( leftX > x || rightX < x) continue; if (leftX == x || rightX == x || top == y || bottom == y) { // if both ends of e are on the edge, return true. if (!firstOnEdge) { firstOnEdge = true; continue; } } return true; } return false; } /** * Check whether e crosses y between left and right (not inclusive of left or right). */ private boolean crossesHorizontalLine(Edge e, Edge left, Edge right, double y) { if (e.endOppositeDir < y || e.startOppositeDir > y) return false; double x = e.getStemPosGivenOppPos(y); /* this doesn't return true when e goes through the corners */ if (left.getStemPosGivenOppPos(y) >= x || right.getStemPosGivenOppPos(y) <= x) return false; return true; } /** * does e intersect cross between top and bottom? e is assumed to be a right edge. * Returns true even if e crosses at top or bottom. */ private boolean crossesEdge(Edge e, Edge cross, double top, double bottom, boolean isLeft) { // parallel lines don't cross. if (e.m == cross.m) return false; // y is the point of intersection in the non-stem direction double y = (e.c - cross.c) / (e.m - cross.m); if (y < bottom || y > top || y < bottom || y > top) return false; // if e hits a corner, check the relative slopes to see if it goes in // the box or not. if (y == bottom || y == top) { if (isLeft) { if (e.m > cross.m) { return true; } }else { if (e.m < cross.m) return true; } return false; } return true; } private boolean intermediateRemovedEdge(Edge left, Edge right, double top, double bottom) { Iterator iter = removedEdges.iterator(); while (iter.hasNext()) { Edge e = (Edge)iter.next(); if (e.endOppositeDir > bottom && e.startOppositeDir < top && e.startStemDir < right.endStemDir && e.endStemDir > left.startStemDir) { // at first glance, e looks like an intermediate edge. is it really? it either // must be entirely contained between left and right or it must intersect one of // top, bottom, left or right. // XXX_lmb fix for horizontal stems if (partiallyContained(e, left, right, top, bottom) || crossesHorizontalLine(e, left, right, top) || crossesHorizontalLine(e, left, right, bottom) || crossesEdge(e, left, top, bottom, true) || crossesEdge(e, right, top, bottom, false)) return true; } } return false; } private double averageWidth(double advanceWidth, double italicAngle, boolean filterValues) { double average = 0; int numEntries = 0; Iterator leftIter = leftEdges.iterator(); List extentList; double percentOfWidth = Edge.MAX_WIDTH_PERCENTAGE * advanceWidth; // The following doesn't try to take overlapping paths into account. // If paths are overlapped, they shouldn't be considered stems. // for each left edge, find the right edges that match it and // compute the stems at the top and bottom of that match. Add those // stems to the running total. while (leftIter.hasNext()) { Edge left = (Edge)leftIter.next(); ListIterator extentIter; // a list of the portions of left that have not been matched against a right edge. extentList = new ArrayList(); // we use Point since it's convenient. x == startOppositeDir. y == endOppositeDir. extentList.add(new Point(left.startOppositeDir, left.endOppositeDir)); extentIter = extentList.listIterator(); while (extentIter.hasNext()) { Iterator rightIter = rightEdges.iterator(); Point p = (Point)extentIter.next(); while (rightIter.hasNext()) { Edge right = (Edge)rightIter.next(); // if the right edge is not to the right of the left edge, skip it. if (right.endStemDir < left.endStemDir) continue; // check if this right edge aligns with at least a portion of p. // since the right edges are sorted from left to right, there can't // be anything between p and right since p is in the list of things not // yet matched if (right.endOppositeDir > p.x && right.startOppositeDir < p.y) { // top and bottom tell the limits of the overlap double top = Math.min(p.y, right.endOppositeDir); double bottom = Math.max(p.x, right.startOppositeDir); // if the overlap is small, skip it. if (!intermediateRemovedEdge(left, right, top, bottom) && (!filterValues || !Edge.isShortOverlap(top, bottom))) { // stem width at the top. double thisWidth = (right.getStemPosGivenOppPos(top) - left.getStemPosGivenOppPos(top)); boolean skip = false; boolean reset = false; if (top - bottom < Edge.SHORT_STEM) skip = true; // if thisWidth is wildly different than what has been computed so far, // take the bigger. else if (numEntries > 0) { double ave = average/numEntries; if ((ave - thisWidth) > Edge.MAX_STEM_VARIANCE) skip = true; else if ((thisWidth - ave) > Edge.MAX_STEM_VARIANCE && top - bottom > Edge.MIN_LENGTH_FOR_STEM_OVERRIDE) reset = true; } if (thisWidth > 0 && (!filterValues || (!skip && thisWidth < percentOfWidth && thisWidth < Edge.MAX_STEM))) { if (reset && filterValues) { average = 0; numEntries = 0; } average += thisWidth; numEntries++; } // stem width at the bottom thisWidth = (right.getStemPosGivenOppPos(bottom) - left.getStemPosGivenOppPos(bottom)); skip = (top - bottom < Edge.SHORT_STEM); reset = false; if (numEntries > 0) { double ave = average/numEntries; if ((ave - thisWidth) > Edge.MAX_STEM_VARIANCE) skip = true; else if ((thisWidth - ave) > Edge.MAX_STEM_VARIANCE && top - bottom > Edge.MIN_LENGTH_FOR_STEM_OVERRIDE) reset = true; } if (thisWidth > 0 && (!filterValues || (!skip && thisWidth < percentOfWidth && thisWidth < Edge.MAX_STEM))) { if (reset && filterValues) { average = 0; numEntries = 0; } average += thisWidth; numEntries++; } } // the top of this portion is matched against a right edge if (top == p.y) { if (bottom != p.x) { // shorten p p.y = bottom; } else break; // we're done with this section of the edge. } else if (bottom == p.x) { // shorten p p.x = top; } else { // XXX_lmb optimization: only add these if they are long enough // right is entirely contained within p. break p into 2 pieces. double tmpbottom = p.x; p.x = right.endOppositeDir; p = new Point(tmpbottom, right.startOppositeDir); extentIter.add(p); extentIter.previous(); } } } } } if (numEntries == 0) return 0; return average/numEntries; } public double getComputedStem(double advanceWidth, double italicAngle) { double retVal; mergeLines(); removeIrrelevantLines(italicAngle); Collections.sort(leftEdges, EdgeComparator.comparator); Collections.sort(rightEdges, EdgeComparator.comparator); retVal = averageWidth(advanceWidth, italicAngle, true); // if we failed, retry looking for stems of any length if (retVal == 0) { retVal = averageWidth(advanceWidth, italicAngle, false); } return retVal; } public void endchar() {} }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy