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 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
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()