com.adobe.fontengine.font.StemFinder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aem-sdk-api Show documentation
Show all versions of aem-sdk-api Show documentation
The Adobe Experience Manager SDK
/*
*
* 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() {}
}