-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsvg_library.py
More file actions
113 lines (85 loc) · 3.4 KB
/
svg_library.py
File metadata and controls
113 lines (85 loc) · 3.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import os
import re
from xml.etree import ElementTree as ET
import cairosvg
THUMBNAIL_SUFFIX = '-tn@2x.png'
THUMBNAIL_LONG_EDGE_PX = 480
SVG_LENGTH_UNIT_TO_PX = {
'': 1,
'px': 1,
'in': 96,
'cm': 96 / 2.54,
'mm': 96 / 25.4,
'pt': 96 / 72,
'pc': 16,
}
def normalize_relative_path(path):
"""Normalize a relative path for use in URLs and cross-platform comparisons."""
return path.replace(os.sep, '/')
def build_public_upload_url(relative_path):
"""Build the public static URL for an uploaded asset."""
return f"/static/uploads/{normalize_relative_path(relative_path)}"
def build_thumbnail_relative_path(relative_path):
"""Return the thumbnail path for a given uploaded SVG path."""
base_path, _ = os.path.splitext(relative_path)
return normalize_relative_path(f"{base_path}{THUMBNAIL_SUFFIX}")
def build_file_entry(relative_path):
"""Build the metadata payload used by the UI for one SVG file."""
normalized_path = normalize_relative_path(relative_path)
return {
'filename': normalized_path,
'svg_url': build_public_upload_url(normalized_path),
'thumbnail_url': build_public_upload_url(build_thumbnail_relative_path(normalized_path)),
}
def parse_svg_length_to_px(value):
"""Convert a raw SVG length string into pixels when the unit is supported."""
if not value:
return None
length_text = value.strip()
if not length_text or length_text.endswith('%'):
return None
match = re.fullmatch(r'([0-9]*\.?[0-9]+)([a-zA-Z]*)', length_text)
if not match:
return None
numeric_value = float(match.group(1))
unit = match.group(2).lower()
unit_scale = SVG_LENGTH_UNIT_TO_PX.get(unit)
if unit_scale is None:
return None
return numeric_value * unit_scale
def get_svg_dimensions_px(svg_path):
"""Determine the SVG canvas dimensions in pixels from viewBox or size attributes."""
try:
root = ET.parse(svg_path).getroot()
except (ET.ParseError, OSError) as error:
print(f"[WARN] Unable to parse SVG for thumbnail sizing: {svg_path} ({error})")
return None
view_box = root.attrib.get('viewBox')
if view_box:
values = [part for part in re.split(r'[\s,]+', view_box.strip()) if part]
if len(values) == 4:
try:
width = abs(float(values[2]))
height = abs(float(values[3]))
if width > 0 and height > 0:
return width, height
except ValueError:
pass
width = parse_svg_length_to_px(root.attrib.get('width'))
height = parse_svg_length_to_px(root.attrib.get('height'))
if width and height:
return width, height
return None
def generate_svg_thumbnail(svg_path, thumbnail_path):
"""Render a PNG thumbnail for an SVG, preserving portrait or landscape orientation."""
dimensions = get_svg_dimensions_px(svg_path)
output_kwargs = {'output_width': THUMBNAIL_LONG_EDGE_PX}
if dimensions:
width, height = dimensions
if height > width:
output_kwargs = {'output_height': THUMBNAIL_LONG_EDGE_PX}
os.makedirs(os.path.dirname(thumbnail_path), exist_ok=True)
cairosvg.svg2png(url=svg_path, write_to=thumbnail_path, **output_kwargs)
def generate_svg_pdf_bytes(svg_path):
"""Render an SVG file to PDF bytes for on-demand download responses."""
return cairosvg.svg2pdf(url=svg_path)