Version: 1.7.1 (bundled) · Type: Pure Python + cairocffi
SPM product: none (by design — see Why there's no SPM product)
Python packages: cairosvg, cairocffi, cffi, cssselect2, tinycss2, webencodings, defusedxml, pillow
CairoSVG converts SVG documents to PNG, PDF, or PostScript. Unlike
manim (which uses pycairo — a statically-linked C extension, fully
self-contained), CairoSVG renders through cairocffi, which
dlopens the shared libcairo.dylib at runtime. That one
difference is the entire reason CairoSVG needs extra setup on iOS while
manim does not.
TL;DR
- Inside CodeBench:
import cairosvgworks out of the box — the app bundleslibcairo.dyliband installs afind_libraryshim.- In your own SwiftPM app: CairoSVG is not a
.product(...)you can just link. You must (1) embedlibcairo.dylibin your app and (2) install thefind_libraryshim shown below. Follow Using CairoSVG in your own app.
Every other bundled library in python-ios-lib ships as a self-contained
SwiftPM product. CairoSVG can't, because of how cairocffi loads
cairo:
# cairocffi/__init__.py
cairo = dlopen(
ffi, ('cairo-2', 'cairo', 'libcairo-2'),
('libcairo.so.2', 'libcairo.2.dylib', 'libcairo-2.dll'))- It calls
ctypes.util.find_library('cairo'). On iOS there is no browsable/usr/liband nootoolinPATH, so the stockfind_libraryreturnsNonefor everything. - It then
dlopens a sharedlibcairoby bare name. The Python C extension that performs thedlopen(_cffi_backend...so) has noLC_RPATH, so an@rpath-embedded dylib (e.g. one delivered by a SwiftPMbinaryTarget) is not resolvable from it.
The only thing that works is an absolute-path dlopen, which means
something must (a) place a real libcairo.dylib inside the app and
(b) teach find_library('cairo') to return its absolute path. Both are
app-bundle-layout-specific and live outside what Package.swift can
express — so a Cairosvg product would raise OSError at
import cairosvg for any consumer. We don't ship broken products.
Contrast with manim: its pycairo (cairo/_cairo...so) statically
links libcairo + libpixman + libfreetype + libfribidi, so import cairo
needs no external dylib at all. That's why full manim works as a clean
SPM product and CairoSVG does not.
CodeBench solves both halves automatically:
- The dylib — an Xcode Run Script build phase copies
Frameworks/cairo/libcairo.dylib→<App>.app/Frameworks/libcairo.dylib, re-IDs it to@rpath/libcairo.dylib, and code-signs it. - The shim —
app_packages/site-packages/sitecustomize.pyinstalls actypes.util.find_libraryoverride that maps the cairo aliases to the bundled dylib's absolute path. Python runssitecustomizeat interpreter startup, so the shim is active before anyimport cairosvg.
# sitecustomize.py (excerpt)
def _install_find_library_shim() -> None:
import os, ctypes.util as _cu
here = os.path.dirname(os.path.abspath(__file__))
fw_dir = os.path.normpath(os.path.join(here, "..", "..", "Frameworks"))
cairo_candidates = ("libcairo.dylib", "libcairo.framework/libcairo")
aliases = {n: cairo_candidates for n in
("cairo", "cairo-2", "libcairo-2",
"libcairo.so.2", "libcairo.2.dylib")}
_orig = _cu.find_library
def _find_library(name):
for rel in aliases.get(name, ()):
path = os.path.join(fw_dir, rel)
if os.path.isfile(path):
return path
try: return _orig(name)
except Exception: return None
_cu.find_library = _find_librarySo in CodeBench:
import cairosvg
cairosvg.svg2png(url="diagram.svg", write_to="diagram.png", scale=2.0)
cairosvg.svg2pdf(bytestring=svg_text.encode(), write_to="out.pdf")If you're building a SwiftPM app on top of python-ios-lib and want
CairoSVG, replicate the two pieces CodeBench provides. The pure-Python
dependencies are already linkable as SPM products; only the native
dylib and the shim are manual.
These are all self-contained SPM products in this package:
// Package.swift (your app)
dependencies: [
.package(url: "https://github.com/yu314-coder/python-ios-lib", branch: "main"),
],
targets: [
.target(name: "YourApp", dependencies: [
.product(name: "Cffi", package: "python-ios-lib"), // cairocffi backend
.product(name: "Cssselect2", package: "python-ios-lib"), // pulls Tinycss2
.product(name: "Defusedxml", package: "python-ios-lib"),
.product(name: "Pillow", package: "python-ios-lib"),
]),
]Then add the cairosvg/ and cairocffi/ package directories to your
app's site-packages (they're pure-Python — copy them from this repo's
app_packages/site-packages/{cairosvg,cairocffi}, or pip install
them into your bundle).
There is intentionally no
Cairosvg/CairocffiSPM product — see Why there's no SPM product. Ship those two pure-Python dirs as resources yourself.
Add a Run Script build phase (after "Embed Frameworks") that copies the dylib this repo already ships and re-IDs + signs it:
# Copy the prebuilt iOS arm64 libcairo (statically links pixman/freetype/
# fribidi) into the app's Frameworks/.
CAIRO_SRC="$SRCROOT/path/to/python-ios-lib/Frameworks/cairo/libcairo.dylib"
CAIRO_DST="$TARGET_BUILD_DIR/$WRAPPER_NAME/Frameworks/libcairo.dylib"
if [ -f "$CAIRO_SRC" ]; then
cp "$CAIRO_SRC" "$CAIRO_DST"
install_name_tool -id "@rpath/libcairo.dylib" "$CAIRO_DST" 2>/dev/null || true
codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$CAIRO_DST" || true
filibcairo.dylib is iOS arm64, minos 17.0, install-name
@rpath/libcairo.dylib, ~6 MB. (No simulator slice — build on a real
device, matching the pending iPhone-Simulator stub work.)
Ship a sitecustomize.py (or usercustomize.py) on sys.path in your
app that points the cairo aliases at wherever your app bundles the
dylib. The CodeBench version assumes
<site-packages>/../../Frameworks/libcairo.dylib; adjust fw_dir to
match your bundle layout. Copy the _install_find_library_shim()
function from
the excerpt above verbatim and call it
at startup.
If you can't run sitecustomize, do the equivalent inline before the
first import cairosvg:
import os, ctypes, ctypes.util
_libcairo = os.path.join(os.environ["HOME"], "..",
"<YourApp>.app", "Frameworks", "libcairo.dylib")
ctypes.CDLL(_libcairo, mode=ctypes.RTLD_GLOBAL) # preload
_orig = ctypes.util.find_library
ctypes.util.find_library = lambda n: _libcairo if "cairo" in n else _orig(n)
import cairosvg # now resolvesimport cairosvg
png = cairosvg.svg2png(bytestring=b'<svg xmlns="http://www.w3.org/2000/svg" '
b'width="16" height="16"><rect width="16" '
b'height="16" fill="red"/></svg>')
assert png[:8] == b"\x89PNG\r\n\x1a\n", "cairo didn't render"
print("CairoSVG OK:", len(png), "bytes")| Symptom | Cause | Fix |
|---|---|---|
OSError: cannot load library 'cairo' at import cairosvg |
find_library('cairo') returned None and dlopen found no libcairo.2.dylib |
Step 3 shim not active / wrong fw_dir path |
dlopen(...): image not found |
libcairo.dylib not in <App>.app/Frameworks/ |
Step 2 build phase didn't run, or wrong CAIRO_SRC |
| Renders blank / wrong fonts | font discovery, not cairo | cairo statically links freetype/fribidi; supply fonts via fontconfig or embed in the SVG |
| Works in CodeBench, fails in your app | you inherited the shim's hard-coded ../../Frameworks path |
recompute fw_dir for your bundle layout (Step 3) |
- Rendering math / animations? Use manim — it needs none of this (statically-linked pycairo). See manim.md.
- Just rasterising an SVG to PNG once? If you already depend on
manim, you can load the SVG as an
SVGMobjectand export a frame, or use Pillow + a simpler path, avoiding the cairo-shared-lib dance entirely. - Need CairoSVG's exact SVG feature coverage (CSS, filters, gradients)? Then follow the 4 steps above.