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, *, ink=None, background=None, output_scale=1) -> PIL.Image.Image: return rasterize_band(page, page.primitives, 0, page.height, fonts, ink=ink, background=background, output_scale=output_scale) def rasterize_band(page: generator.engraving.types.Page, primitives: tuple[generator.engraving.types.Primitive, ...], top: int, bottom: int, fonts: generator.engraving.fonts.Fonts, *, ink=None, background=None, output_scale=1) -> PIL.Image.Image: ink = generator.theme.rgb('ink') if ink is None else ink background = generator.theme.rgb('background') if background is None else background scale = page.supersampling * output_scale image = PIL.Image.new('RGB', (round(page.width * scale), round((bottom - top) * scale)), 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(ink)) else: context.path(path, aggdraw.Pen(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=ink) result = image.resize((round(page.width * output_scale), round((bottom - top) * output_scale)), PIL.Image.Resampling.LANCZOS) image.close() return result @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