106 lines
3.3 KiB
Python
106 lines
3.3 KiB
Python
import asyncio
|
|
import dataclasses
|
|
import enum
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import aiofiles
|
|
from folkugat_web.log import logger
|
|
from folkugat_web.model.lilypond.processing import RenderError
|
|
from folkugat_web.services import files as files_service
|
|
from folkugat_web.utils import Result
|
|
|
|
from .source import SCORE_BEGINNING
|
|
|
|
RenderResult = Result[Path, list[RenderError]]
|
|
|
|
|
|
class RenderOutput(enum.Enum):
|
|
PDF = "pdf"
|
|
PNG_CROPPED = "png-cropped"
|
|
PNG_PREVIEW = "png-preview"
|
|
|
|
|
|
async def render(
|
|
source: str,
|
|
output: RenderOutput,
|
|
output_file: Path | None = None,
|
|
) -> RenderResult:
|
|
async with files_service.tmp_file(content=source) as input_file:
|
|
return await render_file(
|
|
input_file=input_file,
|
|
output=output,
|
|
output_file=output_file,
|
|
)
|
|
|
|
|
|
async def render_file(
|
|
input_file: Path,
|
|
output: RenderOutput,
|
|
output_file: Path | None = None,
|
|
) -> RenderResult:
|
|
output_file = output_file or files_service.create_tmp_filename()
|
|
match output:
|
|
case RenderOutput.PDF:
|
|
command = [
|
|
"lilypond",
|
|
"-f", "pdf",
|
|
"-o", str(output_file),
|
|
str(input_file)
|
|
]
|
|
output_file = output_file.with_suffix(".pdf")
|
|
case RenderOutput.PNG_CROPPED:
|
|
command = [
|
|
"lilypond",
|
|
"-f", "png",
|
|
"-dcrop=#t",
|
|
"-dno-print-pages",
|
|
# "-duse-paper-size-for-page=#f",
|
|
"-o", str(output_file),
|
|
str(input_file)
|
|
]
|
|
output_file = output_file.with_suffix(".cropped.png")
|
|
# output_file = output_file.with_suffix(".png")
|
|
case RenderOutput.PNG_PREVIEW:
|
|
command = [
|
|
"lilypond",
|
|
"-f", "png",
|
|
"-dpreview=#t",
|
|
"-dno-print-pages",
|
|
"-o", str(output_file),
|
|
str(input_file)
|
|
]
|
|
output_file = output_file.with_suffix(".preview.png")
|
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*command,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
|
|
_stdout, stderr = await proc.communicate()
|
|
|
|
if proc.returncode:
|
|
errors = await _build_errors(input_file=input_file, stderr=stderr)
|
|
return Result(error=errors)
|
|
|
|
return Result(result=output_file)
|
|
|
|
|
|
async def _build_errors(input_file: Path, stderr: bytes) -> list[RenderError]:
|
|
stderr_lines = stderr.decode("utf-8").splitlines()
|
|
logger.warning("Lilypond errors:")
|
|
for line in stderr_lines:
|
|
logger.warning(line)
|
|
async with aiofiles.open(input_file, "r") as f:
|
|
lines = await f.readlines()
|
|
|
|
offset_mark = SCORE_BEGINNING + "\n"
|
|
line_offset = lines.index(offset_mark) + 1 if offset_mark in lines else 0
|
|
esc_input_file = re.escape(str(input_file))
|
|
error_re = re.compile(rf"{esc_input_file}:(?P<line>\d+):(?P<pos>\d+): error: (?P<error>.*)$")
|
|
error_matches = filter(None, map(error_re.match, stderr_lines))
|
|
errors = map(lambda m: RenderError.from_dict(m.groupdict()), error_matches)
|
|
errors_offset = [dataclasses.replace(e, line=e.line - line_offset) for e in errors if e.line > line_offset]
|
|
return errors_offset
|