Spaces:
Running
Running
"""Miscellaneous Routines.""" | |
import io | |
import pathlib | |
import string | |
import struct | |
from html import escape | |
from typing import ( | |
TYPE_CHECKING, | |
Any, | |
BinaryIO, | |
Callable, | |
Dict, | |
Generic, | |
Iterable, | |
Iterator, | |
List, | |
Optional, | |
Set, | |
TextIO, | |
Tuple, | |
TypeVar, | |
Union, | |
cast, | |
) | |
from pdf2zh.pdfexceptions import PDFTypeError, PDFValueError | |
if TYPE_CHECKING: | |
from pdf2zh.layout import LTComponent | |
import charset_normalizer # For str encoding detection | |
# from sys import maxint as INF doesn't work anymore under Python3, but PDF | |
# still uses 32 bits ints | |
INF = (1 << 31) - 1 | |
FileOrName = Union[pathlib.PurePath, str, io.IOBase] | |
AnyIO = Union[TextIO, BinaryIO] | |
class open_filename: | |
"""Context manager that allows opening a filename | |
(str or pathlib.PurePath type is supported) and closes it on exit, | |
(just like `open`), but does nothing for file-like objects. | |
""" | |
def __init__(self, filename: FileOrName, *args: Any, **kwargs: Any) -> None: | |
if isinstance(filename, pathlib.PurePath): | |
filename = str(filename) | |
if isinstance(filename, str): | |
self.file_handler: AnyIO = open(filename, *args, **kwargs) | |
self.closing = True | |
elif isinstance(filename, io.IOBase): | |
self.file_handler = cast(AnyIO, filename) | |
self.closing = False | |
else: | |
raise PDFTypeError("Unsupported input type: %s" % type(filename)) | |
def __enter__(self) -> AnyIO: | |
return self.file_handler | |
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: | |
if self.closing: | |
self.file_handler.close() | |
def make_compat_bytes(in_str: str) -> bytes: | |
"""Converts to bytes, encoding to unicode.""" | |
assert isinstance(in_str, str), str(type(in_str)) | |
return in_str.encode() | |
def make_compat_str(o: object) -> str: | |
"""Converts everything to string, if bytes guessing the encoding.""" | |
if isinstance(o, bytes): | |
enc = charset_normalizer.detect(o) | |
try: | |
return o.decode(enc["encoding"]) | |
except UnicodeDecodeError: | |
return str(o) | |
else: | |
return str(o) | |
def shorten_str(s: str, size: int) -> str: | |
if size < 7: | |
return s[:size] | |
if len(s) > size: | |
length = (size - 5) // 2 | |
return f"{s[:length]} ... {s[-length:]}" | |
else: | |
return s | |
def compatible_encode_method( | |
bytesorstring: Union[bytes, str], | |
encoding: str = "utf-8", | |
erraction: str = "ignore", | |
) -> str: | |
"""When Py2 str.encode is called, it often means bytes.encode in Py3. | |
This does either. | |
""" | |
if isinstance(bytesorstring, str): | |
return bytesorstring | |
assert isinstance(bytesorstring, bytes), str(type(bytesorstring)) | |
return bytesorstring.decode(encoding, erraction) | |
def paeth_predictor(left: int, above: int, upper_left: int) -> int: | |
# From http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html | |
# Initial estimate | |
p = left + above - upper_left | |
# Distances to a,b,c | |
pa = abs(p - left) | |
pb = abs(p - above) | |
pc = abs(p - upper_left) | |
# Return nearest of a,b,c breaking ties in order a,b,c | |
if pa <= pb and pa <= pc: | |
return left | |
elif pb <= pc: | |
return above | |
else: | |
return upper_left | |
def apply_png_predictor( | |
pred: int, | |
colors: int, | |
columns: int, | |
bitspercomponent: int, | |
data: bytes, | |
) -> bytes: | |
"""Reverse the effect of the PNG predictor | |
Documentation: http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html | |
""" | |
if bitspercomponent not in [8, 1]: | |
msg = "Unsupported `bitspercomponent': %d" % bitspercomponent | |
raise PDFValueError(msg) | |
nbytes = colors * columns * bitspercomponent // 8 | |
bpp = colors * bitspercomponent // 8 # number of bytes per complete pixel | |
buf = [] | |
line_above = list(b"\x00" * columns) | |
for scanline_i in range(0, len(data), nbytes + 1): | |
filter_type = data[scanline_i] | |
line_encoded = data[scanline_i + 1 : scanline_i + 1 + nbytes] | |
raw = [] | |
if filter_type == 0: | |
# Filter type 0: None | |
raw = list(line_encoded) | |
elif filter_type == 1: | |
# Filter type 1: Sub | |
# To reverse the effect of the Sub() filter after decompression, | |
# output the following value: | |
# Raw(x) = Sub(x) + Raw(x - bpp) | |
# (computed mod 256), where Raw() refers to the bytes already | |
# decoded. | |
for j, sub_x in enumerate(line_encoded): | |
if j - bpp < 0: | |
raw_x_bpp = 0 | |
else: | |
raw_x_bpp = int(raw[j - bpp]) | |
raw_x = (sub_x + raw_x_bpp) & 255 | |
raw.append(raw_x) | |
elif filter_type == 2: | |
# Filter type 2: Up | |
# To reverse the effect of the Up() filter after decompression, | |
# output the following value: | |
# Raw(x) = Up(x) + Prior(x) | |
# (computed mod 256), where Prior() refers to the decoded bytes of | |
# the prior scanline. | |
for up_x, prior_x in zip(line_encoded, line_above): | |
raw_x = (up_x + prior_x) & 255 | |
raw.append(raw_x) | |
elif filter_type == 3: | |
# Filter type 3: Average | |
# To reverse the effect of the Average() filter after | |
# decompression, output the following value: | |
# Raw(x) = Average(x) + floor((Raw(x-bpp)+Prior(x))/2) | |
# where the result is computed mod 256, but the prediction is | |
# calculated in the same way as for encoding. Raw() refers to the | |
# bytes already decoded, and Prior() refers to the decoded bytes of | |
# the prior scanline. | |
for j, average_x in enumerate(line_encoded): | |
if j - bpp < 0: | |
raw_x_bpp = 0 | |
else: | |
raw_x_bpp = int(raw[j - bpp]) | |
prior_x = int(line_above[j]) | |
raw_x = (average_x + (raw_x_bpp + prior_x) // 2) & 255 | |
raw.append(raw_x) | |
elif filter_type == 4: | |
# Filter type 4: Paeth | |
# To reverse the effect of the Paeth() filter after decompression, | |
# output the following value: | |
# Raw(x) = Paeth(x) | |
# + PaethPredictor(Raw(x-bpp), Prior(x), Prior(x-bpp)) | |
# (computed mod 256), where Raw() and Prior() refer to bytes | |
# already decoded. Exactly the same PaethPredictor() function is | |
# used by both encoder and decoder. | |
for j, paeth_x in enumerate(line_encoded): | |
if j - bpp < 0: | |
raw_x_bpp = 0 | |
prior_x_bpp = 0 | |
else: | |
raw_x_bpp = int(raw[j - bpp]) | |
prior_x_bpp = int(line_above[j - bpp]) | |
prior_x = int(line_above[j]) | |
paeth = paeth_predictor(raw_x_bpp, prior_x, prior_x_bpp) | |
raw_x = (paeth_x + paeth) & 255 | |
raw.append(raw_x) | |
else: | |
raise PDFValueError("Unsupported predictor value: %d" % filter_type) | |
buf.extend(raw) | |
line_above = raw | |
return bytes(buf) | |
Point = Tuple[float, float] | |
Rect = Tuple[float, float, float, float] | |
Matrix = Tuple[float, float, float, float, float, float] | |
PathSegment = Union[ | |
Tuple[str], # Literal['h'] | |
Tuple[str, float, float], # Literal['m', 'l'] | |
Tuple[str, float, float, float, float], # Literal['v', 'y'] | |
Tuple[str, float, float, float, float, float, float], | |
] # Literal['c'] | |
# Matrix operations | |
MATRIX_IDENTITY: Matrix = (1, 0, 0, 1, 0, 0) | |
def parse_rect(o: Any) -> Rect: | |
try: | |
(x0, y0, x1, y1) = o | |
return float(x0), float(y0), float(x1), float(y1) | |
except ValueError: | |
raise PDFValueError("Could not parse rectangle") | |
def mult_matrix(m1: Matrix, m0: Matrix) -> Matrix: | |
(a1, b1, c1, d1, e1, f1) = m1 | |
(a0, b0, c0, d0, e0, f0) = m0 | |
"""Returns the multiplication of two matrices.""" | |
return ( | |
a0 * a1 + c0 * b1, | |
b0 * a1 + d0 * b1, | |
a0 * c1 + c0 * d1, | |
b0 * c1 + d0 * d1, | |
a0 * e1 + c0 * f1 + e0, | |
b0 * e1 + d0 * f1 + f0, | |
) | |
def translate_matrix(m: Matrix, v: Point) -> Matrix: | |
"""Translates a matrix by (x, y).""" | |
(a, b, c, d, e, f) = m | |
(x, y) = v | |
return a, b, c, d, x * a + y * c + e, x * b + y * d + f | |
def apply_matrix_pt(m: Matrix, v: Point) -> Point: | |
(a, b, c, d, e, f) = m | |
(x, y) = v | |
"""Applies a matrix to a point.""" | |
return a * x + c * y + e, b * x + d * y + f | |
def apply_matrix_norm(m: Matrix, v: Point) -> Point: | |
"""Equivalent to apply_matrix_pt(M, (p,q)) - apply_matrix_pt(M, (0,0))""" | |
(a, b, c, d, e, f) = m | |
(p, q) = v | |
return a * p + c * q, b * p + d * q | |
def matrix_scale(m: Matrix) -> float: | |
(a, b, c, d, e, f) = m | |
return (a**2 + c**2) ** 0.5 | |
# Utility functions | |
def isnumber(x: object) -> bool: | |
return isinstance(x, (int, float)) | |
_T = TypeVar("_T") | |
def uniq(objs: Iterable[_T]) -> Iterator[_T]: | |
"""Eliminates duplicated elements.""" | |
done = set() | |
for obj in objs: | |
if obj in done: | |
continue | |
done.add(obj) | |
yield obj | |
def fsplit(pred: Callable[[_T], bool], objs: Iterable[_T]) -> Tuple[List[_T], List[_T]]: | |
"""Split a list into two classes according to the predicate.""" | |
t = [] | |
f = [] | |
for obj in objs: | |
if pred(obj): | |
t.append(obj) | |
else: | |
f.append(obj) | |
return t, f | |
def drange(v0: float, v1: float, d: int) -> range: | |
"""Returns a discrete range.""" | |
return range(int(v0) // d, int(v1 + d) // d) | |
def get_bound(pts: Iterable[Point]) -> Rect: | |
"""Compute a minimal rectangle that covers all the points.""" | |
limit: Rect = (INF, INF, -INF, -INF) | |
(x0, y0, x1, y1) = limit | |
for x, y in pts: | |
x0 = min(x0, x) | |
y0 = min(y0, y) | |
x1 = max(x1, x) | |
y1 = max(y1, y) | |
return x0, y0, x1, y1 | |
def pick( | |
seq: Iterable[_T], | |
func: Callable[[_T], float], | |
maxobj: Optional[_T] = None, | |
) -> Optional[_T]: | |
"""Picks the object obj where func(obj) has the highest value.""" | |
maxscore = None | |
for obj in seq: | |
score = func(obj) | |
if maxscore is None or maxscore < score: | |
(maxscore, maxobj) = (score, obj) | |
return maxobj | |
def choplist(n: int, seq: Iterable[_T]) -> Iterator[Tuple[_T, ...]]: | |
"""Groups every n elements of the list.""" | |
r = [] | |
for x in seq: | |
r.append(x) | |
if len(r) == n: | |
yield tuple(r) | |
r = [] | |
def nunpack(s: bytes, default: int = 0) -> int: | |
"""Unpacks 1 to 4 or 8 byte integers (big endian).""" | |
length = len(s) | |
if not length: | |
return default | |
elif length == 1: | |
return ord(s) | |
elif length == 2: | |
return cast(int, struct.unpack(">H", s)[0]) | |
elif length == 3: | |
return cast(int, struct.unpack(">L", b"\x00" + s)[0]) | |
elif length == 4: | |
return cast(int, struct.unpack(">L", s)[0]) | |
elif length == 8: | |
return cast(int, struct.unpack(">Q", s)[0]) | |
else: | |
raise PDFTypeError("invalid length: %d" % length) | |
PDFDocEncoding = "".join( | |
chr(x) | |
for x in ( | |
0x0000, | |
0x0001, | |
0x0002, | |
0x0003, | |
0x0004, | |
0x0005, | |
0x0006, | |
0x0007, | |
0x0008, | |
0x0009, | |
0x000A, | |
0x000B, | |
0x000C, | |
0x000D, | |
0x000E, | |
0x000F, | |
0x0010, | |
0x0011, | |
0x0012, | |
0x0013, | |
0x0014, | |
0x0015, | |
0x0017, | |
0x0017, | |
0x02D8, | |
0x02C7, | |
0x02C6, | |
0x02D9, | |
0x02DD, | |
0x02DB, | |
0x02DA, | |
0x02DC, | |
0x0020, | |
0x0021, | |
0x0022, | |
0x0023, | |
0x0024, | |
0x0025, | |
0x0026, | |
0x0027, | |
0x0028, | |
0x0029, | |
0x002A, | |
0x002B, | |
0x002C, | |
0x002D, | |
0x002E, | |
0x002F, | |
0x0030, | |
0x0031, | |
0x0032, | |
0x0033, | |
0x0034, | |
0x0035, | |
0x0036, | |
0x0037, | |
0x0038, | |
0x0039, | |
0x003A, | |
0x003B, | |
0x003C, | |
0x003D, | |
0x003E, | |
0x003F, | |
0x0040, | |
0x0041, | |
0x0042, | |
0x0043, | |
0x0044, | |
0x0045, | |
0x0046, | |
0x0047, | |
0x0048, | |
0x0049, | |
0x004A, | |
0x004B, | |
0x004C, | |
0x004D, | |
0x004E, | |
0x004F, | |
0x0050, | |
0x0051, | |
0x0052, | |
0x0053, | |
0x0054, | |
0x0055, | |
0x0056, | |
0x0057, | |
0x0058, | |
0x0059, | |
0x005A, | |
0x005B, | |
0x005C, | |
0x005D, | |
0x005E, | |
0x005F, | |
0x0060, | |
0x0061, | |
0x0062, | |
0x0063, | |
0x0064, | |
0x0065, | |
0x0066, | |
0x0067, | |
0x0068, | |
0x0069, | |
0x006A, | |
0x006B, | |
0x006C, | |
0x006D, | |
0x006E, | |
0x006F, | |
0x0070, | |
0x0071, | |
0x0072, | |
0x0073, | |
0x0074, | |
0x0075, | |
0x0076, | |
0x0077, | |
0x0078, | |
0x0079, | |
0x007A, | |
0x007B, | |
0x007C, | |
0x007D, | |
0x007E, | |
0x0000, | |
0x2022, | |
0x2020, | |
0x2021, | |
0x2026, | |
0x2014, | |
0x2013, | |
0x0192, | |
0x2044, | |
0x2039, | |
0x203A, | |
0x2212, | |
0x2030, | |
0x201E, | |
0x201C, | |
0x201D, | |
0x2018, | |
0x2019, | |
0x201A, | |
0x2122, | |
0xFB01, | |
0xFB02, | |
0x0141, | |
0x0152, | |
0x0160, | |
0x0178, | |
0x017D, | |
0x0131, | |
0x0142, | |
0x0153, | |
0x0161, | |
0x017E, | |
0x0000, | |
0x20AC, | |
0x00A1, | |
0x00A2, | |
0x00A3, | |
0x00A4, | |
0x00A5, | |
0x00A6, | |
0x00A7, | |
0x00A8, | |
0x00A9, | |
0x00AA, | |
0x00AB, | |
0x00AC, | |
0x0000, | |
0x00AE, | |
0x00AF, | |
0x00B0, | |
0x00B1, | |
0x00B2, | |
0x00B3, | |
0x00B4, | |
0x00B5, | |
0x00B6, | |
0x00B7, | |
0x00B8, | |
0x00B9, | |
0x00BA, | |
0x00BB, | |
0x00BC, | |
0x00BD, | |
0x00BE, | |
0x00BF, | |
0x00C0, | |
0x00C1, | |
0x00C2, | |
0x00C3, | |
0x00C4, | |
0x00C5, | |
0x00C6, | |
0x00C7, | |
0x00C8, | |
0x00C9, | |
0x00CA, | |
0x00CB, | |
0x00CC, | |
0x00CD, | |
0x00CE, | |
0x00CF, | |
0x00D0, | |
0x00D1, | |
0x00D2, | |
0x00D3, | |
0x00D4, | |
0x00D5, | |
0x00D6, | |
0x00D7, | |
0x00D8, | |
0x00D9, | |
0x00DA, | |
0x00DB, | |
0x00DC, | |
0x00DD, | |
0x00DE, | |
0x00DF, | |
0x00E0, | |
0x00E1, | |
0x00E2, | |
0x00E3, | |
0x00E4, | |
0x00E5, | |
0x00E6, | |
0x00E7, | |
0x00E8, | |
0x00E9, | |
0x00EA, | |
0x00EB, | |
0x00EC, | |
0x00ED, | |
0x00EE, | |
0x00EF, | |
0x00F0, | |
0x00F1, | |
0x00F2, | |
0x00F3, | |
0x00F4, | |
0x00F5, | |
0x00F6, | |
0x00F7, | |
0x00F8, | |
0x00F9, | |
0x00FA, | |
0x00FB, | |
0x00FC, | |
0x00FD, | |
0x00FE, | |
0x00FF, | |
) | |
) | |
def decode_text(s: bytes) -> str: | |
"""Decodes a PDFDocEncoding string to Unicode.""" | |
if s.startswith(b"\xfe\xff"): | |
return str(s[2:], "utf-16be", "ignore") | |
else: | |
return "".join(PDFDocEncoding[c] for c in s) | |
def enc(x: str) -> str: | |
"""Encodes a string for SGML/XML/HTML""" | |
if isinstance(x, bytes): | |
return "" | |
return escape(x) | |
def bbox2str(bbox: Rect) -> str: | |
(x0, y0, x1, y1) = bbox | |
return f"{x0:.3f},{y0:.3f},{x1:.3f},{y1:.3f}" | |
def matrix2str(m: Matrix) -> str: | |
(a, b, c, d, e, f) = m | |
return f"[{a:.2f},{b:.2f},{c:.2f},{d:.2f}, ({e:.2f},{f:.2f})]" | |
def vecBetweenBoxes(obj1: "LTComponent", obj2: "LTComponent") -> Point: | |
"""A distance function between two TextBoxes. | |
Consider the bounding rectangle for obj1 and obj2. | |
Return vector between 2 boxes boundaries if they don't overlap, otherwise | |
returns vector betweeen boxes centers | |
+------+..........+ (x1, y1) | |
| obj1 | : | |
+------+www+------+ | |
: | obj2 | | |
(x0, y0) +..........+------+ | |
""" | |
(x0, y0) = (min(obj1.x0, obj2.x0), min(obj1.y0, obj2.y0)) | |
(x1, y1) = (max(obj1.x1, obj2.x1), max(obj1.y1, obj2.y1)) | |
(ow, oh) = (x1 - x0, y1 - y0) | |
(iw, ih) = (ow - obj1.width - obj2.width, oh - obj1.height - obj2.height) | |
if iw < 0 and ih < 0: | |
# if one is inside another we compute euclidean distance | |
(xc1, yc1) = ((obj1.x0 + obj1.x1) / 2, (obj1.y0 + obj1.y1) / 2) | |
(xc2, yc2) = ((obj2.x0 + obj2.x1) / 2, (obj2.y0 + obj2.y1) / 2) | |
return xc1 - xc2, yc1 - yc2 | |
else: | |
return max(0, iw), max(0, ih) | |
LTComponentT = TypeVar("LTComponentT", bound="LTComponent") | |
class Plane(Generic[LTComponentT]): | |
"""A set-like data structure for objects placed on a plane. | |
Can efficiently find objects in a certain rectangular area. | |
It maintains two parallel lists of objects, each of | |
which is sorted by its x or y coordinate. | |
""" | |
def __init__(self, bbox: Rect, gridsize: int = 50) -> None: | |
self._seq: List[LTComponentT] = [] # preserve the object order. | |
self._objs: Set[LTComponentT] = set() | |
self._grid: Dict[Point, List[LTComponentT]] = {} | |
self.gridsize = gridsize | |
(self.x0, self.y0, self.x1, self.y1) = bbox | |
def __repr__(self) -> str: | |
return "<Plane objs=%r>" % list(self) | |
def __iter__(self) -> Iterator[LTComponentT]: | |
return (obj for obj in self._seq if obj in self._objs) | |
def __len__(self) -> int: | |
return len(self._objs) | |
def __contains__(self, obj: object) -> bool: | |
return obj in self._objs | |
def _getrange(self, bbox: Rect) -> Iterator[Point]: | |
(x0, y0, x1, y1) = bbox | |
if x1 <= self.x0 or self.x1 <= x0 or y1 <= self.y0 or self.y1 <= y0: | |
return | |
x0 = max(self.x0, x0) | |
y0 = max(self.y0, y0) | |
x1 = min(self.x1, x1) | |
y1 = min(self.y1, y1) | |
for grid_y in drange(y0, y1, self.gridsize): | |
for grid_x in drange(x0, x1, self.gridsize): | |
yield (grid_x, grid_y) | |
def extend(self, objs: Iterable[LTComponentT]) -> None: | |
for obj in objs: | |
self.add(obj) | |
def add(self, obj: LTComponentT) -> None: | |
"""Place an object.""" | |
for k in self._getrange((obj.x0, obj.y0, obj.x1, obj.y1)): | |
if k not in self._grid: | |
r: List[LTComponentT] = [] | |
self._grid[k] = r | |
else: | |
r = self._grid[k] | |
r.append(obj) | |
self._seq.append(obj) | |
self._objs.add(obj) | |
def remove(self, obj: LTComponentT) -> None: | |
"""Displace an object.""" | |
for k in self._getrange((obj.x0, obj.y0, obj.x1, obj.y1)): | |
try: | |
self._grid[k].remove(obj) | |
except (KeyError, ValueError): | |
pass | |
self._objs.remove(obj) | |
def find(self, bbox: Rect) -> Iterator[LTComponentT]: | |
"""Finds objects that are in a certain area.""" | |
(x0, y0, x1, y1) = bbox | |
done = set() | |
for k in self._getrange(bbox): | |
if k not in self._grid: | |
continue | |
for obj in self._grid[k]: | |
if obj in done: | |
continue | |
done.add(obj) | |
if obj.x1 <= x0 or x1 <= obj.x0 or obj.y1 <= y0 or y1 <= obj.y0: | |
continue | |
yield obj | |
ROMAN_ONES = ["i", "x", "c", "m"] | |
ROMAN_FIVES = ["v", "l", "d"] | |
def format_int_roman(value: int) -> str: | |
"""Format a number as lowercase Roman numerals.""" | |
assert 0 < value < 4000 | |
result: List[str] = [] | |
index = 0 | |
while value != 0: | |
value, remainder = divmod(value, 10) | |
if remainder == 9: | |
result.insert(0, ROMAN_ONES[index]) | |
result.insert(1, ROMAN_ONES[index + 1]) | |
elif remainder == 4: | |
result.insert(0, ROMAN_ONES[index]) | |
result.insert(1, ROMAN_FIVES[index]) | |
else: | |
over_five = remainder >= 5 | |
if over_five: | |
result.insert(0, ROMAN_FIVES[index]) | |
remainder -= 5 | |
result.insert(1 if over_five else 0, ROMAN_ONES[index] * remainder) | |
index += 1 | |
return "".join(result) | |
def format_int_alpha(value: int) -> str: | |
"""Format a number as lowercase letters a-z, aa-zz, etc.""" | |
assert value > 0 | |
result: List[str] = [] | |
while value != 0: | |
value, remainder = divmod(value - 1, len(string.ascii_lowercase)) | |
result.append(string.ascii_lowercase[remainder]) | |
result.reverse() | |
return "".join(result) | |
def get_device(): | |
"""Get the device to use for computation.""" | |
try: | |
import torch | |
if torch.cuda.is_available(): | |
return "cuda:0" | |
except ImportError: | |
pass | |
return "cpu" | |