"""PostScript file handling and segment drawing (ESFILE, ESDR07, ESLWID, etc.)."""
from __future__ import annotations
from typing import TextIO
from ephemeris_tools.rendering.escher.constants import (
_GRAY,
BUFSZ,
MAXX,
MAXY,
MINWIDTH,
MINX,
MINY,
)
from ephemeris_tools.rendering.escher.state import EscherState
def _opairi(x: int, y: int, suffix: str) -> str:
"""Format ordered pair of integers as 'X Y suffix' (matches OPAIRI + MOVETO(2:))."""
return f'{x} {y} {suffix}'
def _nint(x: float) -> int:
"""FORTRAN IDNINT: round half away from zero (not banker's rounding)."""
if x >= 0.0:
return int(x + 0.5)
return -int(-x + 0.5)
[docs]
def esfile(
filename: str,
creator: str,
fonts: str,
state: EscherState,
) -> None:
"""Set PostScript output filename, creator, and document fonts (port of ESFILE).
Does not open the file; opening happens on first write (esopen or esdr07).
Resets state (drawn flag, stream handle).
Parameters:
filename: Output path for the PostScript file.
creator: Creator string for %%Creator.
fonts: Document fonts for %%DocumentFonts.
state: Escher state (modified in place).
"""
state.outfil = filename.strip() if filename else ' '
state.creator = (creator or ' ').strip()
state.fonts = (fonts or ' ').strip()
state.outuni = None
state.drawn = False
def _ensure_open(state: EscherState) -> TextIO:
"""Open file on first use and write PS header (from ESDR07 first-call block)."""
if state.outuni is not None:
return state.outuni
state.open = True
outfil = state.outfil.strip() or 'escher.ps'
# Extract basename for %%Title (last path component)
f2 = len(outfil)
f1 = f2
for i in range(f2 - 1, -1, -1):
if outfil[i] in '/:]':
f1 = i
break
title = outfil[f1 + 1 : f2] if f1 < f2 else outfil
creator_nb = state.creator.rstrip()
fonts_nb = state.fonts.rstrip()
# File is stored in state.outuni and must stay open for subsequent writes (SIM115).
f = open(outfil, 'w', encoding='utf-8') # noqa: SIM115
state.outuni = f
f.write('%!PS-Adobe-2.0 EPSF-2.0\n')
f.write(f'%%Title: {title}\n')
f.write(f'%%Creator: {creator_nb}\n')
f.write('%%BoundingBox: 0 0 612 792\n')
f.write('%%Pages: 1\n')
f.write(f'%%DocumentFonts: {fonts_nb}\n')
f.write('%%EndComments\n')
f.write('% \n')
f.write('0.1 0.1 scale\n')
f.write('8 setlinewidth\n')
f.write('1 setlinecap\n')
f.write('1 setlinejoin\n')
f.write('/L {lineto} def\n')
f.write('/M {moveto} def\n')
f.write('/N {newpath} def\n')
f.write('/G {setgray} def\n')
f.write('/S {stroke} def\n')
return f
[docs]
def esopen(state: EscherState) -> None:
"""Open the output file and write PostScript header (port of first-call in ESDR07).
Call after esfile. File is opened on first write if not already open.
Parameters:
state: Escher state (outfil, creator, fonts must be set).
"""
_ensure_open(state)
[docs]
def esdr07(nsegs: int, segs: list[int], state: EscherState) -> None:
"""Draw buffered segments to PostScript (port of ESDR07).
Groups connected segments with the same color into paths and strokes them.
Opens the output file on first call if not already open.
Parameters:
nsegs: Number of integers in segs (must be multiple of 5).
segs: Flat list of (BP, BL, EP, EL, COLOR) per segment: begin pixel/line,
end pixel/line, color code.
state: Escher state (file written, xsave/ysave/oldcol updated).
"""
if nsegs < 5:
return
f = _ensure_open(state)
# Group connected segments with same color
offset = 0
bp = segs[offset]
bl = segs[offset + 1]
ep = segs[offset + 2]
el = segs[offset + 3]
color = segs[offset + 4]
xarray = [bp, ep]
yarray = [bl, el]
count = 2
lstcol = segs[4]
lastep = ep
lastel = el
maxdsp = max(abs(ep - bp), abs(el - bl))
i = 5
while i < nsegs:
offset = i
bp = segs[offset]
bl = segs[offset + 1]
ep = segs[offset + 2]
el = segs[offset + 3]
color = segs[offset + 4]
if bp == lastep and bl == lastel and color == lstcol and count < BUFSZ:
lastep = ep
lastel = el
maxdsp = max(maxdsp, max(abs(ep - bp), abs(el - bl)))
count += 1
xarray.append(ep)
yarray.append(el)
else:
# Flush current path
if maxdsp == 0:
if xarray[count - 1] < MAXX:
xarray[count - 1] = xarray[count - 1] + 1
else:
xarray[count - 1] = xarray[count - 1] - 1
if lstcol >= 0:
f.write('N\n')
f.write(_opairi(xarray[0], yarray[0], 'M') + '\n')
state.xsave = xarray[0]
state.ysave = yarray[0]
lastln = _opairi(xarray[0], yarray[0], 'L')
for m in range(1, count):
lineto = _opairi(xarray[m], yarray[m], 'L')
if lineto != lastln:
f.write(lineto + '\n')
state.xsave = xarray[m]
state.ysave = yarray[m]
state.drawn = True
lastln = lineto
col_out = 1 if lstcol > 10 else lstcol
if col_out != state.oldcol and col_out >= 0:
f.write(_GRAY[min(col_out, 10)] + '\n')
state.oldcol = col_out
f.write('S\n')
count = 2
xarray = [bp, ep]
yarray = [bl, el]
maxdsp = max(abs(ep - bp), abs(el - bl))
lstcol = color
lastep = ep
lastel = el
i += 5
# Flush remaining path
if maxdsp == 0:
if xarray[count - 1] < MAXX:
xarray[count - 1] = xarray[count - 1] + 1
else:
xarray[count - 1] = xarray[count - 1] - 1
if lstcol >= 0:
f.write('N\n')
f.write(_opairi(xarray[0], yarray[0], 'M') + '\n')
state.xsave = xarray[0]
state.ysave = yarray[0]
lastln = _opairi(xarray[0], yarray[0], 'L')
for m in range(1, count):
lineto = _opairi(xarray[m], yarray[m], 'L')
if lineto != lastln:
f.write(lineto + '\n')
state.xsave = xarray[m]
state.ysave = yarray[m]
state.drawn = True
lastln = lineto
col_out = 1 if lstcol > 10 else lstcol
if col_out != state.oldcol and col_out >= 0:
f.write(_GRAY[min(col_out, 10)] + '\n')
state.oldcol = col_out
f.write('S\n')
[docs]
def eslwid(points: float, state: EscherState) -> None:
"""Set line width in points for PostScript output (port of ESLWID).
Writes setlinewidth only when the width changes from the previous call.
Parameters:
points: Line width in points (scaled by 10 for internal units).
state: Escher state (oldwidth updated).
"""
if state.outuni is None:
return
width = max(_nint(points * 10.0), MINWIDTH)
if width == state.oldwidth:
return
state.outuni.write(f'{width:3d} setlinewidth\n')
state.oldwidth = width
[docs]
def eswrit(string: str, state: EscherState) -> None:
"""Write a raw string to the PostScript file (port of ESWRIT).
Adds a newline if the string does not end with one.
Parameters:
string: Text to write (e.g. PostScript commands).
state: Escher state (outuni must be open).
"""
if state.outuni is None:
return
state.outuni.write(string)
if string and not string.endswith('\n'):
state.outuni.write('\n')
[docs]
def esmove(state: EscherState) -> None:
"""Emit PostScript moveto to the end of the last stroked path (port of ESMOVE).
Used to continue drawing from the current point (e.g. for labels).
Parameters:
state: Escher state (xsave, ysave are the last point).
"""
if state.outuni is None:
return
state.outuni.write(_opairi(state.xsave, state.ysave, 'M') + '\n')
[docs]
def escl07(hmin: int, hmax: int, vmin: int, vmax: int, state: EscherState) -> None:
"""Clear a region of the page or end the page (port of ESCL07).
If the region equals the full page (MINX, MAXX, MINY, MAXY), writes
showpage and closes the file (unless external_stream is True). Otherwise
draws a white filled rectangle over the region.
Parameters:
hmin, hmax: Horizontal pixel bounds.
vmin, vmax: Vertical line bounds.
state: Escher state (file may be closed on full-page clear).
"""
if state.outuni is None:
return
if hmin == MINX and hmax == MAXX and vmin == MINY and vmax == MAXY:
state.outuni.write('showpage\n')
if not getattr(state, 'external_stream', False):
state.outuni.close()
state.outuni = None
state.open = False
return
f = state.outuni
f.write('% \n')
f.write('% CLEAR PART OF THE PAGE\n')
f.write('% \n')
f.write('N\n')
f.write(_opairi(hmin, vmin, 'M') + '\n')
f.write(_opairi(hmin, vmax, 'L') + '\n')
f.write(_opairi(hmax, vmax, 'L') + '\n')
f.write(_opairi(hmax, vmin, 'L') + '\n')
f.write(_opairi(hmin, vmin, 'L') + '\n')
f.write('closepath\n')
f.write('1 G\n')
f.write('fill\n')
f.write('0 G\n')
state.oldcol = 1
[docs]
def espl07() -> tuple[int, int, int, int]:
"""Return graphics device boundaries in pixel/line (port of ESPL07).
Returns:
Tuple (MINX, MAXX, MINY, MAXY) for the device.
"""
return (MINX, MAXX, MINY, MAXY)