svgtext 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- svgtext-0.0.2/.gitignore +219 -0
- svgtext-0.0.2/PKG-INFO +10 -0
- svgtext-0.0.2/README.md +0 -0
- svgtext-0.0.2/pyproject.toml +24 -0
- svgtext-0.0.2/src/svgtext/__init__.py +0 -0
- svgtext-0.0.2/src/svgtext/fonts/Comfortaa-VariableFont_wght.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuMathTeXGyre.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSans-Bold.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSans-BoldOblique.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSans-ExtraLight.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSans-Oblique.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSans.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSansCondensed-Bold.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSansCondensed-BoldOblique.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSansCondensed-Oblique.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSansCondensed.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSansMono-Bold.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSansMono-BoldOblique.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSansMono-Oblique.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSansMono.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSerif-Bold.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSerif-BoldItalic.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSerif-Italic.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSerif.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSerifCondensed-Bold.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSerifCondensed-BoldItalic.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSerifCondensed-Italic.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/DejaVuSerifCondensed.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/Manrope-VariableFont_wght.ttf +0 -0
- svgtext-0.0.2/src/svgtext/fonts/__init__.py +0 -0
- svgtext-0.0.2/src/svgtext/htmltext.py +139 -0
- svgtext-0.0.2/src/svgtext/render.py +67 -0
- svgtext-0.0.2/src/svgtext/svgsystem.py +719 -0
- svgtext-0.0.2/src/svgtext/textlayout.py +336 -0
- svgtext-0.0.2/uv.lock +955 -0
svgtext-0.0.2/.gitignore
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
ide/
|
|
2
|
+
# Byte-compiled / optimized / DLL files
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[codz]
|
|
5
|
+
*$py.class
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Distribution / packaging
|
|
11
|
+
.Python
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py.cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
# Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# UV
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# uv.lock
|
|
103
|
+
|
|
104
|
+
# poetry
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
106
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
107
|
+
# commonly ignored for libraries.
|
|
108
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
109
|
+
# poetry.lock
|
|
110
|
+
# poetry.toml
|
|
111
|
+
|
|
112
|
+
# pdm
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
114
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
115
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
116
|
+
# pdm.lock
|
|
117
|
+
# pdm.toml
|
|
118
|
+
.pdm-python
|
|
119
|
+
.pdm-build/
|
|
120
|
+
|
|
121
|
+
# pixi
|
|
122
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
123
|
+
# pixi.lock
|
|
124
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
125
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
126
|
+
.pixi
|
|
127
|
+
|
|
128
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
129
|
+
__pypackages__/
|
|
130
|
+
|
|
131
|
+
# Celery stuff
|
|
132
|
+
celerybeat-schedule
|
|
133
|
+
celerybeat.pid
|
|
134
|
+
|
|
135
|
+
# Redis
|
|
136
|
+
*.rdb
|
|
137
|
+
*.aof
|
|
138
|
+
*.pid
|
|
139
|
+
|
|
140
|
+
# RabbitMQ
|
|
141
|
+
mnesia/
|
|
142
|
+
rabbitmq/
|
|
143
|
+
rabbitmq-data/
|
|
144
|
+
|
|
145
|
+
# ActiveMQ
|
|
146
|
+
activemq-data/
|
|
147
|
+
|
|
148
|
+
# SageMath parsed files
|
|
149
|
+
*.sage.py
|
|
150
|
+
|
|
151
|
+
# Environments
|
|
152
|
+
.env
|
|
153
|
+
.envrc
|
|
154
|
+
.venv
|
|
155
|
+
env/
|
|
156
|
+
venv/
|
|
157
|
+
ENV/
|
|
158
|
+
env.bak/
|
|
159
|
+
venv.bak/
|
|
160
|
+
|
|
161
|
+
# Spyder project settings
|
|
162
|
+
.spyderproject
|
|
163
|
+
.spyproject
|
|
164
|
+
|
|
165
|
+
# Rope project settings
|
|
166
|
+
.ropeproject
|
|
167
|
+
|
|
168
|
+
# mkdocs documentation
|
|
169
|
+
/site
|
|
170
|
+
|
|
171
|
+
# mypy
|
|
172
|
+
.mypy_cache/
|
|
173
|
+
.dmypy.json
|
|
174
|
+
dmypy.json
|
|
175
|
+
|
|
176
|
+
# Pyre type checker
|
|
177
|
+
.pyre/
|
|
178
|
+
|
|
179
|
+
# pytype static type analyzer
|
|
180
|
+
.pytype/
|
|
181
|
+
|
|
182
|
+
# Cython debug symbols
|
|
183
|
+
cython_debug/
|
|
184
|
+
|
|
185
|
+
# PyCharm
|
|
186
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
187
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
188
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
189
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
190
|
+
# .idea/
|
|
191
|
+
|
|
192
|
+
# Abstra
|
|
193
|
+
# Abstra is an AI-powered process automation framework.
|
|
194
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
195
|
+
# Learn more at https://abstra.io/docs
|
|
196
|
+
.abstra/
|
|
197
|
+
|
|
198
|
+
# Visual Studio Code
|
|
199
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
200
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
201
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
202
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
203
|
+
# .vscode/
|
|
204
|
+
# Temporary file for partial code execution
|
|
205
|
+
tempCodeRunnerFile.py
|
|
206
|
+
|
|
207
|
+
# Ruff stuff:
|
|
208
|
+
.ruff_cache/
|
|
209
|
+
|
|
210
|
+
# PyPI configuration file
|
|
211
|
+
.pypirc
|
|
212
|
+
|
|
213
|
+
# Marimo
|
|
214
|
+
marimo/_static/
|
|
215
|
+
marimo/_lsp/
|
|
216
|
+
__marimo__/
|
|
217
|
+
|
|
218
|
+
# Streamlit
|
|
219
|
+
.streamlit/secrets.toml
|
svgtext-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: svgtext
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: fonttools>=4.65.0
|
|
7
|
+
Requires-Dist: pandas>=3.0.6
|
|
8
|
+
Requires-Dist: pillow>=12.3.0
|
|
9
|
+
Requires-Dist: pytest-playwright>=0.10.0
|
|
10
|
+
Requires-Dist: tpipe
|
svgtext-0.0.2/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "svgtext"
|
|
7
|
+
version = "0.0.2"
|
|
8
|
+
description = "Add your description here"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fonttools>=4.65.0",
|
|
13
|
+
"pandas>=3.0.6",
|
|
14
|
+
"pillow>=12.3.0",
|
|
15
|
+
"pytest-playwright>=0.10.0",
|
|
16
|
+
"tpipe",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/svgtext"]
|
|
21
|
+
artifacts = ["src/svgtext/fonts/*"]
|
|
22
|
+
|
|
23
|
+
[tool.uv.sources]
|
|
24
|
+
tpipe = { url = "https://github.com/ThomasKimber/tpipe/releases/download/v0.001/tpipe-0.1.0-py3-none-any.whl" }
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from svgtext.textlayout import word_wrap
|
|
2
|
+
from tpipe import phtml
|
|
3
|
+
import networkx as nx
|
|
4
|
+
from networkx import MultiDiGraph
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def html_to_markup(source : str,
|
|
8
|
+
word_wrap_limit : int)->list[list[tuple[str,dict[str,dict]]]]:
|
|
9
|
+
"""Process a section of text, breaking it into
|
|
10
|
+
separate lines based on a number of
|
|
11
|
+
characters defined as a word_wrap_limit and return
|
|
12
|
+
a proprietary markup consisting of lines of text
|
|
13
|
+
broken down into individually styled cells.
|
|
14
|
+
Text can be supplied as straight text, or provided as html
|
|
15
|
+
"""
|
|
16
|
+
tgraph=html_text_to_graph(source)
|
|
17
|
+
if len(tgraph)==0:
|
|
18
|
+
return [[(" ",{}) ]]
|
|
19
|
+
return [[(text,_get_text_feature_from_graph_hierarchy(tgraph, node)) for text, node in row ] for row in _html_text_word_wrap(source, tgraph, word_wrap_limit) ]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _html_section_to_text_layout_grid(html_graph, start_node):
|
|
24
|
+
"""Given an html structural graph (conforming to the definition
|
|
25
|
+
in phtml) and a starting node, traverse the graph and extract
|
|
26
|
+
text nodes, placing them into a grid based on linebreaks and
|
|
27
|
+
reading sequence. This grid can then be used to further refine
|
|
28
|
+
layout."""
|
|
29
|
+
linenumber=-1
|
|
30
|
+
last_linenumber=0
|
|
31
|
+
block_sequence=-1
|
|
32
|
+
subtree=phtml.walk_subtree(html_graph,start_node)
|
|
33
|
+
node_line_assignments=[]
|
|
34
|
+
node_line=[]
|
|
35
|
+
node_rows=[]
|
|
36
|
+
for n in subtree:
|
|
37
|
+
node_content = phtml.get_node_data(html_graph, n)
|
|
38
|
+
node_parent_id = phtml.get_parent_node_id(html_graph, n)
|
|
39
|
+
if node_content.tag in ('p', 'br'):
|
|
40
|
+
linenumber=linenumber+1
|
|
41
|
+
if node_content.text is not None and node_content.node_type=='text':
|
|
42
|
+
block_sequence=block_sequence+1
|
|
43
|
+
node_line.append(n)
|
|
44
|
+
if linenumber != last_linenumber and len(node_line)>0:
|
|
45
|
+
block_sequence=-1
|
|
46
|
+
node_rows.append(node_line)
|
|
47
|
+
node_line=[]
|
|
48
|
+
node_line_assignments.append((n, (block_sequence, linenumber)))
|
|
49
|
+
|
|
50
|
+
last_linenumber=linenumber
|
|
51
|
+
if len(node_line)>0:
|
|
52
|
+
node_rows.append(node_line)
|
|
53
|
+
return node_rows
|
|
54
|
+
|
|
55
|
+
def range_mapping(t_range, labelled_range_dict):
|
|
56
|
+
mappings=[]
|
|
57
|
+
for k,v in labelled_range_dict.items():
|
|
58
|
+
if ranges_overlap(range(*t_range), range(*v)):
|
|
59
|
+
mappings.append(k)
|
|
60
|
+
return mappings
|
|
61
|
+
|
|
62
|
+
def ranges_overlap(r1, r2):
|
|
63
|
+
return max(r1.start, r2.start) < min(r1.stop, r2.stop)
|
|
64
|
+
|
|
65
|
+
def ranges_intersection(r1, r2):
|
|
66
|
+
r_intersect = set(r1).intersection(set(r2))
|
|
67
|
+
return range(min(r_intersect), max(r_intersect))
|
|
68
|
+
|
|
69
|
+
def range_to_start_end_tuple(r):
|
|
70
|
+
return r.start, r.stop+1
|
|
71
|
+
|
|
72
|
+
def html_text_to_graph(source: str) -> nx.MultiDiGraph:
|
|
73
|
+
return phtml.html_to_graph(source)
|
|
74
|
+
|
|
75
|
+
def _html_text_word_wrap(source : str,
|
|
76
|
+
tgraph : MultiDiGraph,
|
|
77
|
+
word_wrap_length : int):
|
|
78
|
+
|
|
79
|
+
root_el=phtml.root_node_id(tgraph)
|
|
80
|
+
node_line_assignments = _html_section_to_text_layout_grid(tgraph, root_el)
|
|
81
|
+
|
|
82
|
+
row_node_extents=[]
|
|
83
|
+
for row in node_line_assignments:
|
|
84
|
+
node_extents={}
|
|
85
|
+
row_len=0
|
|
86
|
+
for node in row:
|
|
87
|
+
n_text = phtml.get_node_data(tgraph, node).text
|
|
88
|
+
node_extents[node]=(row_len, row_len+len(n_text))
|
|
89
|
+
row_len=row_len+len(n_text)
|
|
90
|
+
row_node_extents.append(node_extents)
|
|
91
|
+
|
|
92
|
+
layout_text = ["".join([phtml.get_node_data(tgraph, t).text for t in row_nodes]) for row_nodes in node_line_assignments]
|
|
93
|
+
line_wws=[]
|
|
94
|
+
for line in layout_text:
|
|
95
|
+
wraps=word_wrap(line, word_wrap_length, " .,\n")
|
|
96
|
+
line_wws.append(wraps)
|
|
97
|
+
|
|
98
|
+
layout=[r for l in [
|
|
99
|
+
[
|
|
100
|
+
[
|
|
101
|
+
(e, m,range_to_start_end_tuple(ranges_intersection(range(*row_node_extents[e][m]),range(*c))))
|
|
102
|
+
for m in (range_mapping(c, row_node_extents[e]))
|
|
103
|
+
]
|
|
104
|
+
for c in r
|
|
105
|
+
]
|
|
106
|
+
for e,r in enumerate(line_wws)
|
|
107
|
+
]
|
|
108
|
+
for r in l]
|
|
109
|
+
|
|
110
|
+
return [[(layout_text[node_span[0]][slice(*node_span[2])], node_span[1])
|
|
111
|
+
for node_span in span]
|
|
112
|
+
for span in layout]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_text_feature_from_graph_hierarchy(graph, node):
|
|
116
|
+
features={}
|
|
117
|
+
ancestors = [a for a in phtml.get_ancestors(graph, node)]
|
|
118
|
+
tag_name_mapping={
|
|
119
|
+
"a" : "link",
|
|
120
|
+
"b" : "bold",
|
|
121
|
+
"i" : "italic",
|
|
122
|
+
"sup" : "super",
|
|
123
|
+
"sub" : "sub"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
tag__attribute_mapping={
|
|
127
|
+
"a" : {"href", "title", "target"},
|
|
128
|
+
"b" : {},
|
|
129
|
+
"i" : {},
|
|
130
|
+
"sup" : {},
|
|
131
|
+
"sub" : {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for a in ancestors:
|
|
135
|
+
a_node = graph.nodes(data=True)[a]
|
|
136
|
+
attribute_map = tag__attribute_mapping.get(a_node['data'].tag, None)
|
|
137
|
+
if attribute_map is not None :
|
|
138
|
+
features={**features, **{tag_name_mapping.get(a_node['data'].tag) : {v:a_node['data'].attributes.get(v) for v in attribute_map}}}
|
|
139
|
+
return features
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import Hashable
|
|
2
|
+
from playwright.async_api import async_playwright
|
|
3
|
+
import asyncio, base64
|
|
4
|
+
import re
|
|
5
|
+
import base64
|
|
6
|
+
|
|
7
|
+
def binary_data_to_base64_text(binary_data):
|
|
8
|
+
"""Convert binary data to text data in Base64 format."""
|
|
9
|
+
return base64.b64encode(binary_data).decode('utf-8').replace('\n', '')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def base64_text_to_data_url(base64_text, data_format):
|
|
13
|
+
"""Convert data in form of base64 text to a data URL with suitable prefix."""
|
|
14
|
+
# Argument processing
|
|
15
|
+
if data_format == 'jpg':
|
|
16
|
+
data_format = 'jpeg'
|
|
17
|
+
|
|
18
|
+
# Transformation
|
|
19
|
+
# - MIME type
|
|
20
|
+
if data_format in ['jpeg', 'png', 'gif', 'webp']:
|
|
21
|
+
mime_type = 'image/' + data_format
|
|
22
|
+
elif data_format == 'svg':
|
|
23
|
+
mime_type = 'image/svg+xml'
|
|
24
|
+
elif data_format == 'pdf':
|
|
25
|
+
mime_type = 'application/pdf'
|
|
26
|
+
elif data_format in ['eps', 'ps']:
|
|
27
|
+
mime_type = 'application/postscript'
|
|
28
|
+
else:
|
|
29
|
+
msg = 'Unknown data format. Can not assign a suitable MIME type.'
|
|
30
|
+
raise ValueError(msg)
|
|
31
|
+
# - Data URL
|
|
32
|
+
data_url = 'data:{};base64,{}'.format(mime_type, base64_text)
|
|
33
|
+
return data_url
|
|
34
|
+
|
|
35
|
+
async def batch_svg_to_png(svg_dict : dict)->dict:
|
|
36
|
+
|
|
37
|
+
async with async_playwright() as p:
|
|
38
|
+
browser = await p.chromium.launch()
|
|
39
|
+
sem = asyncio.Semaphore(8)
|
|
40
|
+
async def convert(key : Hashable, svg:str)->tuple[Hashable,bytes]:
|
|
41
|
+
async with sem:
|
|
42
|
+
png = await svg_to_png(browser, svg)
|
|
43
|
+
return key, png
|
|
44
|
+
|
|
45
|
+
results = await asyncio.gather(*(convert(k,s) for k,s in svg_dict.items()))
|
|
46
|
+
await browser.close()
|
|
47
|
+
|
|
48
|
+
return dict(results)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def svg_to_png(browser: async_playwright, svg_text: str, scale : int=2) -> bytes:
|
|
52
|
+
wm = re.search(r'width="([\d.]+)"', svg_text)
|
|
53
|
+
hm = re.search(r'height="([\d.]+)"', svg_text)
|
|
54
|
+
w = int(float(wm.group(1)))
|
|
55
|
+
h = int(float(hm.group(1)))
|
|
56
|
+
|
|
57
|
+
page = await browser.new_page(viewport={"width": w, "height": h},
|
|
58
|
+
device_scale_factor=scale)
|
|
59
|
+
try:
|
|
60
|
+
await page.set_content(
|
|
61
|
+
f'<body style="margin:0">{svg_text}</body>',
|
|
62
|
+
wait_until="load"
|
|
63
|
+
)
|
|
64
|
+
await page.evaluate("document.fonts.ready")
|
|
65
|
+
return await page.locator("svg").screenshot(type="png")
|
|
66
|
+
finally:
|
|
67
|
+
await page.close()
|