python-pages 0.1.0__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.
- pages/__init__.py +65 -0
- pages/api.py +1869 -0
- pages/cli.py +43 -0
- pages/comments.py +168 -0
- pages/components.py +403 -0
- pages/core_properties.py +126 -0
- pages/data/default.pages +0 -0
- pages/document.py +215 -0
- pages/hyperlinks.py +57 -0
- pages/iwa.py +101 -0
- pages/metadata.py +117 -0
- pages/pages.py +365 -0
- pages/pictures.py +549 -0
- pages/protobuf.py +162 -0
- pages/snappy.py +140 -0
- pages/stylesheet.py +892 -0
- pages/tables.py +493 -0
- pages/text.py +294 -0
- python_pages-0.1.0.dist-info/METADATA +271 -0
- python_pages-0.1.0.dist-info/RECORD +24 -0
- python_pages-0.1.0.dist-info/WHEEL +5 -0
- python_pages-0.1.0.dist-info/entry_points.txt +2 -0
- python_pages-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_pages-0.1.0.dist-info/top_level.txt +1 -0
pages/protobuf.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Small lossless protobuf wire parser for schema discovery and mutation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProtobufError(ValueError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read_varint(data: bytes, pos: int = 0) -> tuple[int, int]:
|
|
14
|
+
value = 0
|
|
15
|
+
shift = 0
|
|
16
|
+
for _ in range(10):
|
|
17
|
+
if pos >= len(data):
|
|
18
|
+
raise ProtobufError("truncated varint")
|
|
19
|
+
byte = data[pos]
|
|
20
|
+
pos += 1
|
|
21
|
+
value |= (byte & 0x7F) << shift
|
|
22
|
+
if byte < 0x80:
|
|
23
|
+
return value, pos
|
|
24
|
+
shift += 7
|
|
25
|
+
raise ProtobufError("varint is longer than 10 bytes")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def encode_varint(value: int) -> bytes:
|
|
29
|
+
if value < 0:
|
|
30
|
+
value &= (1 << 64) - 1
|
|
31
|
+
out = bytearray()
|
|
32
|
+
while value >= 0x80:
|
|
33
|
+
out.append((value & 0x7F) | 0x80)
|
|
34
|
+
value >>= 7
|
|
35
|
+
out.append(value)
|
|
36
|
+
return bytes(out)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Field:
|
|
41
|
+
number: int
|
|
42
|
+
wire_type: int
|
|
43
|
+
value: int | bytes
|
|
44
|
+
original: bytes = b""
|
|
45
|
+
dirty: bool = False
|
|
46
|
+
children: "Message | None" = None
|
|
47
|
+
|
|
48
|
+
def encode(self) -> bytes:
|
|
49
|
+
if not self.dirty and self.original:
|
|
50
|
+
return self.original
|
|
51
|
+
key = encode_varint((self.number << 3) | self.wire_type)
|
|
52
|
+
if self.wire_type == 0:
|
|
53
|
+
return key + encode_varint(int(self.value))
|
|
54
|
+
if self.wire_type == 1:
|
|
55
|
+
return key + int(self.value).to_bytes(8, "little")
|
|
56
|
+
if self.wire_type == 2:
|
|
57
|
+
payload = self.children.encode() if self.children is not None else bytes(self.value)
|
|
58
|
+
return key + encode_varint(len(payload)) + payload
|
|
59
|
+
if self.wire_type == 5:
|
|
60
|
+
return key + int(self.value).to_bytes(4, "little")
|
|
61
|
+
raise ProtobufError(f"unsupported wire type {self.wire_type}")
|
|
62
|
+
|
|
63
|
+
def set_bytes(self, value: bytes) -> None:
|
|
64
|
+
if self.wire_type != 2:
|
|
65
|
+
raise TypeError("not a length-delimited field")
|
|
66
|
+
self.value = value
|
|
67
|
+
self.children = None
|
|
68
|
+
self.dirty = True
|
|
69
|
+
|
|
70
|
+
def set_varint(self, value: int) -> None:
|
|
71
|
+
if self.wire_type != 0:
|
|
72
|
+
raise TypeError("not a varint field")
|
|
73
|
+
self.value = value
|
|
74
|
+
self.dirty = True
|
|
75
|
+
|
|
76
|
+
def set_fixed32(self, value: int) -> None:
|
|
77
|
+
if self.wire_type != 5:
|
|
78
|
+
raise TypeError("not a fixed32 field")
|
|
79
|
+
self.value = value
|
|
80
|
+
self.dirty = True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class Message:
|
|
85
|
+
fields: list[Field] = field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def parse(cls, data: bytes) -> "Message":
|
|
89
|
+
fields: list[Field] = []
|
|
90
|
+
pos = 0
|
|
91
|
+
while pos < len(data):
|
|
92
|
+
start = pos
|
|
93
|
+
key, pos = read_varint(data, pos)
|
|
94
|
+
number, wire = key >> 3, key & 7
|
|
95
|
+
if number == 0:
|
|
96
|
+
raise ProtobufError(f"field zero at offset {start}")
|
|
97
|
+
if wire == 0:
|
|
98
|
+
value, pos = read_varint(data, pos)
|
|
99
|
+
elif wire == 1:
|
|
100
|
+
if pos + 8 > len(data):
|
|
101
|
+
raise ProtobufError("truncated fixed64")
|
|
102
|
+
value = int.from_bytes(data[pos : pos + 8], "little")
|
|
103
|
+
pos += 8
|
|
104
|
+
elif wire == 2:
|
|
105
|
+
length, pos = read_varint(data, pos)
|
|
106
|
+
if pos + length > len(data):
|
|
107
|
+
raise ProtobufError("truncated length-delimited field")
|
|
108
|
+
value = data[pos : pos + length]
|
|
109
|
+
pos += length
|
|
110
|
+
elif wire == 5:
|
|
111
|
+
if pos + 4 > len(data):
|
|
112
|
+
raise ProtobufError("truncated fixed32")
|
|
113
|
+
value = int.from_bytes(data[pos : pos + 4], "little")
|
|
114
|
+
pos += 4
|
|
115
|
+
else:
|
|
116
|
+
raise ProtobufError(f"unsupported wire type {wire} at {start}")
|
|
117
|
+
fields.append(Field(number, wire, value, data[start:pos]))
|
|
118
|
+
return cls(fields)
|
|
119
|
+
|
|
120
|
+
def encode(self) -> bytes:
|
|
121
|
+
return b"".join(f.encode() for f in self.fields)
|
|
122
|
+
|
|
123
|
+
def get(self, number: int) -> list[Field]:
|
|
124
|
+
return [f for f in self.fields if f.number == number]
|
|
125
|
+
|
|
126
|
+
def one(self, number: int, default=None):
|
|
127
|
+
items = self.get(number)
|
|
128
|
+
return items[0] if items else default
|
|
129
|
+
|
|
130
|
+
def add_varint(self, number: int, value: int) -> Field:
|
|
131
|
+
result = Field(number, 0, value, dirty=True)
|
|
132
|
+
self.fields.append(result)
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
def add_bytes(self, number: int, value: bytes) -> Field:
|
|
136
|
+
result = Field(number, 2, value, dirty=True)
|
|
137
|
+
self.fields.append(result)
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
def add_fixed32(self, number: int, value: int) -> Field:
|
|
141
|
+
result = Field(number, 5, value, dirty=True)
|
|
142
|
+
self.fields.append(result)
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
def iter_strings(self, min_length: int = 2) -> Iterable[tuple[Field, str]]:
|
|
146
|
+
for item in self.fields:
|
|
147
|
+
if item.wire_type != 2 or len(item.value) < min_length:
|
|
148
|
+
continue
|
|
149
|
+
try:
|
|
150
|
+
text = bytes(item.value).decode("utf-8")
|
|
151
|
+
except UnicodeDecodeError:
|
|
152
|
+
continue
|
|
153
|
+
if text.isprintable() or any(c in text for c in "\n\t"):
|
|
154
|
+
yield item, text
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def try_message(data: bytes) -> Message | None:
|
|
158
|
+
try:
|
|
159
|
+
message = Message.parse(data)
|
|
160
|
+
except ProtobufError:
|
|
161
|
+
return None
|
|
162
|
+
return message if message.fields else None
|
pages/snappy.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Raw Snappy and iWork's small custom framing.
|
|
2
|
+
|
|
3
|
+
iWork uses blocks consisting of byte 0 followed by a 24-bit little-endian
|
|
4
|
+
compressed length and a raw (not standard framed-stream) Snappy block.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SnappyError(ValueError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _read_varint(data: bytes, pos: int = 0) -> tuple[int, int]:
|
|
15
|
+
value = 0
|
|
16
|
+
shift = 0
|
|
17
|
+
while pos < len(data) and shift < 35:
|
|
18
|
+
byte = data[pos]
|
|
19
|
+
pos += 1
|
|
20
|
+
value |= (byte & 0x7F) << shift
|
|
21
|
+
if byte < 0x80:
|
|
22
|
+
return value, pos
|
|
23
|
+
shift += 7
|
|
24
|
+
raise SnappyError("invalid Snappy length varint")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _varint(value: int) -> bytes:
|
|
28
|
+
out = bytearray()
|
|
29
|
+
while value >= 0x80:
|
|
30
|
+
out.append((value & 0x7F) | 0x80)
|
|
31
|
+
value >>= 7
|
|
32
|
+
out.append(value)
|
|
33
|
+
return bytes(out)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decompress_raw(data: bytes) -> bytes:
|
|
37
|
+
expected, pos = _read_varint(data)
|
|
38
|
+
out = bytearray()
|
|
39
|
+
while pos < len(data):
|
|
40
|
+
tag = data[pos]
|
|
41
|
+
pos += 1
|
|
42
|
+
kind = tag & 3
|
|
43
|
+
if kind == 0:
|
|
44
|
+
n = tag >> 2
|
|
45
|
+
if n < 60:
|
|
46
|
+
length = n + 1
|
|
47
|
+
else:
|
|
48
|
+
width = n - 59
|
|
49
|
+
if width > 4 or pos + width > len(data):
|
|
50
|
+
raise SnappyError("invalid literal length")
|
|
51
|
+
length = int.from_bytes(data[pos : pos + width], "little") + 1
|
|
52
|
+
pos += width
|
|
53
|
+
if pos + length > len(data):
|
|
54
|
+
raise SnappyError("truncated literal")
|
|
55
|
+
out += data[pos : pos + length]
|
|
56
|
+
pos += length
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
if kind == 1:
|
|
60
|
+
if pos >= len(data):
|
|
61
|
+
raise SnappyError("truncated COPY_1")
|
|
62
|
+
length = ((tag >> 2) & 7) + 4
|
|
63
|
+
offset = ((tag & 0xE0) << 3) | data[pos]
|
|
64
|
+
pos += 1
|
|
65
|
+
elif kind == 2:
|
|
66
|
+
if pos + 2 > len(data):
|
|
67
|
+
raise SnappyError("truncated COPY_2")
|
|
68
|
+
length = (tag >> 2) + 1
|
|
69
|
+
offset = int.from_bytes(data[pos : pos + 2], "little")
|
|
70
|
+
pos += 2
|
|
71
|
+
else:
|
|
72
|
+
if pos + 4 > len(data):
|
|
73
|
+
raise SnappyError("truncated COPY_4")
|
|
74
|
+
length = (tag >> 2) + 1
|
|
75
|
+
offset = int.from_bytes(data[pos : pos + 4], "little")
|
|
76
|
+
pos += 4
|
|
77
|
+
if offset <= 0 or offset > len(out):
|
|
78
|
+
raise SnappyError(f"invalid copy offset {offset}")
|
|
79
|
+
for _ in range(length):
|
|
80
|
+
out.append(out[-offset])
|
|
81
|
+
|
|
82
|
+
if len(out) != expected:
|
|
83
|
+
raise SnappyError(f"decoded {len(out)} bytes, expected {expected}")
|
|
84
|
+
return bytes(out)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def compress_raw_literal(data: bytes) -> bytes:
|
|
88
|
+
"""Create a valid raw Snappy block using literals only.
|
|
89
|
+
|
|
90
|
+
It is deliberately simple: ZIP will deflate the outer .pages archive, and
|
|
91
|
+
correctness/preservation is more important here than IWA-level ratio.
|
|
92
|
+
"""
|
|
93
|
+
out = bytearray(_varint(len(data)))
|
|
94
|
+
pos = 0
|
|
95
|
+
while pos < len(data):
|
|
96
|
+
length = min(len(data) - pos, 65536)
|
|
97
|
+
n = length - 1
|
|
98
|
+
if n < 60:
|
|
99
|
+
out.append(n << 2)
|
|
100
|
+
else:
|
|
101
|
+
width = max(1, (n.bit_length() + 7) // 8)
|
|
102
|
+
out.append((59 + width) << 2)
|
|
103
|
+
out += n.to_bytes(width, "little")
|
|
104
|
+
out += data[pos : pos + length]
|
|
105
|
+
pos += length
|
|
106
|
+
return bytes(out)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def decompress_iwork(data: bytes) -> tuple[bytes, list[int]]:
|
|
110
|
+
out = bytearray()
|
|
111
|
+
decoded_sizes: list[int] = []
|
|
112
|
+
pos = 0
|
|
113
|
+
while pos < len(data):
|
|
114
|
+
if pos + 4 > len(data) or data[pos] != 0:
|
|
115
|
+
raise SnappyError(f"bad iWork frame at offset {pos}")
|
|
116
|
+
length = int.from_bytes(data[pos + 1 : pos + 4], "little")
|
|
117
|
+
pos += 4
|
|
118
|
+
if pos + length > len(data):
|
|
119
|
+
raise SnappyError("truncated iWork frame")
|
|
120
|
+
block = decompress_raw(data[pos : pos + length])
|
|
121
|
+
out += block
|
|
122
|
+
decoded_sizes.append(len(block))
|
|
123
|
+
pos += length
|
|
124
|
+
return bytes(out), decoded_sizes
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def compress_iwork(data: bytes, decoded_sizes: list[int] | None = None) -> bytes:
|
|
128
|
+
if not decoded_sizes or sum(decoded_sizes) != len(data):
|
|
129
|
+
decoded_sizes = [min(65536, len(data) - pos) for pos in range(0, len(data), 65536)]
|
|
130
|
+
out = bytearray()
|
|
131
|
+
pos = 0
|
|
132
|
+
for size in decoded_sizes:
|
|
133
|
+
compressed = compress_raw_literal(data[pos : pos + size])
|
|
134
|
+
if len(compressed) >= 1 << 24:
|
|
135
|
+
raise SnappyError("compressed iWork block exceeds 24-bit length")
|
|
136
|
+
out.append(0)
|
|
137
|
+
out += len(compressed).to_bytes(3, "little")
|
|
138
|
+
out += compressed
|
|
139
|
+
pos += size
|
|
140
|
+
return bytes(out)
|