Skip to content

Commit e63bb35

Browse files
committed
add embedded images
1 parent 3dfe7a4 commit e63bb35

6 files changed

Lines changed: 2043 additions & 140 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ typst_express(
4545

4646
The Typst color remains the fill color and the generated stroke is black.
4747

48+
4849
More python examples at:
4950
https://kolibril13.github.io/bpy-gallery/n4typst_examples/

tests/test_svg_images_blender52.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Blender 5.2 coverage for SVG raster images produced by Typst."""
2+
3+
from __future__ import annotations
4+
5+
import base64
6+
from pathlib import Path
7+
import struct
8+
import zlib
9+
10+
import bpy
11+
import pytest
12+
13+
from typst_importer.image_import import extract_svg_images
14+
from typst_importer.svg_preprocessing import SVG_NS, preprocess_svg
15+
from typst_importer.typst_to_svg import typst_to_blender_curves
16+
17+
18+
pytestmark = pytest.mark.skipif(
19+
bpy.app.version < (5, 2, 0),
20+
reason="These workflows require Blender 5.2 or newer",
21+
)
22+
23+
24+
def _png_bytes(width=1, height=1):
25+
def chunk(kind, data):
26+
checksum = zlib.crc32(kind + data) & 0xFFFFFFFF
27+
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", checksum)
28+
29+
row = b"\x00" + b"\xff\x40\x20\xff" * width
30+
return (
31+
b"\x89PNG\r\n\x1a\n"
32+
+ chunk("IHDR".encode(), struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0))
33+
+ chunk("IDAT".encode(), zlib.compress(row * height))
34+
+ chunk("IEND".encode(), b"")
35+
)
36+
37+
38+
def _data_uri(width=1, height=1):
39+
return "data:image/png;base64," + base64.b64encode(
40+
_png_bytes(width, height)
41+
).decode("ascii")
42+
43+
44+
def test_preprocessing_extracts_embedded_use_image_with_viewport_geometry():
45+
svg = f'''<svg xmlns="{SVG_NS}" width="100" height="100">
46+
<defs><symbol id="asset" viewBox="0 0 10 20"
47+
preserveAspectRatio="none">
48+
<image width="10" height="20" preserveAspectRatio="none"
49+
href="{_data_uri()}"/>
50+
</symbol></defs>
51+
<use href="#asset" x="4" y="5" width="40" height="60"/>
52+
</svg>'''
53+
54+
images, warnings = extract_svg_images(preprocess_svg(svg))
55+
56+
assert warnings == []
57+
assert len(images) == 1
58+
xs = [corner[0] for corner in images[0]["corners"]]
59+
ys = [corner[1] for corner in images[0]["corners"]]
60+
assert min(xs) == pytest.approx(4.0)
61+
assert max(xs) == pytest.approx(44.0)
62+
assert min(ys) == pytest.approx(5.0)
63+
assert max(ys) == pytest.approx(65.0)
64+
65+
66+
def test_external_images_are_contained_to_the_typst_source_folder(tmp_path: Path):
67+
source_dir = tmp_path / "source"
68+
source_dir.mkdir()
69+
outside_image = tmp_path / "outside.png"
70+
outside_image.write_bytes(_png_bytes())
71+
svg = f'''<svg xmlns="{SVG_NS}" width="10" height="10">
72+
<image width="10" height="10" href="../outside.png"/>
73+
</svg>'''
74+
75+
images, warnings = extract_svg_images(
76+
preprocess_svg(svg),
77+
svg_dir=source_dir,
78+
)
79+
80+
assert images == []
81+
assert any("outside the SVG directory" in warning for warning in warnings)
82+
83+
84+
def test_preprocessing_bounds_recursive_use_expansion():
85+
svg = f'''<svg xmlns="{SVG_NS}" width="10" height="10">
86+
<defs><g id="loop">
87+
<use href="#loop"/><use href="#loop"/><use href="#loop"/>
88+
</g></defs>
89+
<use href="#loop"/>
90+
</svg>'''
91+
92+
with pytest.raises(ValueError, match="expansion limit"):
93+
preprocess_svg(svg)
94+
95+
96+
def test_typst_image_becomes_a_packed_textured_plane(tmp_path: Path):
97+
image_file = tmp_path / "pixel.png"
98+
image_file.write_bytes(_png_bytes())
99+
typst_file = tmp_path / "image.typ"
100+
typst_file.write_text(
101+
"#set page(width: auto, height: auto, margin: 0pt, fill: none)\n"
102+
'#rect(width: 20pt, height: 20pt, fill: rgb("#336699"))\n'
103+
'#image("pixel.png", width: 20pt)\n',
104+
encoding="utf-8",
105+
)
106+
107+
collection = typst_to_blender_curves(
108+
typst_file,
109+
scale_factor=100.0,
110+
convert_to_mesh=False,
111+
)
112+
113+
planes = [obj for obj in collection.objects if obj.type == "MESH"]
114+
assert len(planes) == 1
115+
plane = planes[0]
116+
assert plane.get("typst_svg_image_object") is True
117+
curves = [obj for obj in collection.objects if obj.type == "CURVE"]
118+
assert len(curves) == 1
119+
assert plane.get("svg_paint_index") > curves[0].get("svg_paint_index")
120+
assert plane.dimensions.x == pytest.approx(curves[0].dimensions.x, abs=2e-6)
121+
assert plane.dimensions.y == pytest.approx(curves[0].dimensions.y, abs=2e-6)
122+
material = plane.data.materials[0]
123+
texture = next(
124+
node
125+
for node in material.node_tree.nodes
126+
if node.bl_idname == "ShaderNodeTexImage"
127+
)
128+
assert texture.image.packed_file
129+
assert not any(
130+
obj.name.startswith("__ESVG_IMG_") for obj in bpy.data.objects
131+
)
132+
assert not any(
133+
material.get("typst_svg_blender_material") and material.users == 0
134+
for material in bpy.data.materials
135+
)

0 commit comments

Comments
 (0)