返回 ppt-master
media.py
1 """SVG to PNG conversion for Office compatibility mode."""
2
3 from __future__ import annotations
4
5 import hashlib
6 import shutil
7 import tempfile
8 from pathlib import Path
9
10 # SVG to PNG library detection
11 # Prefer CairoSVG (better quality), fall back to svglib
12 PNG_RENDERER: str | None = None
13
14 try:
15 import cairosvg
16 PNG_RENDERER = 'cairosvg'
17 except (ImportError, OSError):
18 try:
19 from svglib.svglib import svg2rlg
20 from reportlab.graphics import renderPM
21 PNG_RENDERER = 'svglib'
22 except (ImportError, OSError):
23 pass
24
25
26 def get_png_renderer_info() -> tuple[str | None, str, str | None]:
27 """Get PNG renderer status information.
28
29 Returns:
30 (renderer_name, status_text, install_hint) tuple.
31 """
32 if PNG_RENDERER == 'cairosvg':
33 return ('cairosvg', '(full gradient/filter support)', None)
34 elif PNG_RENDERER == 'svglib':
35 return ('svglib', '(some gradients may be lost)',
36 'Install cairosvg for better results: pip install cairosvg')
37 else:
38 return (None, '(not installed)',
39 'Install via: pip install cairosvg or pip install svglib reportlab')
40
41
42 def convert_svg_to_png(
43 svg_path: Path,
44 png_path: Path,
45 width: int | None = None,
46 height: int | None = None,
47 ) -> bool:
48 """Convert SVG to PNG using the available renderer.
49
50 Args:
51 svg_path: SVG file path.
52 png_path: Output PNG file path.
53 width: Output width in pixels.
54 height: Output height in pixels.
55
56 Returns:
57 Whether the conversion was successful.
58 """
59 if PNG_RENDERER is None:
60 return False
61
62 try:
63 if PNG_RENDERER == 'cairosvg':
64 cairosvg.svg2png(
65 url=str(svg_path),
66 write_to=str(png_path),
67 output_width=width,
68 output_height=height,
69 )
70 return True
71
72 elif PNG_RENDERER == 'svglib':
73 drawing = svg2rlg(str(svg_path))
74 if drawing is None:
75 print(f" Warning: Unable to parse SVG ({svg_path.name})")
76 return False
77 renderPM.drawToFile(
78 drawing,
79 str(png_path),
80 fmt="PNG",
81 configPIL={'quality': 95},
82 )
83 return True
84
85 except Exception as e:
86 print(f" Warning: SVG to PNG conversion failed ({svg_path.name}): {e}")
87 return False
88
89 return False
90
91
92 def _cache_key(svg_path: Path, width: int | None, height: int | None) -> str:
93 h = hashlib.sha256()
94 with open(svg_path, 'rb') as f:
95 for chunk in iter(lambda: f.read(65536), b''):
96 h.update(chunk)
97 return f"{h.hexdigest()}_{width or 0}x{height or 0}_{PNG_RENDERER or 'none'}"
98
99
100 def convert_svg_to_png_cached(
101 svg_path: Path,
102 png_path: Path,
103 width: int | None = None,
104 height: int | None = None,
105 cache_dir: Path | None = None,
106 ) -> bool:
107 """Cache-aware SVG→PNG conversion.
108
109 Returns True on success (cache hit or fresh render). Cache key bakes in
110 SVG content hash + size + renderer name; switching renderers invalidates
111 naturally. Failures are never cached.
112 """
113 if cache_dir is None:
114 return convert_svg_to_png(svg_path, png_path, width, height)
115
116 if PNG_RENDERER is None:
117 return False
118
119 try:
120 key = _cache_key(svg_path, width, height)
121 except OSError as e:
122 print(f" Warning: Failed to hash SVG ({svg_path.name}): {e}")
123 return convert_svg_to_png(svg_path, png_path, width, height)
124
125 cached = cache_dir / f"{key}.png"
126 if cached.is_file():
127 try:
128 shutil.copy(cached, png_path)
129 return True
130 except OSError as e:
131 print(f" Warning: Cache copy failed, re-rendering ({svg_path.name}): {e}")
132
133 cache_dir.mkdir(parents=True, exist_ok=True)
134 tmp_fd, tmp_name = tempfile.mkstemp(suffix='.png', dir=str(cache_dir))
135 tmp_path = Path(tmp_name)
136 import os
137 os.close(tmp_fd)
138
139 ok = convert_svg_to_png(svg_path, tmp_path, width, height)
140 if not ok:
141 try:
142 tmp_path.unlink()
143 except OSError:
144 pass
145 return False
146
147 try:
148 os.replace(tmp_path, cached)
149 except OSError:
150 try:
151 tmp_path.unlink()
152 except OSError:
153 pass
154
155 try:
156 shutil.copy(cached, png_path)
157 return True
158 except OSError as e:
159 print(f" Warning: Cache copy failed ({svg_path.name}): {e}")
160 return False
161
161 lines PYTHON