|
11 | 11 | SPDX-License-Identifier: GPL-3.0-or-later |
12 | 12 | """ |
13 | 13 |
|
| 14 | +import subprocess |
14 | 15 | import urllib.request |
15 | 16 | from pathlib import Path |
| 17 | +from typing import Optional |
16 | 18 | from urllib.parse import urlparse |
17 | 19 |
|
| 20 | +# Modern SVG conversion backends (in order of preference) |
| 21 | +try: |
| 22 | + import cairosvg |
| 23 | + |
| 24 | + HAS_CAIROSVG = True |
| 25 | +except ImportError: |
| 26 | + HAS_CAIROSVG = False |
| 27 | + |
| 28 | +try: |
| 29 | + from wand import image as wand_image |
| 30 | + |
| 31 | + HAS_WAND = True |
| 32 | +except ImportError: |
| 33 | + HAS_WAND = False |
| 34 | + |
| 35 | +try: |
| 36 | + from reportlab.graphics import renderPDF, renderPM |
| 37 | + from svglib.svglib import renderSVG |
| 38 | + |
| 39 | + HAS_SVGLIB = True |
| 40 | +except ImportError: |
| 41 | + HAS_SVGLIB = False |
| 42 | + |
| 43 | + |
| 44 | +# Check for system tools |
| 45 | +def has_inkscape() -> bool: |
| 46 | + """Check if Inkscape is available.""" |
| 47 | + try: |
| 48 | + subprocess.run(["inkscape", "--version"], capture_output=True, check=True) |
| 49 | + return True |
| 50 | + except (subprocess.CalledProcessError, FileNotFoundError): |
| 51 | + return False |
| 52 | + |
| 53 | + |
| 54 | +def get_inkscape_version() -> tuple[int, int]: |
| 55 | + """Get Inkscape major and minor version numbers.""" |
| 56 | + try: |
| 57 | + result = subprocess.run(["inkscape", "--version"], capture_output=True, check=True, text=True) |
| 58 | + # Parse version like "Inkscape 1.4.2 (2aeb623e1d, 2025-05-12)" |
| 59 | + version_line = result.stdout.strip().split("\n")[0] |
| 60 | + version_part = version_line.split()[1] # Get "1.4.2" |
| 61 | + major, minor = map(int, version_part.split(".")[:2]) # Get 1, 4 |
| 62 | + return major, minor |
| 63 | + except (subprocess.CalledProcessError, FileNotFoundError, ValueError, IndexError): |
| 64 | + return 0, 92 # Default to old version format |
| 65 | + |
| 66 | + |
| 67 | +def has_rsvg_convert() -> bool: |
| 68 | + """Check if rsvg-convert is available.""" |
| 69 | + try: |
| 70 | + subprocess.run(["rsvg-convert", "--version"], capture_output=True, check=True) |
| 71 | + return True |
| 72 | + except (subprocess.CalledProcessError, FileNotFoundError): |
| 73 | + return False |
| 74 | + |
| 75 | + |
18 | 76 | # ruff: noqa: T201 |
19 | 77 |
|
20 | 78 | # List of all motor diagram SVG files from the ArduPilot documentation at |
@@ -109,5 +167,126 @@ def download_motor_diagrams() -> None: |
109 | 167 | print(f"\nDownload complete: {downloaded} succeeded, {failed} failed") |
110 | 168 |
|
111 | 169 |
|
| 170 | +def convert_svg_to_png(result_height: Optional[int] = None) -> None: |
| 171 | + """ |
| 172 | + Convert all downloaded SVG files to PNG using the best available backend. |
| 173 | +
|
| 174 | + Args: |
| 175 | + result_height: Optional target height in pixels. Width will be calculated |
| 176 | + to preserve aspect ratio. If None, uses original size. |
| 177 | +
|
| 178 | + Tries conversion backends in order of preference: |
| 179 | + 1. Inkscape (most reliable, high quality) |
| 180 | + 2. rsvg-convert (librsvg, good quality) |
| 181 | + 3. Wand/ImageMagick (good for complex SVGs) |
| 182 | + 4. svglib + reportlab (pure Python, fast) |
| 183 | + 5. cairosvg (fallback, sometimes has issues) |
| 184 | +
|
| 185 | + """ |
| 186 | + images_dir = Path("ardupilot_methodic_configurator/images") |
| 187 | + |
| 188 | + # Determine the best available backend |
| 189 | + backend = None |
| 190 | + if has_inkscape(): |
| 191 | + backend = "inkscape" |
| 192 | + print("Using Inkscape for SVG conversion (best quality)") |
| 193 | + elif has_rsvg_convert(): |
| 194 | + backend = "rsvg-convert" |
| 195 | + print("Using rsvg-convert for SVG conversion (good quality)") |
| 196 | + elif HAS_WAND: |
| 197 | + backend = "wand" |
| 198 | + print("Using Wand/ImageMagick for SVG conversion") |
| 199 | + elif HAS_SVGLIB: |
| 200 | + backend = "svglib" |
| 201 | + print("Using svglib for SVG conversion") |
| 202 | + elif HAS_CAIROSVG: |
| 203 | + backend = "cairosvg" |
| 204 | + print("Using cairosvg for SVG conversion (fallback)") |
| 205 | + else: |
| 206 | + print("No SVG conversion backend available. Install one of:") |
| 207 | + print(" - inkscape (apt install inkscape)") |
| 208 | + print(" - librsvg2-bin (apt install librsvg2-bin)") |
| 209 | + print(" - pip install Wand") |
| 210 | + print(" - pip install svglib reportlab") |
| 211 | + print(" - pip install cairosvg") |
| 212 | + return |
| 213 | + |
| 214 | + converted = 0 |
| 215 | + failed = 0 |
| 216 | + |
| 217 | + for filename in motor_diagrams: |
| 218 | + try: |
| 219 | + svg_path = images_dir / filename |
| 220 | + png_path = images_dir / filename.replace(".svg", ".png") |
| 221 | + |
| 222 | + if not svg_path.exists(): |
| 223 | + print(f"SVG file not found: {svg_path}") |
| 224 | + failed += 1 |
| 225 | + continue |
| 226 | + |
| 227 | + print(f"Converting {filename} to PNG...") |
| 228 | + |
| 229 | + if backend == "inkscape": |
| 230 | + # Use different syntax based on Inkscape version |
| 231 | + major, minor = get_inkscape_version() |
| 232 | + |
| 233 | + if major >= 1: # Modern Inkscape (1.0+) with modern syntax |
| 234 | + cmd = ["inkscape", "--export-type=png", f"--export-filename={png_path}", str(svg_path)] |
| 235 | + if result_height is not None: |
| 236 | + cmd.insert(-1, f"--export-height={result_height}") |
| 237 | + else: # Legacy Inkscape (0.x) with old syntax |
| 238 | + cmd = ["inkscape", str(svg_path), "--export-png", str(png_path), "--export-dpi=300"] |
| 239 | + if result_height is not None: |
| 240 | + cmd.append(f"--export-height={result_height}") |
| 241 | + |
| 242 | + subprocess.run(cmd, check=True, capture_output=True) |
| 243 | + |
| 244 | + elif backend == "rsvg-convert": |
| 245 | + cmd = [ |
| 246 | + "rsvg-convert", |
| 247 | + "-f", |
| 248 | + "png", |
| 249 | + "-d", |
| 250 | + "600", # DPI |
| 251 | + "-p", |
| 252 | + "600", # DPI |
| 253 | + ] |
| 254 | + |
| 255 | + # Add height parameter if specified |
| 256 | + if result_height is not None: |
| 257 | + cmd.extend(["-h", str(result_height)]) |
| 258 | + |
| 259 | + cmd.append(str(svg_path)) |
| 260 | + |
| 261 | + with open(png_path, "wb") as f: |
| 262 | + subprocess.run(cmd, stdout=f, check=True) |
| 263 | + |
| 264 | + elif backend == "wand": |
| 265 | + with wand_image.Image() as img: |
| 266 | + img.read(filename=str(svg_path)) |
| 267 | + img.format = "png" |
| 268 | + img.resolution = (300, 300) |
| 269 | + img.save(filename=str(png_path)) |
| 270 | + |
| 271 | + elif backend == "svglib": |
| 272 | + from reportlab.graphics import renderPM |
| 273 | + from svglib.svglib import renderSVG |
| 274 | + |
| 275 | + drawing = renderSVG.renderSVG(str(svg_path)) |
| 276 | + renderPM.drawToFile(drawing, str(png_path), fmt="PNG", dpi=300) |
| 277 | + |
| 278 | + elif backend == "cairosvg": |
| 279 | + cairosvg.svg2png(url=str(svg_path), write_to=str(png_path), dpi=300) |
| 280 | + |
| 281 | + converted += 1 |
| 282 | + |
| 283 | + except Exception as e: # pylint: disable=broad-exception-caught |
| 284 | + print(f"Failed to convert {filename}: {e}") |
| 285 | + failed += 1 |
| 286 | + |
| 287 | + print(f"\nConversion complete: {converted} succeeded, {failed} failed") |
| 288 | + |
| 289 | + |
112 | 290 | if __name__ == "__main__": |
113 | | - download_motor_diagrams() |
| 291 | + # download_motor_diagrams() |
| 292 | + convert_svg_to_png(result_height=320) |
0 commit comments