mirror of
https://github.com/curioustorvald/Terrarum-sans-bitmap.git
synced 2026-03-07 11:51:50 +09:00
optimised contour generation
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
Convert 1-bit bitmap arrays to TrueType quadratic outlines.
|
Convert 1-bit bitmap arrays to CFF outlines by tracing connected pixel blobs.
|
||||||
|
|
||||||
Each set pixel becomes part of a rectangle contour drawn clockwise.
|
Each connected component of filled pixels becomes a single closed contour
|
||||||
Adjacent identical horizontal runs are merged vertically into rectangles.
|
(plus additional contours for any holes). Adjacent collinear edges are
|
||||||
|
merged, minimising vertex count.
|
||||||
|
|
||||||
Scale: x_left = col * SCALE, y_top = (BASELINE_ROW - row) * SCALE
|
Scale: x = col * SCALE, y = (BASELINE_ROW - row) * SCALE
|
||||||
where BASELINE_ROW = 16 (ascent in pixels).
|
where BASELINE_ROW = 16 (ascent in pixels).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict, List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
import sheet_config as SC
|
import sheet_config as SC
|
||||||
|
|
||||||
@@ -16,15 +17,56 @@ SCALE = SC.SCALE
|
|||||||
BASELINE_ROW = 16 # pixels from top to baseline
|
BASELINE_ROW = 16 # pixels from top to baseline
|
||||||
|
|
||||||
|
|
||||||
|
def _turn_priority(in_dx, in_dy, out_dx, out_dy):
|
||||||
|
"""
|
||||||
|
Return priority for outgoing direction relative to incoming.
|
||||||
|
Lower = preferred (rightmost turn in y-down grid coordinates).
|
||||||
|
|
||||||
|
This produces outer contours that are CW in font coordinates (y-up)
|
||||||
|
and hole contours that are CCW, matching the non-zero winding rule.
|
||||||
|
"""
|
||||||
|
# Normalise to unit directions
|
||||||
|
nidx = (1 if in_dx > 0 else -1) if in_dx else 0
|
||||||
|
nidy = (1 if in_dy > 0 else -1) if in_dy else 0
|
||||||
|
ndx = (1 if out_dx > 0 else -1) if out_dx else 0
|
||||||
|
ndy = (1 if out_dy > 0 else -1) if out_dy else 0
|
||||||
|
|
||||||
|
# Right turn in y-down coords: (-in_dy, in_dx)
|
||||||
|
if (ndx, ndy) == (-nidy, nidx):
|
||||||
|
return 0
|
||||||
|
# Straight
|
||||||
|
if (ndx, ndy) == (nidx, nidy):
|
||||||
|
return 1
|
||||||
|
# Left turn: (in_dy, -in_dx)
|
||||||
|
if (ndx, ndy) == (nidy, -nidx):
|
||||||
|
return 2
|
||||||
|
# U-turn
|
||||||
|
return 3
|
||||||
|
|
||||||
|
|
||||||
|
def _simplify(contour):
|
||||||
|
"""Remove collinear intermediate vertices from a rectilinear contour."""
|
||||||
|
n = len(contour)
|
||||||
|
if n < 3:
|
||||||
|
return contour
|
||||||
|
result = []
|
||||||
|
for i in range(n):
|
||||||
|
p = contour[(i - 1) % n]
|
||||||
|
c = contour[i]
|
||||||
|
q = contour[(i + 1) % n]
|
||||||
|
# Cross product of consecutive edge vectors
|
||||||
|
if (c[0] - p[0]) * (q[1] - c[1]) - (c[1] - p[1]) * (q[0] - c[0]) != 0:
|
||||||
|
result.append(c)
|
||||||
|
return result if len(result) >= 3 else contour
|
||||||
|
|
||||||
|
|
||||||
def trace_bitmap(bitmap, glyph_width_px):
|
def trace_bitmap(bitmap, glyph_width_px):
|
||||||
"""
|
"""
|
||||||
Convert a bitmap to a list of rectangle contours.
|
Convert a bitmap to polygon contours by tracing connected pixel blobs.
|
||||||
|
|
||||||
Each rectangle is ((x0, y0), (x1, y1)) in font units, where:
|
Returns a list of contours, where each contour is a list of (x, y)
|
||||||
- (x0, y0) is bottom-left
|
tuples in font units. Outer contours are clockwise, hole contours
|
||||||
- (x1, y1) is top-right
|
counter-clockwise (non-zero winding rule).
|
||||||
|
|
||||||
Returns list of (x0, y0, x1, y1) tuples representing rectangles.
|
|
||||||
"""
|
"""
|
||||||
if not bitmap or not bitmap[0]:
|
if not bitmap or not bitmap[0]:
|
||||||
return []
|
return []
|
||||||
@@ -32,66 +74,79 @@ def trace_bitmap(bitmap, glyph_width_px):
|
|||||||
h = len(bitmap)
|
h = len(bitmap)
|
||||||
w = len(bitmap[0])
|
w = len(bitmap[0])
|
||||||
|
|
||||||
# Step 1: Find horizontal runs per row
|
def filled(r, c):
|
||||||
runs = [] # list of (row, col_start, col_end)
|
return 0 <= r < h and 0 <= c < w and bitmap[r][c]
|
||||||
for row in range(h):
|
|
||||||
col = 0
|
|
||||||
while col < w:
|
|
||||||
if bitmap[row][col]:
|
|
||||||
start = col
|
|
||||||
while col < w and bitmap[row][col]:
|
|
||||||
col += 1
|
|
||||||
runs.append((row, start, col))
|
|
||||||
else:
|
|
||||||
col += 1
|
|
||||||
|
|
||||||
# Step 2: Merge vertically adjacent identical runs into rectangles
|
# -- Step 1: collect directed boundary edges --
|
||||||
rects = [] # (row_start, row_end, col_start, col_end)
|
# Pixel (r, c) occupies grid square (c, r)-(c+1, r+1).
|
||||||
used = [False] * len(runs)
|
# Edge direction keeps the filled region to the left (in y-down coords).
|
||||||
|
edge_map = {} # start_vertex -> [end_vertex, ...]
|
||||||
|
|
||||||
for i, (row, cs, ce) in enumerate(runs):
|
for r in range(h):
|
||||||
if used[i]:
|
for c in range(w):
|
||||||
|
if not bitmap[r][c]:
|
||||||
continue
|
continue
|
||||||
# Try to extend this run downward
|
if not filled(r - 1, c): # top boundary
|
||||||
row_end = row + 1
|
edge_map.setdefault((c, r), []).append((c + 1, r))
|
||||||
j = i + 1
|
if not filled(r + 1, c): # bottom boundary
|
||||||
while j < len(runs):
|
edge_map.setdefault((c + 1, r + 1), []).append((c, r + 1))
|
||||||
r2, cs2, ce2 = runs[j]
|
if not filled(r, c - 1): # left boundary
|
||||||
if r2 > row_end:
|
edge_map.setdefault((c, r + 1), []).append((c, r))
|
||||||
break
|
if not filled(r, c + 1): # right boundary
|
||||||
if r2 == row_end and cs2 == cs and ce2 == ce and not used[j]:
|
edge_map.setdefault((c + 1, r), []).append((c + 1, r + 1))
|
||||||
used[j] = True
|
|
||||||
row_end = r2 + 1
|
|
||||||
j += 1
|
|
||||||
rects.append((row, row_end, cs, ce))
|
|
||||||
|
|
||||||
# Step 3: Convert to font coordinates
|
if not edge_map:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# -- Step 2: trace contours using rightmost-turn rule --
|
||||||
|
used = set()
|
||||||
contours = []
|
contours = []
|
||||||
for row_start, row_end, col_start, col_end in rects:
|
|
||||||
x0 = col_start * SCALE
|
for sv in sorted(edge_map):
|
||||||
x1 = col_end * SCALE
|
for ev in edge_map[sv]:
|
||||||
y_top = (BASELINE_ROW - row_start) * SCALE
|
if (sv, ev) in used:
|
||||||
y_bottom = (BASELINE_ROW - row_end) * SCALE
|
continue
|
||||||
contours.append((x0, y_bottom, x1, y_top))
|
|
||||||
|
path = [sv]
|
||||||
|
prev, curr = sv, ev
|
||||||
|
used.add((sv, ev))
|
||||||
|
|
||||||
|
while curr != sv:
|
||||||
|
path.append(curr)
|
||||||
|
idx, idy = curr[0] - prev[0], curr[1] - prev[1]
|
||||||
|
|
||||||
|
candidates = [e for e in edge_map.get(curr, [])
|
||||||
|
if (curr, e) not in used]
|
||||||
|
if not candidates:
|
||||||
|
break
|
||||||
|
|
||||||
|
best = min(candidates,
|
||||||
|
key=lambda e: _turn_priority(
|
||||||
|
idx, idy, e[0] - curr[0], e[1] - curr[1]))
|
||||||
|
|
||||||
|
used.add((curr, best))
|
||||||
|
prev, curr = curr, best
|
||||||
|
|
||||||
|
path = _simplify(path)
|
||||||
|
if len(path) >= 3:
|
||||||
|
contours.append([
|
||||||
|
(x * SCALE, (BASELINE_ROW - y) * SCALE)
|
||||||
|
for x, y in path
|
||||||
|
])
|
||||||
|
|
||||||
return contours
|
return contours
|
||||||
|
|
||||||
|
|
||||||
def draw_glyph_to_pen(contours, pen, x_offset=0, y_offset=0):
|
def draw_glyph_to_pen(contours, pen, x_offset=0, y_offset=0):
|
||||||
"""
|
"""
|
||||||
Draw rectangle contours to a TTGlyphPen or similar pen.
|
Draw polygon contours to a T2CharStringPen (or compatible pen).
|
||||||
Each rectangle is drawn as a clockwise closed contour (4 on-curve points).
|
|
||||||
|
|
||||||
|
Each contour is a list of (x, y) vertices forming a closed polygon.
|
||||||
x_offset/y_offset shift all contours (used for alignment positioning).
|
x_offset/y_offset shift all contours (used for alignment positioning).
|
||||||
"""
|
"""
|
||||||
for x0, y0, x1, y1 in contours:
|
for contour in contours:
|
||||||
ax0 = x0 + x_offset
|
x, y = contour[0]
|
||||||
ax1 = x1 + x_offset
|
pen.moveTo((x + x_offset, y + y_offset))
|
||||||
ay0 = y0 + y_offset
|
for x, y in contour[1:]:
|
||||||
ay1 = y1 + y_offset
|
pen.lineTo((x + x_offset, y + y_offset))
|
||||||
# Clockwise: bottom-left -> top-left -> top-right -> bottom-right
|
|
||||||
pen.moveTo((ax0, ay0))
|
|
||||||
pen.lineTo((ax0, ay1))
|
|
||||||
pen.lineTo((ax1, ay1))
|
|
||||||
pen.lineTo((ax1, ay0))
|
|
||||||
pen.closePath()
|
pen.closePath()
|
||||||
|
|||||||
Reference in New Issue
Block a user