svgtext 0.0.2__py3-none-any.whl
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/__init__.py +0 -0
- svgtext/fonts/Comfortaa-VariableFont_wght.ttf +0 -0
- svgtext/fonts/DejaVuMathTeXGyre.ttf +0 -0
- svgtext/fonts/DejaVuSans-Bold.ttf +0 -0
- svgtext/fonts/DejaVuSans-BoldOblique.ttf +0 -0
- svgtext/fonts/DejaVuSans-ExtraLight.ttf +0 -0
- svgtext/fonts/DejaVuSans-Oblique.ttf +0 -0
- svgtext/fonts/DejaVuSans.ttf +0 -0
- svgtext/fonts/DejaVuSansCondensed-Bold.ttf +0 -0
- svgtext/fonts/DejaVuSansCondensed-BoldOblique.ttf +0 -0
- svgtext/fonts/DejaVuSansCondensed-Oblique.ttf +0 -0
- svgtext/fonts/DejaVuSansCondensed.ttf +0 -0
- svgtext/fonts/DejaVuSansMono-Bold.ttf +0 -0
- svgtext/fonts/DejaVuSansMono-BoldOblique.ttf +0 -0
- svgtext/fonts/DejaVuSansMono-Oblique.ttf +0 -0
- svgtext/fonts/DejaVuSansMono.ttf +0 -0
- svgtext/fonts/DejaVuSerif-Bold.ttf +0 -0
- svgtext/fonts/DejaVuSerif-BoldItalic.ttf +0 -0
- svgtext/fonts/DejaVuSerif-Italic.ttf +0 -0
- svgtext/fonts/DejaVuSerif.ttf +0 -0
- svgtext/fonts/DejaVuSerifCondensed-Bold.ttf +0 -0
- svgtext/fonts/DejaVuSerifCondensed-BoldItalic.ttf +0 -0
- svgtext/fonts/DejaVuSerifCondensed-Italic.ttf +0 -0
- svgtext/fonts/DejaVuSerifCondensed.ttf +0 -0
- svgtext/fonts/Manrope-VariableFont_wght.ttf +0 -0
- svgtext/fonts/__init__.py +0 -0
- svgtext/htmltext.py +139 -0
- svgtext/render.py +67 -0
- svgtext/svgsystem.py +719 -0
- svgtext/textlayout.py +336 -0
- svgtext-0.0.2.dist-info/METADATA +10 -0
- svgtext-0.0.2.dist-info/RECORD +33 -0
- svgtext-0.0.2.dist-info/WHEEL +4 -0
svgtext/__init__.py
ADDED
|
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
|
svgtext/htmltext.py
ADDED
|
@@ -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
|
svgtext/render.py
ADDED
|
@@ -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()
|