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 have height+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 each segment of the form [dir, x0, y0, x1, y2] (optionally followed by 'tip' if this segment comes from Tail/MFork spur). Here dir is one of 8 compass direction in 0..7 (with step 45°; see offs below), or is -20 for "diagonal of a rhombus" strokes. The last two elements (runs and breaks) 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 and breaks 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 contains 0; 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 if y 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 to Tail/MFork if tail has cnt==3 (as on top of the glyph for “M”). Likewise for a symmetrized case (as at bottom of “V”): if it is fake-curve/Fork with cnt!=1 and the opposite is 1Spur/Probable-curve, or unrecognized (instead of doubleray); here rot=0. Here Tail is the spur direction going from the branch point, and MFork 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 also tailEdge.)

  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 type MFork; 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 type MFork 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'