NAME
Image::Bitmap2Paths - Perl extension to convert line-drawing (and font's) bitmaps to paths (of vector formats).
SYNOPSIS
use Image::Bitmap2Paths;
my $bm = Image::Bitmap2Paths->new( minibitmap => my_uncompress($filecontent) );
my $strokes = $bm->get('strokes');
my @BM = map {[map $_ ne '-', split //]} split /\n/, <<EOB; # At (5,3), (6,4), (7,5)
---------
---------
----x----
-----x---
------x--
---------
EOB
DESCRIPTION
v0.01 is a very early stage of refactoring a generally useful code out of the script hex_parse.pl used to generate vector fonts on
http://ilyaz.org/fonts
Hairlines are converted to the corresponding paths (which are assumed to be visualized by drawing with an appropriate pen). The rest is converted to “blobs” with appropriately calculated boundary paths.
Due to the workflow of postprocessing (drawing of hairlines is done by ExpandStroke()
in fontforge
, and its programming interface is not flexible enough to treat different paths differently), the output boundaries of blobs “go through middles of boundary pixels” — in the same way as for hairlines.
One should keep in mind that currently the overwhelming amount of work focuses on the detection of “junctions” — the places where hairlines meet and/or cross each other. Unfortunately, this meant that so far, almost no resources were left to cover “the sexier” problem of finding “a beautiful curve” approximating the given rough broken line path. (On the first stage of their processing, the extracted paths connect centers of pixels. So far, the only “really nice beautification engine” is one to extract the longish straight segments out of such “pixellated paths”. The rest of the paths is converted to curves in an extremely naive way now.)
(Contrary to my initial expectations, decyphering the geometry of a junction from its “pixellated version” turned out to be an unimaginably harder process that what one could predict. Fortunately — while the code is convoluted, bulky and ugly — the majority of the complications has been worked out, and this process is now “almost” flawless — as far as the testbed of unifont
’s glyphs shows. On the other hand, the details of how hairlines “enter” into blobs are not fully worked out yet.)
The structure of the API
One runs the process flow by calling methods set()
and get()
on the fields, or providing them as pairs of arguments to the new()
method. (The object $obj of this class is actually a wrapped Data::Flow
object $$obj; one can access it directly correspondingly.)
(Currently, the code contains several hardwired “constants” related to workarounds for bugs in fontforge
— as well as for details of working with unifont
’s glyphs. Eventually, these showd better be separated into “configurable” realm — like all other field.)
The fields one may set are (these docs are very incomplete now; one may need to inspect the details how these fields are used in the module, and in output_sfd_char()
/output_human_readable()
subrounes in the examples):
bitmap
-
(Extended) array of arrays (of length
width+2
, with the first/last elements false).The external array of the
bitmap
should haveheight+2
elements; the first/last one should contain only false entries, and may be empty. (Essentially, the whole border of this extended 2D bitmap should be blank.) minibitmap
-
Likewise, but without blank borders (they will be added when the field 'bitmap' is calculated).
The fields to get are
strokes
-
The most important output field.
Array with elements like
[is_loop, is_blob, [@segments], [@runs], [@breaks]]
, with eachsegment
of the form[dir, x0, y0, x1, y2]
(optionally followed by'tip'
if this segment comes fromTail/MFork
spur). Heredir
is one of 8 compass direction in0..7
(with step 45°; seeoffs
below), or is-20
for "diagonal of a rhombus" strokes. The last two elements (runs
andbreaks
) are optional (and do not appear for stroks of one segment???).is_loop
is negative if the loop is “smooth”: has no corners with angles sharper than 135° (if a loop has such “sharp corners”, the start and the end of the loop are at such a corner).The arrays
runs
andbreaks
are artefacts of the current (primitive) algorithm. The meaning is subject to change.They contain information about how a stroke is subdivided into “smooth” parts. Currently, the parts are either straight segments (currently of length ≥3), or convex runs of “edges”, or non-convex runs which cannot be converted to straight segments. The array
breaks
gives (???) indices of edges in a stroke which start a new “smooth” substroke (without angles smaller than 135°). (It always contains0
; even for smooth closed loops.) - width height
-
… Of the minibitmap. In other words, 2 less than for bitmap.
Lb Rb
-
Left and right bearings. Offsets in the extended bitmap: last blank column at start; likewise, first at end.
stageOne
-
Combines:
offs cnt cntmin near nearmin doublerays
. offs
-
At
[$y][$x]
contains the list of 8 possible directions (in 0..7, with step 45°) where it has a neighbor. Directions are consecutive, 0 decreases $y, 2 is to the right, (This is clockwise from top ify
increases going down.) cnt
=cntmin
-
The length of the corresponding list.
near
=nearmin
-
Like off, but an array of length 8 with 1 or 0 at each direction 0..7.
doublerays
-
At
[$y][$x]
, the count of directions in which a ray goes for length >=2. stage10
-
Combines:
rays
longedges10
seenlong10
inLong10
midLong10
(done in two long iterations of DO_RAYS + a minor massage) rays
-
For each
[$y][$x][$dir]
which corresponds to a known neighbor, contains the (preliminary) result of the deep inspection of the narrow sector going “about” the direction $dir. The result is an array; its 0th element is the string describing “the type” of this sector. The rest may contain extra info specific to the type. (Usually, the 1st element is “rotation”: the main “branch” of how one should “correct”dir
when one goes one step “further inside the sector”.)(Types are indicated on the 3x-magnified debugging output, near each vertex.) Dictionary of ray candidates: Dense (>=7 neighbors at dist 1 or 2); otherwise as below (dot denotes an empty place; d is a dependency: rays there “must be good”). Below
*
denotes the vertex($x,$y)
; the lines indicate how a vertex in this position “is connected” to the*
. The direction of the sector startubg at*
is not ambiguous in these pictures:. . . / |. d . . \ .... .. doubleray: *-- curve: *-. Fork: *d. ./- fake-curve: *-. d/- rhombus: *d. tail: --*- *- ish; serif --* notch:.-* ..* .|/ .\ | . \ *. \ \ *. d * d .|\ .|. / .|/ | ./. ./|. fork4: *d. Near-corner: *. m-joint: || elses-ray: *| / 3fork3: *d. *d. Sharp: *--- /. ..| .|\ .d-- *d d d .|\ . ..\ \.. \- \ Note that dependent is not a neighbor for diagonal elses-ray (and is not unique) - .d- ...\ \ fork4 and one flavor of fork3 are particular cases of fork! Corner-curve: *.\ 3fork3: *| bend-sharp: --* Later may put: ignore, Ignore, Tail, 2fork3, Enforced, Arrow/(x-)arrow, Probable-curve: *| Joint???: d*-. 1Spur, MFork; Rhombus-force, Zh/K-fake-curve is intended to be ½-of-segment .|- Btail, 4fork, xFork, °. (Also allow longer shaft on Sharp) |.
In these docs, the only dependent type which is mentioned corresponds to opposite-direction pair
tail
/doubleray
; it is converted toTail
/MFork
iftail
hascnt==3
(as on top of the glyph for “M”). Likewise for a symmetrized case (as at bottom of “V”): if it isfake-curve
/Fork
withcnt!=1
and the opposite is1Spur
/Probable-curve
, or unrecognized (instead ofdoubleray
); hererot=0
. HereTail
is the spur direction going from the branch point, andMFork
is the direction of the spur going into the branch point.Essentially, the type
MFork
indicates that a short spur on a very sharp fork (=the handle) is presumed to be the artifact due to a very sharp angle on a broken line. The joint of two (long) joining lines should be actually moved to the end of the spur. (See alsotailEdge
.)Do we support such a sharp angle “sideways” to another line??? Can two such guys appear on opposite sides when we “punch a curve through” a joint??? Removal of certain points during ray-detection (between two passes) not explained???
longedges10
-
Lists candidates for edges of length √5 going “in between” our 8 compass directions. This long edge should not be “made” of two shorter edges since it is “intersected” by another (long) path. The list element contain 2 coodinates of the start, 2 of the end, offset in the list, “the approximate”
$dir
and$rot
≟ ±1. ???(This corresponds to the ray type
2fork3
. There must be a neighbor in the direction $dir. These are only candidates: we list them since we do not know which of two ways “around a 45° rhomus” is preferable; in the following stages the answer may become more clear, and then such candidates are going be removed from the corresponding updated lists.) seenlong10
-
Hash with the same elements indexed by
{$x,$y,$x1,$y1}
and by{$x1,$y1,$x,$y}
. inLong10
-
Hash indexed by
{$X,$Y}
, true of this point is one of the ends of a longedge. midLong10
-
Likewise for a (doubled) midpoint of the longedge.
сtage20
-
Combines
edge20
,cntedge20
,lastedge20
,longedges20
,seenlong20
,midLong20
,inLong20
. сtage30
-
Combines
edge30
,cntedge30
,lastedge30
,blobs30
,blob30
.Miss: skipExtraBlob (30) strokes nextEdgeBlob entryPointBlob (A0) <-- coarse_blobs
stage40
-
Combines
edge40
,cntedge40
,lastedge40
. tailEdge
-
Lists the vertices of the type
Tail
(i.e., opposite to the typeMFork
; see "rays").A hash with “composite keys” (accessed as
$tailEdge->{$x,$y}
) with values[x,y,dir,rot]
describing an oriented edge going from a “Tail
node to a node with “the opposite ray” of typeMFork
with “rotation”rot
. The value is created by[$x, $y, $dir, my $rot = $rays->[$Y][$X][$DIR][1]] # X,Y,DIR are opposite to x,y,dir On the last (???) stage we may delete certain entries in this hash! Fix???
The list above is very cursory. The parts written are very cursory. Blah blah blah.
EXPORT
None by default.
CAVEATS
Due to inept imperfect incomplete refactoring, the handlers may edit the fields, and may expect that the changes are going to propagate to the caller. One (I?!) should not forget to go through this, inspect the code, and introduce new names for edited duplicates.
@rays, $cnt, $offs seems to be preserved by stage20
stage30 etc not even investigated for this yet!!! ??? Force all updates to go through sub calls???
$marked is a placeholder (The initial intent was that when debugging, one could raise one of these flags seeing a certain difficulty, and only the output for characters having this problem would be generated.
Since we do not do deep copying of input fields in the filters, some modifications of these arguments may be left unnoticed during refactoring.
SEE ALSO
Mention other useful documentation such as the documentation of related modules or operating system documentation (such as man pages in UNIX), or any relevant external documentation such as RFCs or standards.
If you have a mailing list set up for your module, mention it here.
If you have a web site set up for your module, mention it here.
AUTHOR
Ilya Zakharevich, <ilyaz@cpan.com>
COPYRIGHT AND LICENSE
Copyright (C) 2025 by Ilya Zakharevich
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.22.2 or, at your option, any later version of Perl 5 you may have available.
1 POD Error
The following errors were encountered while parsing the POD:
- Around line 2756:
You forgot a '=back' before '=head2'
You forgot a '=back' before '=head2'