v0.5.3
- Мелкие исправления увеличивающие работу конвертера
This commit is contained in:
+208
-30
@@ -60,6 +60,11 @@ PLANTUML_JAR_URL = (
|
||||
f"https://github.com/plantuml/plantuml/releases/download/"
|
||||
f"v{PLANTUML_VERSION}/plantuml-{PLANTUML_VERSION}.jar"
|
||||
)
|
||||
# Part of the cache key: a new jar draws the same source differently.
|
||||
PLANTUML_CACHE_PREFIX = f"plantuml-{PLANTUML_VERSION}\n"
|
||||
|
||||
# One JVM per chunk; keeps the command line far below the Windows 32k limit.
|
||||
PLANTUML_BATCH_CHUNK = 60
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
@@ -99,6 +104,7 @@ def configure_diagrams(
|
||||
global _CONFIG
|
||||
ensure_user_schemes()
|
||||
configure_schemes(schemes_path=schemes_path, md_dir=md_dir)
|
||||
reset_diagram_batch()
|
||||
try:
|
||||
from .mermaid_renderer import ensure_playwright_browsers_env, reset_mermaid_session
|
||||
|
||||
@@ -298,17 +304,45 @@ def _is_svg(data: bytes) -> bool:
|
||||
return head.startswith(b"<svg") or head.startswith(b"<?xml") or b"<svg" in head
|
||||
|
||||
|
||||
def _render_plantuml_jar_one(
|
||||
source: str,
|
||||
out_path: Path,
|
||||
@dataclass
|
||||
class _PumlJob:
|
||||
"""One PlantUML source and where its rendered file must land."""
|
||||
|
||||
source: str
|
||||
out_path: Path
|
||||
|
||||
|
||||
def _pick_produced(out_dir: Path, fmt: str) -> Path | None:
|
||||
"""Output name follows `@startuml <name>`, so take what the jar actually wrote."""
|
||||
if not out_dir.is_dir():
|
||||
return None
|
||||
files = [
|
||||
p for p in out_dir.iterdir()
|
||||
if p.is_file() and p.suffix.lower() == f".{fmt}"
|
||||
]
|
||||
if not files:
|
||||
return None
|
||||
# `newpage` adds name_001, name_002…; the shortest name is the first page.
|
||||
files.sort(key=lambda p: (len(p.name), p.name))
|
||||
return files[0]
|
||||
|
||||
|
||||
def _plantuml_run_chunk(
|
||||
chunk: list[_PumlJob],
|
||||
jar: str,
|
||||
fmt: Literal["png", "svg"],
|
||||
) -> bool:
|
||||
if not jar or not os.path.isfile(jar) or not _java_available():
|
||||
return False
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src = Path(tmp) / "diagram.puml"
|
||||
src.write_text(source, encoding="utf-8")
|
||||
done: list[bool],
|
||||
offset: int,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="md2gost-puml-") as tmp:
|
||||
root = Path(tmp)
|
||||
sources: list[str] = []
|
||||
for i, job in enumerate(chunk):
|
||||
sub = root / f"{i:04d}"
|
||||
sub.mkdir()
|
||||
src = sub / "src.puml"
|
||||
src.write_text(job.source, encoding="utf-8")
|
||||
sources.append(str(src))
|
||||
cmd = [
|
||||
"java",
|
||||
f"-Dplantuml.include.path={diagrams_dir().resolve()}",
|
||||
@@ -316,27 +350,63 @@ def _render_plantuml_jar_one(
|
||||
"-jar", jar,
|
||||
f"-t{fmt}",
|
||||
"-charset", "UTF-8",
|
||||
"-o", tmp,
|
||||
str(src),
|
||||
# Relative -o resolves against each input's own folder, so two sources
|
||||
# naming themselves `@startuml Name` cannot collide on one output file.
|
||||
"-o", "out",
|
||||
*sources,
|
||||
]
|
||||
timeout = min(600, 120 + 30 * (len(chunk) - 1))
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=120, check=False,
|
||||
cmd, capture_output=True, text=True, timeout=timeout, check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as e:
|
||||
_log.warning("PlantUML jar failed: %s", e)
|
||||
return False
|
||||
produced = Path(tmp) / f"diagram.{fmt}"
|
||||
if proc.returncode != 0 or not produced.is_file():
|
||||
return
|
||||
if proc.returncode != 0:
|
||||
_log.warning("PlantUML jar error: %s", proc.stderr or proc.stdout)
|
||||
return False
|
||||
data = produced.read_bytes()
|
||||
if fmt == "png" and not _is_png(data):
|
||||
return False
|
||||
if fmt == "svg" and not _is_svg(data):
|
||||
return False
|
||||
shutil.copyfile(produced, out_path)
|
||||
return True
|
||||
return
|
||||
for i, job in enumerate(chunk):
|
||||
produced = _pick_produced(root / f"{i:04d}" / "out", fmt)
|
||||
if produced is None:
|
||||
_log.warning("PlantUML jar: %s не создан", fmt)
|
||||
continue
|
||||
data = produced.read_bytes()
|
||||
if fmt == "png" and not _is_png(data):
|
||||
continue
|
||||
if fmt == "svg" and not _is_svg(data):
|
||||
continue
|
||||
job.out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(produced, job.out_path)
|
||||
done[offset + i] = True
|
||||
|
||||
|
||||
def _plantuml_run(
|
||||
jobs: list[_PumlJob],
|
||||
jar: str,
|
||||
fmt: Literal["png", "svg"],
|
||||
) -> list[bool]:
|
||||
"""Render every job in one JVM per chunk. Returns per-job success flags."""
|
||||
if not jobs:
|
||||
return []
|
||||
if not jar or not os.path.isfile(jar) or not _java_available():
|
||||
return [False] * len(jobs)
|
||||
done = [False] * len(jobs)
|
||||
for start in range(0, len(jobs), PLANTUML_BATCH_CHUNK):
|
||||
_plantuml_run_chunk(
|
||||
jobs[start:start + PLANTUML_BATCH_CHUNK], jar, fmt, done, start,
|
||||
)
|
||||
return done
|
||||
|
||||
|
||||
def _render_plantuml_jar_one(
|
||||
source: str,
|
||||
out_path: Path,
|
||||
jar: str,
|
||||
fmt: Literal["png", "svg"],
|
||||
) -> bool:
|
||||
done = _plantuml_run([_PumlJob(source, Path(out_path))], jar, fmt)
|
||||
return bool(done and done[0])
|
||||
|
||||
|
||||
def _render_plantuml_jar(
|
||||
@@ -355,6 +425,100 @@ def _render_plantuml_jar(
|
||||
return True
|
||||
|
||||
|
||||
def _plantuml_scaled(prepared: str, scale: float) -> tuple[str, float]:
|
||||
"""Return (source, pixel_scale); pixel_scale stays 1 when the author set scale."""
|
||||
if scale <= 1:
|
||||
return prepared, 1.0
|
||||
boosted = _ensure_plantuml_png_scale(prepared, scale)
|
||||
return boosted, (scale if boosted != prepared else 1.0)
|
||||
|
||||
|
||||
# Fences seen while parsing, so the whole document can share one JVM.
|
||||
_PENDING: list[tuple[str, str]] = []
|
||||
_PENDING_SEEN: set[tuple[str, str]] = set()
|
||||
_BATCH_FLUSHED = False
|
||||
|
||||
|
||||
def register_pending_diagram(lang: str, source: str) -> None:
|
||||
"""Announce a diagram fence found during parsing (see _flush_plantuml_batch)."""
|
||||
key = (lang or "", source or "")
|
||||
if key in _PENDING_SEEN:
|
||||
return
|
||||
_PENDING_SEEN.add(key)
|
||||
_PENDING.append(key)
|
||||
|
||||
|
||||
def reset_diagram_batch() -> None:
|
||||
global _BATCH_FLUSHED
|
||||
_PENDING.clear()
|
||||
_PENDING_SEEN.clear()
|
||||
_BATCH_FLUSHED = False
|
||||
|
||||
|
||||
def _flush_plantuml_batch() -> None:
|
||||
"""
|
||||
Pre-pass before the first diagram renders: draw every cache-missing PlantUML
|
||||
source of the document in one JVM instead of one (two in svg mode) per diagram.
|
||||
Anything that fails here simply stays a cache miss and takes the normal path.
|
||||
"""
|
||||
global _BATCH_FLUSHED
|
||||
if _BATCH_FLUSHED:
|
||||
return
|
||||
_BATCH_FLUSHED = True
|
||||
pending = list(_PENDING)
|
||||
_PENDING.clear()
|
||||
_PENDING_SEEN.clear()
|
||||
if len(pending) < 2:
|
||||
return
|
||||
jar = resolve_plantuml_jar(_CONFIG.plantuml_jar or None, download=False)
|
||||
if not jar or not _java_available():
|
||||
return
|
||||
|
||||
want_svg = _CONFIG.diagram_format == "svg"
|
||||
scale = _CONFIG.diagram_scale
|
||||
png_jobs: list[_PumlJob] = []
|
||||
svg_jobs: list[_PumlJob] = []
|
||||
seen: set[str] = set()
|
||||
for lang, source in pending:
|
||||
try:
|
||||
prepared, kroki_type = prepare_source(lang, source)
|
||||
except Exception:
|
||||
continue
|
||||
if kroki_type != "plantuml":
|
||||
continue
|
||||
prepared, _ = _plantuml_scaled(prepared, scale)
|
||||
out_png, out_svg = _cache_paths(prepared, cache_prefix=PLANTUML_CACHE_PREFIX)
|
||||
if str(out_png) in seen:
|
||||
continue
|
||||
seen.add(str(out_png))
|
||||
if not (out_png.is_file() and out_png.stat().st_size > 0):
|
||||
png_jobs.append(_PumlJob(prepared, out_png))
|
||||
if want_svg and not (out_svg.is_file() and out_svg.stat().st_size > 0):
|
||||
svg_jobs.append(_PumlJob(prepared, out_svg))
|
||||
|
||||
if not png_jobs and not svg_jobs:
|
||||
return
|
||||
_log.info(
|
||||
"PlantUML: пакетный рендер %d диаграмм одной JVM",
|
||||
max(len(png_jobs), len(svg_jobs)),
|
||||
)
|
||||
if png_jobs:
|
||||
_plantuml_run(png_jobs, jar, "png")
|
||||
if svg_jobs:
|
||||
_plantuml_run(svg_jobs, jar, "svg")
|
||||
|
||||
|
||||
def shutdown_diagrams() -> None:
|
||||
"""Release per-conversion resources: headless browser and the batch queue."""
|
||||
reset_diagram_batch()
|
||||
try:
|
||||
from .mermaid_renderer import reset_mermaid_session
|
||||
|
||||
reset_mermaid_session()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _render_kroki(
|
||||
source: str,
|
||||
diagram_type: str,
|
||||
@@ -414,22 +578,32 @@ def render_diagram(
|
||||
diagram_scale: float | None = None,
|
||||
) -> DiagramRenderResult:
|
||||
"""Render diagram to PNG (+ optional SVG). Raises RuntimeError if all backends fail."""
|
||||
ensure_user_schemes()
|
||||
# Batch only the plain document path; explicit overrides would not match _CONFIG.
|
||||
if not any(
|
||||
arg is not None
|
||||
for arg in (
|
||||
plantuml_jar, kroki_url, fallback, cache_dir, diagram_format, diagram_scale,
|
||||
)
|
||||
):
|
||||
try:
|
||||
_flush_plantuml_batch()
|
||||
except Exception as exc:
|
||||
_log.warning("PlantUML: пакетный рендер не удался (%s)", exc)
|
||||
|
||||
prepared, kroki_type = prepare_source(lang, source)
|
||||
scale = _CONFIG.diagram_scale if diagram_scale is None else float(diagram_scale)
|
||||
if scale < 1:
|
||||
scale = 1.0
|
||||
# pixel_scale: only when we injected scale (author override → treat as 1 for display).
|
||||
pixel_scale = 1.0
|
||||
if kroki_type == "plantuml" and scale > 1:
|
||||
before = prepared
|
||||
prepared = _ensure_plantuml_png_scale(prepared, scale)
|
||||
if prepared != before:
|
||||
pixel_scale = scale
|
||||
if kroki_type == "plantuml":
|
||||
prepared, pixel_scale = _plantuml_scaled(prepared, scale)
|
||||
# Mermaid: pixel_scale set only after successful local hi-res render (not Kroki).
|
||||
|
||||
cache_prefix = ""
|
||||
if kroki_type == "mermaid":
|
||||
if kroki_type == "plantuml":
|
||||
cache_prefix = PLANTUML_CACHE_PREFIX
|
||||
elif kroki_type == "mermaid":
|
||||
try:
|
||||
from .mermaid_renderer import CACHE_KEY_PREFIX
|
||||
|
||||
@@ -633,8 +807,12 @@ __all__ = [
|
||||
"fetch_plantuml_jar",
|
||||
"is_diagram_lang",
|
||||
"prepare_source",
|
||||
"register_pending_diagram",
|
||||
"render_diagram",
|
||||
"reset_diagram_batch",
|
||||
"resolve_plantuml_jar",
|
||||
"shutdown_diagrams",
|
||||
"PLANTUML_CACHE_PREFIX",
|
||||
"PLANTUML_JAR_URL",
|
||||
"PLANTUML_VERSION",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user