picosvg

repository·main·Indexed 42 days ago

https://github.com/googlefonts/picosvg

A Python-based utility for simplifying and normalizing SVG files into a predictable 'pico' format. It expands shorthand commands, materializes <use> references, enforces a strict element hierarchy, and converts coordinates to absolute values. The tool includes a CLI for processing SVGs and a library for performing boolean operations on paths, affine transformations via Affine2D, and SVG path manipulation using skia-pathops.

Tokens
5.4K
Snippets
21
Records
23
Agent score
52%

What's inside picosvg

  1. What is picosvg and how does it simplify SVGs?

    main

    picosvg is a tool that converts standard SVG files into a simplified "pico" SVG format. The transformation enforces a strict structure to ensure predictability and compatibility:

    • Structure: Exactly one <defs> element as the first child of the root, containing only gradients.
    • Elements: After the <defs>, only <g> and <path> elements are allowed. <g> elements are removed unless they are required for opacity.
    • Coordinates: All coordinates are converted to absolute values.
    • Commands: SVG path commands are expanded to use full coordinates only (no shorthand like H, S, etc.).
    • Materialization: <use> references are materialized, and clip paths/strokes are rendered into equivalent paths using Skia via skia-pathops.

    Unsupported Features:

    • <filter>
    • ``
  2. Set up the development environment and run tests

    main

    To contribute or run tests, install the project with the [dev] extra dependencies.

    Note for zsh users: If you encounter zsh: no matches found: .[dev], wrap the installation target in quotes: pip install -e '.[dev]'.

    pip install -e .[dev]
    pytest
  3. Run specific pytest tests

    main

    You can use pytest to run specific test files or individual test functions within those files.

    To run a specific test file: pytest tests/svg_test.py

    To run a specific test function: pytest tests/svg_test.py::test_topicosvg

    To see detailed diff information during a test run, use the --vv flag.

    # Test a specific file
    pytest tests/svg_test.py
    
    # Test a specific function
    pytest tests/svg_test.py::test_topicosvg
    
    # Test a specific function with detailed diffs
    pytest tests/svg_test.py::test_topicosvg --vv
  4. Use SVGShape for common SVG attributes

    main

    The SVGShape class serves as a base for all SVG shapes, providing standard attributes such as fill, stroke, opacity, transform, and display. It includes utility methods for managing these attributes:

    • apply_style_attribute(inplace=False): Converts inlined CSS in the style attribute into individual SVG attributes.
    • round_floats(ndigits, inplace=False): Rounds all float values in the shape to a specific number of decimal digits.
    • round_multiple(multiple_of, inplace=False): Rounds all float values to the nearest multiple of the provided value.
    • normalize_opacity(inplace=False): Merges fill-opacity or stroke-opacity into the generic opacity attribute where possible to simplify the shape.
    # Example of applying styles and rounding
    shape = SVGRect(x=10.123, y=10.123, width=50, height=50, style="fill: red; opacity: 0.5;")
    shape = shape.apply_style_attribute()
    shape = shape.round_floats(ndigits=2)
  5. Perform boolean operations on SVG paths

    main

    You can perform boolean operations (union, intersection, and difference) on sequences of SVG commands. These functions take a sequence of SVGCommandSeq (lists of (command, args) tuples) and a corresponding sequence of fill_rule strings (either 'nonzero' or 'evenodd'). The result is an SVGCommandGen generator that yields new SVG commands.

    Supported operations:

    • union(...)
    • intersection(...)
    • difference(...)
    from picosvg.svg_pathops import union
    
    # Example: union of two paths
    # path1_cmds and path2_cmds are SVGCommandSeq
    # fill_rules is a list of strings like ['nonzero', 'nonzero']
    result_gen = union([path1_cmds, path2_cmds], ['nonzero', 'nonzero'])
    new_cmds = list(result_gen)
  6. Perform Boolean operations on shapes

    main

    You can perform Boolean operations (union, intersection, difference) on a collection of SVGShape objects. These operations return a generator of SVGCommand sequences which can be used to create a new SVGPath.

    • union(shapes): Returns the union of all shapes.
    • intersection(shapes, fill_rules=None): Returns the intersection of all shapes. If fill_rules is not provided, it defaults to the clip_rule of each shape.
    • difference(shapes): Returns the difference of the shapes (subtracting subsequent shapes from the first).
    from picosvg.svg_types import union, SVGPath
    
    shape1 = SVGCircle(cx=50, cy=50, r=30)
    shape2 = SVGRect(x=20, y=20, width=60, height=60)
    
    # Perform union
    union_cmds = union([shape1, shape2])
    union_path = SVGPath.from_commands(union_cmds)
  7. Convert basic shapes to SVGPath

    main

    Several basic SVG shape classes can be converted into an SVGPath object using the as_path() method. This is useful for performing path operations on simple shapes.

    Supported shapes:

    • SVGCircle $\rightarrow$ SVGEllipse path
    • SVGEllipse $\rightarrow$ SVGPath (using two arcs)
    • SVGLine $\rightarrow$ SVGPath (M + L)
    • SVGPolygon $\rightarrow$ SVGPath (M + points + Z)
    • SVGPolyline $\rightarrow$ SVGPath (M + points)
    • SVGRect $\rightarrow$ SVGPath (M + H + A + V + A + H + A + V + A + end)
    rect = SVGRect(x=0, y=0, width=100, height=100, rx=10)
    path = rect.as_path()
  8. Use Affine2D for 2D transformations

    main

    The Affine2D class provides tools for performing 2D affine transformations, such as translation, scaling, rotation, and skewing. It can be represented as a vector of 6 values or a matrix:

    | a c e | | b d f | | 0 0 1 |

    Common operations include:

    • Identity: Affine2D.identity() returns a no-op transformation.
    • Matrix Multiplication: Use the @ operator to compose transformations. self @ other applies other before self (mapping by other then self).
    • Parsing: Affine2D.fromstring(raw_transform) parses an SVG transform string (e.g., "translate(10, 20) rotate(45)").
    • Mapping: Use map_point(pt) to transform a coordinate (x, y) or map_vector(vec) to transform a direction vector (ignoring translation).
    from picosvg.svg_transform import Affine2D
    
    # Create a transformation from an SVG string
    transform = Affine2D.fromstring("translate(10, 10) scale(2, 2)")
    
    # Apply to a point
    new_point = transform.map_point((0, 0))  # Point(10.0, 10.0)
    
    # Compose transformations using @
    combined = transform @ Affine2D.identity().rotate(1.57)  # Rotate by ~90 degrees
  9. Perform common affine transformations

    main

    The Affine2D class supports several standard SVG transformations. Note that angles for rotation and skewing must be provided in radians.

    • translate(tx, ty=0): Moves the coordinate system by tx and ty.
    • scale(sx, sy=None): Scales by sx and sy. If sy is omitted, it scales uniformly.
    • rotate(a, cx=0.0, cy=0.0): Rotates by a radians around the point (cx, cy).
    • skewx(a): Skews along the X axis by a radians.
    • skewy(a): Skews along the Y axis by a radians.
    • skew(xAngle, yAngle): Skews along both axes using radians.
    from picosvg.svg_transform import Affine2D
    
    trans = Affine2D.identity().translate(50, 50)
    scaled = trans.scale(2, 2)
    rotated = scaled.rotate(1.0)  # 1.0 radian
    skewed = rotated.skewx(0.5)   # 0.5 radians
  10. Map a source Rect to a destination Rect

    main

    The Affine2D.rect_to_rect(src, dst, preserveAspectRatio) method calculates the transformation required to map a source rectangle (src) to a destination rectangle (dst). This is useful for implementing SVG viewBox logic.

    preserveAspectRatio options:

    • "none": The mapping fills the destination completely, ignoring aspect ratio.
    • "[align] meet": Scales the source to fit within the destination while preserving aspect ratio (the source will be fully visible).
    • "[align] slice": Scales the source to cover the entire destination while preserving aspect ratio (parts of the source may be clipped).

    Alignment values ([align]): none, xminymin, xminymid, xminymax, xmidymin, xmidymid, xmidymax, xmaxymin, xmaxymid, xmaxymax.

    from picosvg.geometric_types import Rect
    from picosvg.svg_transform import Affine2D
    
    src_rect = Rect(0, 0, 100, 100)
    dst_rect = Rect(0, 0, 200, 200)
    
    # Scale to fit while preserving aspect ratio (centered)
    transform = Affine2D.rect_to_rect(src_rect, dst_rect, "xmidymid meet")
  11. Invert an Affine2D transformation

    main

    To reverse a transformation, use the inverse() method.

    • If the transformation is the identity, it returns itself.
    • If the transformation is degenerate (determinant is 0), it returns Affine2D.degenerate().
    • Otherwise, it returns the mathematical inverse.
    from picosvg.svg_transform import Affine2D
    
    trans = Affine2D.identity().translate(10, 20).scale(2, 2)
    inverse_trans = trans.inverse()
    
    # Applying the original then the inverse should return the identity
    identity_check = trans @ inverse_trans