Rework layout engine around exact-glyph collision, add tests and docs sync
Replace the old bbox/heuristic placement (scale search rounds, large-font capping, stratified sampling, fill-retry ladders) with an area-model font sizing pass feeding a C++ exact-glyph collision engine (centroid-biased spiral + random probing, HD clearance refinement, density/hole optimization). Simplify the frontend advanced-params panel and JobParams type to match the surviving config surface, add a layout-constraints test suite and a repeatable benchmark tool, and bring docs/*.md back in sync with current code (plus new TESTING.md and DEPLOYMENT.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -101,8 +101,6 @@ class EfficientWordCloud:
|
||||
Probability a word is placed horizontally (0–1).
|
||||
mode : str
|
||||
PIL image mode ('RGB', 'RGBA', …).
|
||||
use_spiral_search : bool
|
||||
Use center-out sorted search (True) or reservoir sampling (False).
|
||||
scale : float
|
||||
Scaling factor between layout computation and final rendering.
|
||||
``scale=2`` means the output image is 2× the canvas size in each
|
||||
@@ -142,8 +140,6 @@ class EfficientWordCloud:
|
||||
How much word frequency (vs rank) influences font size.
|
||||
0 = rank only, 1 = fully frequency-driven.
|
||||
When *repeat* is True, defaults to 0.
|
||||
font_step : int
|
||||
Step size when reducing font size to find a fit.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
@@ -156,16 +152,14 @@ class EfficientWordCloud:
|
||||
background_color="black",
|
||||
prefer_horizontal=0.9,
|
||||
mode="RGB",
|
||||
use_spiral_search=True,
|
||||
scale=1,
|
||||
scale=1,
|
||||
contour_width=0,
|
||||
contour_color="black",
|
||||
margin=2,
|
||||
color_func=None,
|
||||
colormap=None,
|
||||
random_state=None,
|
||||
relative_scaling="auto",
|
||||
font_step=1,
|
||||
relative_scaling="auto",
|
||||
repeat=False,
|
||||
stopwords=None,
|
||||
regexp=None,
|
||||
@@ -185,7 +179,6 @@ class EfficientWordCloud:
|
||||
self.background_color = background_color
|
||||
self.prefer_horizontal = prefer_horizontal
|
||||
self.mode = mode
|
||||
self.use_spiral_search = use_spiral_search
|
||||
self.scale = scale
|
||||
self.contour_width = contour_width
|
||||
self.contour_color = contour_color
|
||||
@@ -197,7 +190,6 @@ class EfficientWordCloud:
|
||||
else:
|
||||
self.relative_scaling = relative_scaling
|
||||
self.margin = margin
|
||||
self.font_step = font_step
|
||||
self.stopwords = stopwords if stopwords is not None else STOPWORDS
|
||||
self.regexp = regexp
|
||||
self.collocations = collocations
|
||||
@@ -319,10 +311,9 @@ class EfficientWordCloud:
|
||||
def _query(qh, qw):
|
||||
return self.grid.query_direct(qh, qw, rs.randint(0, 2**31))
|
||||
|
||||
# v4: Ref-like linear step-down placement with bitmap occupancy
|
||||
# After each word placement, stamp glyph bitmap into C++ canvas
|
||||
# and rebuild integral for pixel-accurate collision detection.
|
||||
# No PIL image drawn during placement — to_image() renders later.
|
||||
# Each word is tried at exactly its weight-derived target size. A
|
||||
# failed word may change orientation, but never receives a private
|
||||
# fallback size. Whole-cloud scaling belongs to the caller.
|
||||
|
||||
# Dummy draw for textbbox measurement
|
||||
_measure_img = Image.new("L", (1, 1))
|
||||
@@ -344,36 +335,26 @@ class EfficientWordCloud:
|
||||
else:
|
||||
orientation = Image.ROTATE_90
|
||||
|
||||
tried_other_orientation = False
|
||||
|
||||
while True:
|
||||
if font_size < self.min_font_size:
|
||||
break
|
||||
|
||||
font = _get_font(font_size)
|
||||
transposed = ImageFont.TransposedFont(font, orientation=orientation)
|
||||
bbox = _measure_draw.textbbox((0, 0), word, font=transposed)
|
||||
tw = bbox[2] - bbox[0]
|
||||
th = bbox[3] - bbox[1]
|
||||
pos = None
|
||||
orientations = [orientation]
|
||||
if self.prefer_horizontal < 1:
|
||||
orientations.append(Image.ROTATE_90 if orientation is None else None)
|
||||
for candidate_orientation in orientations:
|
||||
font = _get_font(font_size)
|
||||
transposed = ImageFont.TransposedFont(font, orientation=candidate_orientation)
|
||||
bbox = _measure_draw.textbbox((0, 0), word, font=transposed)
|
||||
tw = bbox[2] - bbox[0]
|
||||
th = bbox[3] - bbox[1]
|
||||
qh = th + self.margin
|
||||
qw = tw + self.margin
|
||||
|
||||
pos = _query(qh, qw)
|
||||
if pos is not None:
|
||||
break
|
||||
|
||||
# No position found — try alternate orientation, then reduce size
|
||||
if not tried_other_orientation and self.prefer_horizontal < 1:
|
||||
orientation = Image.ROTATE_90 if orientation is None else None
|
||||
tried_other_orientation = True
|
||||
else:
|
||||
font_size -= self.font_step
|
||||
orientation = None
|
||||
tried_other_orientation = False
|
||||
|
||||
if font_size < self.min_font_size:
|
||||
# Canvas full — no more words can fit
|
||||
break
|
||||
pos = _query(qh, qw)
|
||||
if pos is not None:
|
||||
orientation = candidate_orientation
|
||||
break
|
||||
|
||||
if pos is None:
|
||||
continue
|
||||
|
||||
y, x = pos
|
||||
# Adjust position for margin (like ref: x,y += margin // 2)
|
||||
|
||||
Reference in New Issue
Block a user