How 'thick' is a glyph?

95 views
Skip to first unread message

jdaw1

unread,
Mar 29, 2014, 1:27:46 PM3/29/14
to
This question asks how a PostScript program can know how thick is a glyph.

What do I mean by "thick"?

Start with
http://www.jdawiseman.com/2013/20131011_champagne_ahb.pdf

On the first page observe the styling of the large letters ("MC", "H", "ED"). This is made with a charpath, a clip, and then repeated gsave stroke grestore: very thick black; slightly less thick white; less thick black; etc.

Works well. Looks good. Also see
http://www.jdawiseman.com/2014/20140402_g80.pdf
http://www.jdawiseman.com/2014/20140422_1966.pdf

How big does the first setlinewidth need to be, to cover the whole character? That is, how thick is the font?

Currently my code needs the user to choose this. But can it be chosen by the PostScript? Can the PostScript know, for any given glyph (or even at the level of a font), what is the greatest distance between an interior point and the edge?

For those interested, this question is about the InlineTitles parameters in
http://www.jdawiseman.com/papers/placemat/placemat.html#inlinetitles

Thank you.

Mark Carroll

unread,
Mar 29, 2014, 1:44:20 PM3/29/14
to
jdaw1 <jdawi...@gmail.com> writes:

> How big does the first setlinewidth need to be, to cover the whole
> character? That is, how thick is the font?

If I'm understanding you correctly at all, then one thing you /could/
do, is use charpath to get the outline of a capital I, flattenpath to
turn that to line segments, sort the segments by height, and for the
segments that intersect the vertical midpoint, find the leftmost and
rightmost points. Hopefully there's a better solution coming from
somebody else though.

-- Mark

jdaw1

unread,
Mar 29, 2014, 4:07:11 PM3/29/14
to
That is at least three things: interesting; a kludge; and sometimes wrong.

The first two are obvious.

Third, part I. It might be that other glyphs contain wider strokes than the 'I'. For example, the bottom two corners in a 'W' can contain points further from the edge than happens in the middle of a straight pen stroke.

Third, part II. What if the 'I' bows outward. That is, what if the narrowest part of the vertical stroke is at the vertical middle? That would cause my code to do too few 'gsave stroke grestore's.

jdaw1

unread,
Mar 31, 2014, 5:38:16 AM3/31/14
to
Here's a horrible way to do it, imperfectly.

There is a string to be 'inlined'. Compute its path. Compute bounding box. Divide the height by 64, and take a grid of points with that spacing. So if the string is long, there will be many more columns than rows. Test each point in the grid with infill, and keep in an array those points that are in the interior of the path.

Set WidthMin to be tiny. Set Width to be WidthMin 2 mul. Then strokepath. Forall the stored interior points, infill. If there are any unpainted points, keep only those points, /WidthMin Width def, and repeat. If there are no unpainted points, /WidthMax Width def, and henceforth /Width WidthMin WidthMax add 2 div def. Repeat until WidthMax sufficiently close to WidthMin.

This succeeds only if the grid of points is sufficiently dense. But infill is slow, and 64^2 infills, some done many times, would be very slow. Yuck!

jdaw1

unread,
Mar 31, 2014, 7:36:55 PM3/31/14
to
On testing, it isn't that slow. The string "GC60", in /Optima-Bold, with 128 rows of dots, and the number of columns being a modest multiple of this, using Distiller XI on a 2.7GHz Mac, takes about 1.3 seconds. That's not immediate, but not terrible.

Nonetheless, the algorithm is still horrible.

jdaw1

unread,
Apr 6, 2014, 7:00:38 PM4/6/14
to
The 'answer', horrible algorithm and all, is now implemented as the function LineWidthThatCoversPath, which can be found in
http://www.jdawiseman.com/papers/placemat/placemat.ps

Share and enjoy.

jdaw1

unread,
Apr 7, 2015, 7:25:13 AM4/7/15
to
> On testing, it isn't that slow. The string "GC60", in /Optima-Bold, with 128 rows of dots, and the number of columns being a modest multiple of this, using Distiller XI on a 2.7GHz Mac, takes about 1.3 seconds. That's not immediate, but not terrible.

When I said that it "isn't that slow", that is a feature of insufficient testing. It's quick enough in Distiller, but painfully slow in Ghostscript, as has recently been reported.
http://bugs.ghostscript.com/show_bug.cgi?id=695906
My OP's report:
http://www.theportforum.com/viewtopic.php?t=175&start=913

jdaw1

unread,
Apr 7, 2015, 7:27:18 AM4/7/15
to
I'll also post the code of LineWidthThatCoversPath, in case it changes in a later version. But be careful with line breaks that you can copy but that weren't in my original code.




% TargetAccuracy LineWidthThatCoversPath WidthMax WidthMin
% Calculates, for the current path, the greatest distance from an interior point to the edge.
% WidthMin is known to be less than this distance. WidthMax will be greater.
% https://groups.google.com/forum/#!topic/comp.lang.postscript/86b7Sg8v7B0
/LineWidthThatCoversPath
{
DeBugLevel 25 le {(+LineWidthThatCoversPath) OutputToLog} if
20 dict begin
//PrinterEpsilon 2 copy lt {exch} if pop /TargetAccuracy exch def
% Target accuracy is split into two equal parts. The grid separation is half. And then the WidthMax-WidthMin separation is allowed the other half, and sometimes less than that.

PathBBox /ury exch def /urx exch def /lly exch def /llx exch def
urx llx sub //PrinterEpsilon gt ury lly sub //PrinterEpsilon gt and
{
% For an equilateral triangle the point furthest from the three corners is 2/3 of the height from the corners, which is Sqrt3/3 of the base.
/NumRows ury lly sub TargetAccuracy 0.75 mul div def % Error to be <= TargetAccuracy/2, which is 2/3 of row height. So row height = 0.75*TargetAccuracy.
/NumCols urx llx sub TargetAccuracy 1.5 mul Sqrt3 div div def % Error to be <= TargetAccuracy/2, which is Sqrt3/3 of the width. So width = TargetAccuracy * 1.5 / Sqrt3.
30000 NumRows ceiling 1 add NumCols ceiling 1 add mul div dup 1 gt % 30k => max stack of about 60k
{sqrt dup NumRows mul ceiling cvi 1 add /NumRows exch def NumCols mul ceiling cvi 1 add /NumCols exch def}
{pop /NumRows NumRows ceiling cvi 1 add def /NumCols NumCols ceiling cvi 1 add def}
ifelse % NumRows*NumCols small
/HalfGapRows ury lly sub NumRows 2 mul div def
/HalfGapCols urx llx sub NumCols 2 mul div def
/MaxResolutionError HalfGapRows dup 4 mul HalfGapCols dup mul exch div add def
DeBugLevel 15 le {mark ( LineWidthThatCoversPath: NumRows = ) NumRows (; NumCols = ) NumCols (; HalfGapRows = ) HalfGapRows (; HalfGapCols = ) HalfGapCols (; MaxResolutionError = ) MaxResolutionError ConcatenateToMark OutputToLog} if

count currentsystemparams /MaxOpStack 2 copy known {get} {pop pop 65535} ifelse exch sub /StackSpaceRemaining exch def
/PointsMostDistanceFromEdge
[
1 1 NumCols 2 mul 1 sub
{
/ColNum exch def /X ColNum HalfGapCols mul llx add def
1 2 NumRows 2 mul
{
/RowNum exch def /Y RowNum HalfGapRows mul lly add def
RowNum 4 mod 2 idiv ColNum 2 mod eq {X Y infill {X Y /StackSpaceRemaining StackSpaceRemaining 2 sub def} if StackSpaceRemaining 2 lt {exit} if} if
} for % Y
StackSpaceRemaining 2 lt {exit} if
} for % X
] def % /PointsMostDistanceFromEdge
StackSpaceRemaining 2 lt {(Warning: LineWidthThatCoversPath has incomplete calculation because there might be too little stack space. Increasing TargetAccuracy would help.) OutputToLog} if
DeBugLevel 15 le {mark ( LineWidthThatCoversPath: PointsMostDistanceFromEdge length = ) PointsMostDistanceFromEdge length ConcatenateToMark OutputToLog} if

/WidthMin null def /WidthMax null def
/Width ury lly sub 6 div PrinterEpsilon 2 copy lt {exch} if pop def
{
gsave
Width setlinewidth strokepath
/NewArrayLength 0 def
0 2 PointsMostDistanceFromEdge length 2 sub
{
dup PointsMostDistanceFromEdge exch get exch 1 add PointsMostDistanceFromEdge exch get 2 copy
infill not
{
PointsMostDistanceFromEdge NewArrayLength 1 add 3 -1 roll put PointsMostDistanceFromEdge NewArrayLength 3 -1 roll put
/NewArrayLength NewArrayLength 2 add def
} {pop pop} ifelse % infill not
} for % PointsMostDistanceFromEdge
NewArrayLength 0 gt {/WidthMin Width def /PointsMostDistanceFromEdge PointsMostDistanceFromEdge 0 NewArrayLength getinterval def} {/WidthMax Width def} ifelse
grestore
DeBugLevel 15 le {mark ( LineWidthThatCoversPath: WidthMin = ) WidthMin (; WidthMax = ) WidthMax (; PointsMostDistanceFromEdge length = ) PointsMostDistanceFromEdge length ConcatenateToMark OutputToLog} if

WidthMax null ne
{
WidthMin null ne
{
% Complicated exit conditions. Generally by the time this becomes close, PointsMostDistanceFromEdge is short, so a a few extra rounds are fast.
WidthMax WidthMin sub MaxResolutionError lt WidthMin TargetAccuracy div cvi WidthMax MaxResolutionError add TargetAccuracy div cvi eq and {exit} if
WidthMax WidthMin sub dup TargetAccuracy 64 div lt exch //PrinterEpsilon le or {exit} if
} {WidthMax TargetAccuracy 2 div le WidthMax PrinterEpsilon le or {/WidthMin 0 def exit} if} ifelse % WidthMin null ne
} if % WidthMax null ne

/Width WidthMax null eq {WidthMin 2 mul} {WidthMin null eq {WidthMax 2 div} {WidthMin WidthMax add 2 div} ifelse} ifelse def
} loop

WidthMax MaxResolutionError add WidthMin
} {//PrinterEpsilon 0} ifelse end % Non-zero area
DeBugLevel 25 le {(-LineWidthThatCoversPath) OutputToLog} if
} def % /LineWidthThatCoversPath
Reply all
Reply to author
Forward
0 new messages