"""A library for reading and converting SVG files. This module provides a converter from SVG to ReportLab Graphics (RLG) drawings. It handles basic shapes, paths, and simple text elements. The intended usage is either as a module within other projects or from the command-line for converting SVG files to PDF. It also supports gzip-compressed SVG files with the .svgz extension. Example: To convert an SVG file to a ReportLab Drawing object:: from svglib.svglib import svg2rlg drawing = svg2rlg("foo.svg") To convert an SVG file to a PDF from the command-line:: $ python -m svglib foo.svg """ import base64 import copy import gzip import itertools import logging import os import pathlib import re import shlex import shutil from collections import defaultdict, namedtuple from io import BytesIO from typing import Any, Dict, List, Optional, Tuple, Union from PIL import Image as PILImage from reportlab.graphics.shapes import ( _CLOSEPATH, Circle, Drawing, Ellipse, Group, Image, Line, Path, Polygon, PolyLine, Rect, SolidShape, String, ) from reportlab.lib import colors from reportlab.lib.units import pica, toLength from reportlab.pdfbase.pdfmetrics import stringWidth from reportlab.pdfgen.canvas import FILL_EVEN_ODD, FILL_NON_ZERO from reportlab.pdfgen.pdfimages import PDFImage try: from reportlab.graphics.transform import mmult except ImportError: # Before Reportlab 3.5.61 from reportlab.graphics.shapes import mmult import cssselect2 import tinycss2 from lxml import etree from .fonts import ( DEFAULT_FONT_NAME, DEFAULT_FONT_SIZE, DEFAULT_FONT_STYLE, DEFAULT_FONT_WEIGHT, get_global_font_map, ) from .fonts import ( find_font as _fonts_find_font, ) # To keep backward compatibility, since those functions where previously part of # the svglib module from .fonts import ( register_font as _fonts_register_font, ) from .utils import ( bezier_arc_from_end_points, convert_quadratic_to_cubic_path, normalise_svg_path, ) def _convert_palette_to_rgba(image: PILImage.Image) -> PILImage.Image: """Convert a palette-based image with transparency to RGBA format. This function checks if a PIL Image is in palette mode ('P') and has transparency information. If so, it converts the image to RGBA to prevent potential warnings or errors during processing. Args: image: The input PIL Image object. Returns: The converted RGBA PIL Image object if changes were made, otherwise the original image. """ if image.mode == "P" and "transparency" in image.info: # Convert palette image with transparency to RGBA return image.convert("RGBA") return image def register_font( font_name: str, font_path: Optional[str] = None, weight: str = "normal", style: str = "normal", rlgFontName: Optional[str] = None, ) -> Tuple[Optional[str], bool]: """Register a font for use in SVG processing. This function serves as a backward-compatible wrapper for the font registration logic defined in the `svglib.fonts` module. Args: font_name: The name of the font to register. font_path: The file path to the font file (optional). weight: The font weight (e.g., 'normal', 'bold'). style: The font style (e.g., 'normal', 'italic'). rlgFontName: The ReportLab-specific font name (optional). Returns: A tuple containing the registered font name and a boolean indicating if the registration was successful. """ return _fonts_register_font(font_name, font_path, weight, style, rlgFontName) def find_font( font_name: str, weight: str = "normal", style: str = "normal" ) -> Tuple[str, bool]: """Find a registered font by its properties. This function serves as a backward-compatible wrapper for the font finding logic defined in the `svglib.fonts` module. Args: font_name: The name of the font to find. weight: The font weight to match. style: The font style to match. Returns: A tuple containing the matched font name and a boolean indicating if an exact match was found. """ return _fonts_find_font(font_name, weight, style) XML_NS = "http://www.w3.org/XML/1998/namespace" # A sentinel to identify a situation where a node reference a fragment not yet defined. DELAYED = object() logger = logging.getLogger(__name__) Box = namedtuple("Box", ["x", "y", "width", "height"]) split_whitespace = re.compile(r"[^ \t\r\n\f]+").findall class NoStrokePath(Path): """A Path object that never has a stroke width. This class is used to create filled shapes from unclosed paths, where only the fill should be rendered and the stroke should be ignored. """ def __init__(self, *args: Any, **kwargs: Any) -> None: copy_from = kwargs.pop("copy_from", None) super().__init__(*args, **kwargs) if copy_from: self.__dict__.update(copy.deepcopy(copy_from.__dict__)) def getProperties(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: """Return the properties of the path, ensuring no stroke is applied.""" props = super().getProperties(*args, **kwargs) if "strokeWidth" in props: props["strokeWidth"] = 0 if "strokeColor" in props: props["strokeColor"] = None return props class ClippingPath(Path): """A Path object used for defining a clipping region. This path will not be rendered with a fill or stroke but will be used as a clipping mask for other shapes. """ def __init__(self, *args: Any, **kwargs: Any) -> None: copy_from = kwargs.pop("copy_from", None) Path.__init__(self, *args, **kwargs) if copy_from: self.__dict__.update(copy.deepcopy(copy_from.__dict__)) self.isClipPath = 1 def getProperties(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: """Return the properties of the path, ensuring no fill or stroke.""" props = Path.getProperties(self, *args, **kwargs) if "fillColor" in props: props["fillColor"] = None if "strokeColor" in props: props["strokeColor"] = None return props class CSSMatcher(cssselect2.Matcher): """A CSS matcher to handle styles defined in SVG