forked from reingart/pyfpdf
-
Notifications
You must be signed in to change notification settings - Fork 227
/
util.py
252 lines (206 loc) · 8.11 KB
/
util.py
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
"""
Various utilities that could not be gathered logically in a specific module.
The contents of this module are internal to fpdf2, and not part of the public API.
They may change at any time without prior warning or any deprecation period,
in non-backward-compatible ways.
"""
import gc, os, warnings
from numbers import Number
from tracemalloc import get_traced_memory, is_tracing
from typing import Iterable, Tuple, Union, NamedTuple
# default block size from src/libImaging/Storage.c:
PIL_MEM_BLOCK_SIZE_IN_MIB = 16
class Padding(NamedTuple):
top: Number = 0
right: Number = 0
bottom: Number = 0
left: Number = 0
@classmethod
def new(cls, padding: Union[int, float, tuple, list]):
"""Return a 4-tuple of padding values from a single value or a 2, 3 or 4-tuple according to CSS rules"""
if isinstance(padding, (int, float)):
return Padding(padding, padding, padding, padding)
if len(padding) == 2:
return Padding(padding[0], padding[1], padding[0], padding[1])
if len(padding) == 3:
return Padding(padding[0], padding[1], padding[2], padding[1])
if len(padding) == 4:
return Padding(*padding)
raise ValueError(
f"padding shall be a number or a sequence of 2, 3 or 4 numbers, got {str(padding)}"
)
def buffer_subst(buffer, placeholder, value):
buffer_size = len(buffer)
assert len(placeholder) == len(value), f"placeholder={placeholder} value={value}"
buffer = buffer.replace(placeholder.encode(), value.encode(), 1)
assert len(buffer) == buffer_size
return buffer
def escape_parens(s):
"""Add a backslash character before , ( and )"""
if isinstance(s, str):
return (
s.replace("\\", "\\\\")
.replace(")", "\\)")
.replace("(", "\\(")
.replace("\r", "\\r")
)
return (
s.replace(b"\\", b"\\\\")
.replace(b")", b"\\)")
.replace(b"(", b"\\(")
.replace(b"\r", b"\\r")
)
def get_scale_factor(unit: Union[str, Number]) -> float:
"""
Get how many pts are in a unit. (k)
Args:
unit (str, float, int): Any of "pt", "mm", "cm", "in", or a number.
Returns:
float: The number of points in that unit (assuming 72dpi)
Raises:
ValueError
"""
if isinstance(unit, Number):
return float(unit)
if unit == "pt":
return 1
if unit == "mm":
return 72 / 25.4
if unit == "cm":
return 72 / 2.54
if unit == "in":
return 72.0
raise ValueError(f"Incorrect unit: {unit}")
def convert_unit(
to_convert: Union[float, int, Iterable[Union[float, int, Iterable]]],
old_unit: Union[str, Number],
new_unit: Union[str, Number],
) -> Union[float, tuple]:
"""
Convert a number or sequence of numbers from one unit to another.
If either unit is a number it will be treated as the number of points per unit. So 72 would mean 1 inch.
Args:
to_convert (float, int, Iterable): The number / list of numbers, or points, to convert
old_unit (str, float, int): A unit accepted by fpdf.FPDF or a number
new_unit (str, float, int): A unit accepted by fpdf.FPDF or a number
Returns:
(float, tuple): to_convert converted from old_unit to new_unit or a tuple of the same
"""
unit_conversion_factor = get_scale_factor(new_unit) / get_scale_factor(old_unit)
if isinstance(to_convert, Iterable):
return tuple(convert_unit(i, 1, unit_conversion_factor) for i in to_convert)
return to_convert / unit_conversion_factor
ROMAN_NUMERAL_MAP = (
("M", 1000),
("CM", 900),
("D", 500),
("CD", 400),
("C", 100),
("XC", 90),
("L", 50),
("XL", 40),
("X", 10),
("IX", 9),
("V", 5),
("IV", 4),
("I", 1),
)
def int2roman(n):
"Convert an integer to Roman numeral"
result = ""
for numeral, integer in ROMAN_NUMERAL_MAP:
while n >= integer:
result += numeral
n -= integer
return result
################################################################################
################### Utility functions to track memory usage ####################
################################################################################
def print_mem_usage(prefix):
print(get_mem_usage(prefix))
def get_mem_usage(prefix) -> str:
_collected_count = gc.collect()
rss = get_process_rss()
# heap_size, stack_size = get_process_heap_and_stack_sizes()
# objs_size_sum = get_gc_managed_objs_total_size()
pillow = get_pillow_allocated_memory()
# malloc_stats = "Malloc stats: " + get_pymalloc_allocated_over_total_size()
malloc_stats = ""
if is_tracing():
malloc_stats = "Malloc stats: " + get_tracemalloc_traced_memory()
return f"{prefix:<40} {malloc_stats} | Pillow: {pillow} | Process RSS: {rss}"
def get_process_rss() -> str:
rss_as_mib = get_process_rss_as_mib()
if rss_as_mib:
return f"{rss_as_mib:.1f} MiB"
return "<unavailable>"
def get_process_rss_as_mib() -> Union[Number, None]:
"Inspired by psutil source code"
pid = os.getpid()
try:
with open(f"/proc/{pid}/statm", encoding="utf8") as statm:
return (
int(statm.readline().split()[1])
* os.sysconf("SC_PAGE_SIZE")
/ 1024
/ 1024
)
except FileNotFoundError: # /proc files only exist under Linux
return None
def get_process_heap_and_stack_sizes() -> Tuple[str]:
heap_size_in_mib, stack_size_in_mib = "<unavailable>", "<unavailable>"
pid = os.getpid()
try:
with open(f"/proc/{pid}/maps", encoding="utf8") as maps_file:
maps_lines = list(maps_file)
except FileNotFoundError: # This file only exists under Linux
return heap_size_in_mib, stack_size_in_mib
for line in maps_lines:
words = line.split()
addr_range, path = words[0], words[-1]
addr_start, addr_end = addr_range.split("-")
addr_start, addr_end = int(addr_start, 16), int(addr_end, 16)
size = addr_end - addr_start
if path == "[heap]":
heap_size_in_mib = f"{size / 1024 / 1024:.1f} MiB"
elif path == "[stack]":
stack_size_in_mib = f"{size / 1024 / 1024:.1f} MiB"
return heap_size_in_mib, stack_size_in_mib
def get_pymalloc_allocated_over_total_size() -> Tuple[str]:
"""
Get PyMalloc stats from sys._debugmallocstats()
From experiments, not very reliable
"""
try:
# pylint: disable=import-outside-toplevel
from pymemtrace.debug_malloc_stats import get_debugmallocstats
allocated, total = -1, -1
for line in get_debugmallocstats().decode().splitlines():
if line.startswith("Total"):
total = int(line.split()[-1].replace(",", ""))
elif line.startswith("# bytes in allocated blocks"):
allocated = int(line.split()[-1].replace(",", ""))
return f"{allocated / 1024 / 1024:.1f} / {total / 1024 / 1024:.1f} MiB"
except ImportError:
warnings.warn("pymemtrace could not be imported - Run: pip install pymemtrace")
return "<unavailable>"
def get_gc_managed_objs_total_size() -> str:
"From experiments, not very reliable"
try:
# pylint: disable=import-outside-toplevel
from pympler.muppy import get_objects, getsizeof
objs_total_size = sum(getsizeof(obj) for obj in get_objects())
return f"{objs_total_size / 1024 / 1024:.1f} MiB"
except ImportError:
warnings.warn("pympler could not be imported - Run: pip install pympler")
return "<unavailable>"
def get_tracemalloc_traced_memory() -> str:
"Requires python -X tracemalloc"
current, peak = get_traced_memory()
return f"{current / 1024 / 1024:.1f} (peak={peak / 1024 / 1024:.1f}) MiB"
def get_pillow_allocated_memory() -> str:
# pylint: disable=c-extension-no-member,import-outside-toplevel
from PIL import Image
stats = Image.core.get_stats()
blocks_in_use = stats["allocated_blocks"] - stats["freed_blocks"]
return f"{blocks_in_use * PIL_MEM_BLOCK_SIZE_IN_MIB:.1f} MiB"