Samad14 commited on
Commit
f3682c6
·
verified ·
1 Parent(s): dcff487

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. app/services/figure_export.py +94 -0
app/services/figure_export.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Publication figure export helpers.
2
+
3
+ BioNexus composes figures as SVG first, then derives distribution formats from
4
+ that single source so panel layout and labels do not diverge between formats.
5
+ Exports are content-addressed and return metadata suitable for figure
6
+ provenance records.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import io
12
+ from dataclasses import dataclass
13
+ from typing import Literal
14
+
15
+ FigureFormat = Literal["svg", "png", "pdf", "tiff"]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class FigureArtifact:
20
+ payload: bytes
21
+ format: FigureFormat
22
+ content_type: str
23
+ dpi: int | None
24
+ sha256: str
25
+ width_px: int | None = None
26
+ height_px: int | None = None
27
+
28
+ def metadata(self) -> dict:
29
+ return {
30
+ "format": self.format,
31
+ "content_type": self.content_type,
32
+ "dpi": self.dpi,
33
+ "sha256": self.sha256,
34
+ "bytes": len(self.payload),
35
+ "width_px": self.width_px,
36
+ "height_px": self.height_px,
37
+ }
38
+
39
+
40
+ def _sha(data: bytes) -> str:
41
+ return hashlib.sha256(data).hexdigest()
42
+
43
+
44
+ def _raster_size(data: bytes) -> tuple[int | None, int | None]:
45
+ try:
46
+ from PIL import Image
47
+ with Image.open(io.BytesIO(data)) as image:
48
+ return image.size
49
+ except Exception:
50
+ return None, None
51
+
52
+
53
+ def export_figure(svg: str, fmt: FigureFormat = "svg", dpi: int = 300) -> FigureArtifact:
54
+ """Convert the canonical SVG into a requested publication format.
55
+
56
+ PNG/TIFF are rendered at 300-600 DPI. PDF remains vector. Conversion
57
+ failures are raised to the caller; an endpoint must never return SVG bytes
58
+ while labelling them as a raster/PDF artifact.
59
+ """
60
+ if fmt not in {"svg", "png", "pdf", "tiff"}:
61
+ raise ValueError(f"unsupported figure format: {fmt}")
62
+ if dpi < 300 or dpi > 600:
63
+ raise ValueError("publication raster DPI must be between 300 and 600")
64
+
65
+ svg_bytes = svg.encode("utf-8")
66
+ if fmt == "svg":
67
+ return FigureArtifact(svg_bytes, "svg", "image/svg+xml", None, _sha(svg_bytes))
68
+
69
+ try:
70
+ import cairosvg
71
+ except ImportError as exc:
72
+ raise RuntimeError("CairoSVG is required for PNG/PDF/TIFF figure export") from exc
73
+
74
+ if fmt == "pdf":
75
+ payload = cairosvg.svg2pdf(bytestring=svg_bytes)
76
+ return FigureArtifact(payload, "pdf", "application/pdf", None, _sha(payload))
77
+
78
+ # CairoSVG interprets output_width/height in pixels; scale from CSS 96 DPI.
79
+ scale = dpi / 96.0
80
+ png = cairosvg.svg2png(bytestring=svg_bytes, scale=scale)
81
+ if fmt == "png":
82
+ width, height = _raster_size(png)
83
+ return FigureArtifact(png, "png", "image/png", dpi, _sha(png), width, height)
84
+
85
+ try:
86
+ from PIL import Image
87
+ except ImportError as exc:
88
+ raise RuntimeError("Pillow is required for TIFF figure export") from exc
89
+ source = Image.open(io.BytesIO(png)).convert("RGB")
90
+ out = io.BytesIO()
91
+ source.save(out, format="TIFF", dpi=(dpi, dpi), compression="tiff_lzw")
92
+ payload = out.getvalue()
93
+ width, height = source.size
94
+ return FigureArtifact(payload, "tiff", "image/tiff", dpi, _sha(payload), width, height)