103 lines
4.4 KiB
Python
103 lines
4.4 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
|
|
import generator.theme
|
|
|
|
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), generator.theme.rgb('background'))
|
|
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(generator.theme.rgb('ink')))
|
|
else:
|
|
context.path(path, aggdraw.Pen(generator.theme.rgb('ink'), 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=generator.theme.rgb('ink'))
|
|
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), generator.theme.rgb('background'))
|
|
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
|