Source code for psd2svg.rasterizer.resvg_rasterizer

"""Resvg-based rasterizer module.

This module provides SVG rasterization using the resvg library via resvg-py,
offering fast and accurate rendering with no external dependencies.
"""

import logging
import os
import re
from io import BytesIO
from typing import Union

import resvg_py
from PIL import Image

from .base_rasterizer import BaseRasterizer

logger = logging.getLogger(__name__)


[docs] class ResvgRasterizer(BaseRasterizer): """High-performance SVG rasterizer using resvg. This rasterizer uses the resvg library (via resvg-py) to convert SVG documents to raster images. Resvg is a fast, accurate SVG renderer written in Rust that provides excellent quality with minimal dependencies. Note: Resvg does not support CSS @font-face rules with embedded fonts (data URIs). This implementation automatically extracts font file paths from @font-face src: url("file://...") declarations and passes them to resvg's native font loading API. The @font-face CSS rules themselves are ignored by resvg. Data URIs (data:font/...) are not supported and will be silently ignored. Example: >>> rasterizer = ResvgRasterizer(dpi=96) >>> image = rasterizer.from_file('input.svg') >>> image.save('output.png') >>> svg_content = '<svg>...</svg>' >>> image = rasterizer.from_string(svg_content) >>> image.save('output.png') """
[docs] def __init__(self, dpi: int = 0) -> None: """Initialize the resvg rasterizer. Args: dpi: Dots per inch for rendering. If 0 (default), uses resvg's default of 96 DPI. Higher values produce larger, higher resolution images (e.g., 300 DPI for print quality). """ self.dpi = dpi
@staticmethod def _extract_font_file_paths(svg_content: str) -> list[str]: """Extract font file paths from @font-face CSS rules in SVG. Args: svg_content: SVG content as string. Returns: List of valid font file paths found in src: url("file://...") declarations. Invalid paths (non-existent files or non-font extensions) are filtered out with a warning. Note: This method validates extracted paths to prevent: - Access to non-existent files - Access to non-font files (security mitigation) - Log pollution from invalid paths """ # Valid font file extensions VALID_FONT_EXTENSIONS = {".ttf", ".otf", ".woff", ".woff2", ".ttc"} font_files = [] # Pattern to match: src: url("file:///path/to/font.ttf") pattern = re.compile(r'src:\s*url\(["\']?(file://[^"\')]+)["\']?\)') matches = pattern.findall(svg_content) for match in matches: # Remove file:// prefix to get the actual path font_path = match.replace("file://", "") # Validate file extension _, ext = os.path.splitext(font_path.lower()) if ext not in VALID_FONT_EXTENSIONS: logger.warning( f"Skipping invalid font file extension: {font_path} " f"(expected one of {VALID_FONT_EXTENSIONS})" ) continue # Validate file existence if not os.path.isfile(font_path): logger.warning(f"Skipping non-existent font file: {font_path}") continue font_files.append(font_path) return font_files
[docs] def from_file( self, filepath: str, font_files: list[str] | None = None ) -> Image.Image: """Rasterize an SVG file to a PIL Image. Args: filepath: Path to the SVG file to rasterize. font_files: Optional list of font file paths to use for rendering. Returns: PIL Image object in RGBA mode containing the rasterized SVG. Raises: ValueError: If the SVG file does not exist or the content is invalid. """ try: png_bytes = resvg_py.svg_to_bytes( svg_path=filepath, dpi=int(self.dpi), font_files=font_files ) image = Image.open(BytesIO(png_bytes)) return self._composite_background(image) except ValueError as e: raise ValueError(f"Failed to rasterize SVG file '{filepath}': {e}") from e
[docs] def from_string( self, svg_content: Union[str, bytes], font_files: list[str] | None = None ) -> Image.Image: """Rasterize SVG content from a string to a PIL Image. This method provides an optimized implementation that directly rasterizes the SVG content without creating a temporary file. If font_files is not provided, this method automatically extracts font file paths from @font-face CSS rules in the SVG content (file:// URLs). Args: svg_content: SVG content as string or bytes. font_files: Optional list of font file paths to use for rendering. If None, font paths are extracted from the SVG content. Returns: PIL Image object in RGBA mode containing the rasterized SVG. Raises: ValueError: If the SVG content is invalid. """ # Convert bytes to string if necessary svg_string = ( svg_content.decode("utf-8") if isinstance(svg_content, bytes) else svg_content ) # Auto-extract font files from @font-face CSS if not provided if font_files is None: font_files = self._extract_font_file_paths(svg_string) if font_files: logger.debug(f"Extracted {len(font_files)} font file(s) from SVG") try: png_bytes = resvg_py.svg_to_bytes( svg_string=svg_string, dpi=int(self.dpi), font_files=font_files ) image = Image.open(BytesIO(png_bytes)) return self._composite_background(image) except ValueError as e: raise ValueError(f"Failed to rasterize SVG content: {e}") from e