Files
product-vamp/generator/engraving/raster.py
T
khodowany 9fd5abd075
ci / checks (push) Successful in 1h16m10s
Rhythm Selection & Cleanup
2026-09-26 02:50:22 -05:00

102 lines
4.3 KiB
Python

from __future__ import annotations
import dataclasses
import math
import PIL.Image
import PIL.ImageDraw
import aggdraw
import fontTools.pens.basePen
import generator.engraving.fonts
import generator.engraving.types
class OutlinePen(fontTools.pens.basePen.BasePen):
def __init__(self, glyphs, scale: float, x: float, y: float):
super().__init__(glyphs)
self.path = aggdraw.Path()
self.scale, self.x, self.y = scale, x, y
def point(self, point):
return self.x + point[0] * self.scale, self.y - point[1] * self.scale
def _moveTo(self, point):
self.path.moveto(*self.point(point))
def _lineTo(self, point):
self.path.lineto(*self.point(point))
def _curveToOne(self, first, second, last):
self.path.curveto(*self.point(first), *self.point(second), *self.point(last))
def _closePath(self):
self.path.close()
def _endPath(self):
pass
def rasterize_page(page: generator.engraving.types.Page, fonts: generator.engraving.fonts.Fonts) -> PIL.Image.Image:
return rasterize_band(page, page.primitives, 0, page.height, fonts)
def rasterize_band(page: generator.engraving.types.Page, primitives: tuple[generator.engraving.types.Primitive, ...], top: int, bottom: int, fonts: generator.engraving.fonts.Fonts) -> PIL.Image.Image:
scale = page.supersampling
image = PIL.Image.new('RGB', (page.width * scale, (bottom - top) * scale), 'white')
context = aggdraw.Draw(image)
for item in primitives:
points = [(x * scale, y * scale - top * scale) for x, y in item.points]
if item.kind == 'text':
continue
if item.kind == 'glyph':
pen = OutlinePen(fonts.glyphs, item.size * scale / fonts.units, *points[0])
fonts.outline(item.value).replay(pen)
path = pen.path
else:
path = aggdraw.Path()
path.moveto(*points[0])
if item.kind == 'curve':
path.curveto(*points[1], *points[2], *points[3])
else:
for point in points[1:]:
path.lineto(*point)
if item.kind == 'polygon':
path.close()
if item.kind in ('glyph', 'polygon'):
context.path(path, None, aggdraw.Brush('black'))
else:
context.path(path, aggdraw.Pen('black', item.thickness * scale))
context.flush()
drawing = PIL.ImageDraw.Draw(image)
for item in primitives:
if item.kind == 'text':
x, y = item.points[0]
drawing.text((x * scale, y * scale - top * scale), item.value, font=fonts.text_font(item.size * scale), anchor='ls', fill='black')
return image.resize((page.width, bottom - top), PIL.Image.Resampling.LANCZOS)
@dataclasses.dataclass(frozen=True)
class Band:
top: int
bottom: int
primitives: tuple[generator.engraving.types.Primitive, ...]
def page_bands(page: generator.engraving.types.Page) -> tuple[Band, Band, Band] | None:
body = tuple(p for system in page.systems for p in system.primitives)
header = tuple(p for p in page.primitives if p.role in ('title', 'subtitle'))
footer = tuple(p for p in page.primitives if p.role == 'page-number')
if not body or not header or not footer or page.primitives != header + body + footer:
return None
bands = []
for primitives in (header, body, footer):
box = generator.engraving.types.bounds(primitives)
if not box.finite() or box.left < 4 or box.right > page.width - 4 or box.top < 4 or box.bottom > page.height - 4:
return None
# Six output pixels cover raster antialiasing and the three-pixel Lanczos
# support. Integer crop origins preserve the full-page sampling phase.
bands.append(Band(max(0, math.floor(box.top) - 6), min(page.height, math.ceil(box.bottom) + 6), primitives))
if bands[0].bottom > bands[1].top or bands[1].bottom > bands[2].top:
return None
return tuple(bands)
def compose_page(page: generator.engraving.types.Page, fonts: generator.engraving.fonts.Fonts, bands: tuple[Band, Band, Band], body: PIL.Image.Image) -> PIL.Image.Image:
image = PIL.Image.new('RGB', (page.width, page.height), 'white')
if body.mode != 'RGB' or body.size != (page.width, bands[1].bottom - bands[1].top):
raise generator.engraving.types.EngravingError('notation band has incompatible geometry')
image.paste(body, (0, bands[1].top))
for band in (bands[0], bands[2]):
with rasterize_band(page, band.primitives, band.top, band.bottom, fonts) as decoration:
image.paste(decoration, (0, band.top))
return image