emboss-pdf 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.
- emboss/__init__.py +77 -0
- emboss/__main__.py +311 -0
- emboss/adapters/__init__.py +7 -0
- emboss/adapters/docx_export.py +221 -0
- emboss/adapters/html_export.py +342 -0
- emboss/adapters/markdown_export.py +224 -0
- emboss/adapters/pydantic_schema.py +999 -0
- emboss/bibliography.py +208 -0
- emboss/charts.py +375 -0
- emboss/code_highlight.py +292 -0
- emboss/colors.py +298 -0
- emboss/constraints.py +272 -0
- emboss/crossref.py +174 -0
- emboss/generate.py +367 -0
- emboss/images.py +289 -0
- emboss/intelligence.py +843 -0
- emboss/layout/__init__.py +5 -0
- emboss/layout/engine.py +1269 -0
- emboss/markdown.py +290 -0
- emboss/math_render.py +772 -0
- emboss/numbering.py +37 -0
- emboss/pdf/__init__.py +12 -0
- emboss/pdf/assembler.py +153 -0
- emboss/pdf/fonts.py +233 -0
- emboss/pdf/objects.py +200 -0
- emboss/pdf/streams.py +275 -0
- emboss/pdf/tags.py +151 -0
- emboss/pdf/verify.py +136 -0
- emboss/pdfa.py +291 -0
- emboss/py.typed +0 -0
- emboss/redaction.py +99 -0
- emboss/signing.py +263 -0
- emboss/slides.py +223 -0
- emboss/spec.py +646 -0
- emboss/styles.py +295 -0
- emboss/svg.py +332 -0
- emboss/templates.py +133 -0
- emboss/toc.py +152 -0
- emboss/typography/__init__.py +10 -0
- emboss/typography/font_metrics.py +456 -0
- emboss/typography/hyphenation.py +324 -0
- emboss/typography/ligatures.py +42 -0
- emboss/typography/line_breaking.py +570 -0
- emboss/typography/protrusion.py +84 -0
- emboss/writer.py +1391 -0
- emboss_pdf-0.1.0.dist-info/METADATA +835 -0
- emboss_pdf-0.1.0.dist-info/RECORD +52 -0
- emboss_pdf-0.1.0.dist-info/WHEEL +5 -0
- emboss_pdf-0.1.0.dist-info/entry_points.txt +2 -0
- emboss_pdf-0.1.0.dist-info/licenses/LICENSE +191 -0
- emboss_pdf-0.1.0.dist-info/licenses/NOTICE +9 -0
- emboss_pdf-0.1.0.dist-info/top_level.txt +1 -0
emboss/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Emboss - constraint-driven PDF generation.
|
|
2
|
+
|
|
3
|
+
Describe a document; the engine handles layout, typography, and the
|
|
4
|
+
PDF/UA structure tree. Output is deterministic: identical input always
|
|
5
|
+
produces identical bytes.
|
|
6
|
+
|
|
7
|
+
from emboss import Document
|
|
8
|
+
|
|
9
|
+
doc = Document(title="Quarterly Report", style="finance")
|
|
10
|
+
doc.heading("Revenue Analysis", level=1)
|
|
11
|
+
doc.paragraph("Revenue increased 12% year over year.")
|
|
12
|
+
doc.table(headers=["Region", "Q3"], rows=[["North", "$2.4M"]])
|
|
13
|
+
doc.save("report.pdf")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .constraints import (
|
|
17
|
+
ConstraintValidator, Issue, ValidationError, ValidationResult,
|
|
18
|
+
)
|
|
19
|
+
from .spec import (
|
|
20
|
+
BibliographyBlock, BulletList, Callout, Chart, Citation, CodeBlock,
|
|
21
|
+
Document, Footnote, HeaderFooter, Heading, HorizontalRule, Image,
|
|
22
|
+
LegalFeatures, MathBlock, NumberedList, PageBreak, PageSpec, Paragraph,
|
|
23
|
+
SvgBlock, Table, TableCell, TextRun,
|
|
24
|
+
)
|
|
25
|
+
from .bibliography import format_citation, format_bibliography
|
|
26
|
+
from .code_highlight import tokenize, colorize, THEMES as CODE_THEMES, LANGUAGES
|
|
27
|
+
from .math_render import parse_math, MathExpression, render_math
|
|
28
|
+
from .colors import (
|
|
29
|
+
ColorTheme, resolve_color, PALETTES,
|
|
30
|
+
CmykColor, SpotColor, rgb_to_cmyk, parse_cmyk,
|
|
31
|
+
)
|
|
32
|
+
from .crossref import CrossReferenceIndex
|
|
33
|
+
from .svg import SvgImage, parse_svg, render_svg
|
|
34
|
+
from .numbering import NumberingContext
|
|
35
|
+
from .styles import PRESETS, Style, StyleSheet, resolve_preset
|
|
36
|
+
from .typography.font_metrics import FontMetrics, FontRegistry
|
|
37
|
+
from .typography.hyphenation import Hyphenator
|
|
38
|
+
from .typography.line_breaking import LineBreaker
|
|
39
|
+
from .intelligence import (
|
|
40
|
+
ContentAnalyzer, QualityScorer, SmartTypography, TableIntelligence,
|
|
41
|
+
DocumentTypeDetector,
|
|
42
|
+
)
|
|
43
|
+
from .slides import slide_document, SlideConfig, SLIDE_16_9, SLIDE_4_3
|
|
44
|
+
from .templates import (
|
|
45
|
+
memo, report, letter, invoice, academic_paper, legal_brief,
|
|
46
|
+
slide_deck, data_sheet,
|
|
47
|
+
)
|
|
48
|
+
from .markdown import parse_markdown
|
|
49
|
+
from .generate import spec_prompt, generate, parse_spec_json
|
|
50
|
+
from .writer import RenderResult, render_document
|
|
51
|
+
|
|
52
|
+
__version__ = "0.1.0"
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"Document", "PageSpec", "Heading", "Paragraph", "BulletList",
|
|
56
|
+
"NumberedList", "Table", "TableCell", "TextRun", "Image", "Chart",
|
|
57
|
+
"Footnote", "Callout", "CodeBlock", "MathBlock", "BibliographyBlock",
|
|
58
|
+
"Citation", "SvgBlock", "PageBreak", "HorizontalRule", "LegalFeatures",
|
|
59
|
+
"HeaderFooter", "SvgImage", "parse_svg", "render_svg", "NumberingContext",
|
|
60
|
+
"format_citation", "format_bibliography",
|
|
61
|
+
"tokenize", "colorize", "CODE_THEMES", "LANGUAGES",
|
|
62
|
+
"parse_math", "MathExpression", "render_math",
|
|
63
|
+
"ColorTheme", "resolve_color", "PALETTES",
|
|
64
|
+
"CmykColor", "SpotColor", "rgb_to_cmyk", "parse_cmyk",
|
|
65
|
+
"CrossReferenceIndex",
|
|
66
|
+
"Style", "StyleSheet", "PRESETS", "resolve_preset",
|
|
67
|
+
"FontRegistry", "FontMetrics", "Hyphenator", "LineBreaker",
|
|
68
|
+
"ConstraintValidator", "ValidationResult", "ValidationError", "Issue",
|
|
69
|
+
"slide_document", "SlideConfig", "SLIDE_16_9", "SLIDE_4_3",
|
|
70
|
+
"memo", "report", "letter", "invoice", "academic_paper",
|
|
71
|
+
"legal_brief", "slide_deck", "data_sheet",
|
|
72
|
+
"render_document", "RenderResult",
|
|
73
|
+
"parse_markdown", "spec_prompt", "generate", "parse_spec_json",
|
|
74
|
+
"ContentAnalyzer", "QualityScorer", "SmartTypography",
|
|
75
|
+
"TableIntelligence", "DocumentTypeDetector",
|
|
76
|
+
"__version__",
|
|
77
|
+
]
|
emboss/__main__.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""CLI entry point: emboss render spec.json -o output.pdf
|
|
2
|
+
|
|
3
|
+
Supports JSON input from files or stdin, so an LLM can pipe its output
|
|
4
|
+
directly into PDF generation:
|
|
5
|
+
|
|
6
|
+
llm "Generate a financial report" --json | emboss render - -o report.pdf
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _render(args: argparse.Namespace) -> int:
|
|
18
|
+
try:
|
|
19
|
+
from .adapters.pydantic_schema import DocumentSpec
|
|
20
|
+
except ImportError:
|
|
21
|
+
print(
|
|
22
|
+
"error: pydantic is required for JSON-to-PDF conversion.\n"
|
|
23
|
+
" pip install emboss-pdf[llm]",
|
|
24
|
+
file=sys.stderr,
|
|
25
|
+
)
|
|
26
|
+
return 1
|
|
27
|
+
|
|
28
|
+
if args.input == "-":
|
|
29
|
+
raw = sys.stdin.read()
|
|
30
|
+
else:
|
|
31
|
+
path = Path(args.input)
|
|
32
|
+
if not path.exists():
|
|
33
|
+
print(f"error: file not found: {path}", file=sys.stderr)
|
|
34
|
+
return 1
|
|
35
|
+
raw = path.read_text(encoding="utf-8")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
data = json.loads(raw)
|
|
39
|
+
except json.JSONDecodeError as exc:
|
|
40
|
+
print(f"error: invalid JSON: {exc}", file=sys.stderr)
|
|
41
|
+
return 1
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
spec = DocumentSpec.model_validate(data)
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
print(f"error: invalid document spec:\n {exc}", file=sys.stderr)
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
doc = spec.to_document()
|
|
51
|
+
pdf_bytes = doc.render()
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
print(f"error: rendering failed: {exc}", file=sys.stderr)
|
|
54
|
+
return 1
|
|
55
|
+
|
|
56
|
+
output = Path(args.output)
|
|
57
|
+
output.write_bytes(pdf_bytes)
|
|
58
|
+
|
|
59
|
+
if not args.quiet:
|
|
60
|
+
from .pdf.verify import verify_pdf
|
|
61
|
+
|
|
62
|
+
report = verify_pdf(pdf_bytes)
|
|
63
|
+
print(
|
|
64
|
+
f"{output} — {len(pdf_bytes):,} bytes, "
|
|
65
|
+
f"{report.page_count} page{'s' if report.page_count != 1 else ''}, "
|
|
66
|
+
f"tagged={report.has_struct_tree}",
|
|
67
|
+
)
|
|
68
|
+
if not report.ok:
|
|
69
|
+
for problem in report.problems:
|
|
70
|
+
print(f" warning: {problem}", file=sys.stderr)
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _schema(args: argparse.Namespace) -> int:
|
|
75
|
+
try:
|
|
76
|
+
from .adapters.pydantic_schema import generate_json_schema
|
|
77
|
+
except ImportError:
|
|
78
|
+
print(
|
|
79
|
+
"error: pydantic is required for schema generation.\n"
|
|
80
|
+
" pip install emboss-pdf[llm]",
|
|
81
|
+
file=sys.stderr,
|
|
82
|
+
)
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
output = generate_json_schema(indent=2)
|
|
86
|
+
|
|
87
|
+
if args.output:
|
|
88
|
+
Path(args.output).write_text(output, encoding="utf-8")
|
|
89
|
+
if not args.quiet:
|
|
90
|
+
print(f"Schema written to {args.output}")
|
|
91
|
+
else:
|
|
92
|
+
print(output)
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _verify(args: argparse.Namespace) -> int:
|
|
97
|
+
from .pdf.verify import verify_pdf
|
|
98
|
+
|
|
99
|
+
path = Path(args.input)
|
|
100
|
+
if not path.exists():
|
|
101
|
+
print(f"error: file not found: {path}", file=sys.stderr)
|
|
102
|
+
return 1
|
|
103
|
+
|
|
104
|
+
data = path.read_bytes()
|
|
105
|
+
report = verify_pdf(data)
|
|
106
|
+
print(report)
|
|
107
|
+
return 0 if report.ok else 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _export(args: argparse.Namespace) -> int:
|
|
111
|
+
try:
|
|
112
|
+
from .adapters.pydantic_schema import DocumentSpec
|
|
113
|
+
except ImportError:
|
|
114
|
+
print(
|
|
115
|
+
"error: pydantic is required.\n pip install emboss-pdf[llm]",
|
|
116
|
+
file=sys.stderr,
|
|
117
|
+
)
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
if args.input == "-":
|
|
121
|
+
raw = sys.stdin.read()
|
|
122
|
+
else:
|
|
123
|
+
path = Path(args.input)
|
|
124
|
+
if not path.exists():
|
|
125
|
+
print(f"error: file not found: {path}", file=sys.stderr)
|
|
126
|
+
return 1
|
|
127
|
+
raw = path.read_text(encoding="utf-8")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
spec = DocumentSpec.model_validate_json(raw)
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
print(f"error: invalid document spec:\n {exc}", file=sys.stderr)
|
|
133
|
+
return 1
|
|
134
|
+
|
|
135
|
+
doc = spec.to_document()
|
|
136
|
+
fmt = args.format
|
|
137
|
+
|
|
138
|
+
if fmt == "html":
|
|
139
|
+
from .adapters.html_export import to_html
|
|
140
|
+
|
|
141
|
+
output = to_html(doc, standalone=True)
|
|
142
|
+
elif fmt == "markdown":
|
|
143
|
+
from .adapters.markdown_export import to_markdown
|
|
144
|
+
|
|
145
|
+
output = to_markdown(doc)
|
|
146
|
+
elif fmt == "office-json":
|
|
147
|
+
from .adapters.docx_export import to_office_dict
|
|
148
|
+
|
|
149
|
+
output = json.dumps(to_office_dict(doc), indent=2, ensure_ascii=False)
|
|
150
|
+
else:
|
|
151
|
+
print(f"error: unknown format: {fmt}", file=sys.stderr)
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
if args.output:
|
|
155
|
+
Path(args.output).write_text(output, encoding="utf-8")
|
|
156
|
+
if not args.quiet:
|
|
157
|
+
print(f"Exported {fmt} to {args.output}")
|
|
158
|
+
else:
|
|
159
|
+
print(output)
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _analyze(args: argparse.Namespace) -> int:
|
|
164
|
+
from .intelligence import ContentAnalyzer
|
|
165
|
+
|
|
166
|
+
if args.input == "-":
|
|
167
|
+
raw = sys.stdin.read()
|
|
168
|
+
else:
|
|
169
|
+
path = Path(args.input)
|
|
170
|
+
if not path.exists():
|
|
171
|
+
print(f"error: file not found: {path}", file=sys.stderr)
|
|
172
|
+
return 1
|
|
173
|
+
raw = path.read_text(encoding="utf-8")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
data = json.loads(raw)
|
|
177
|
+
except json.JSONDecodeError as exc:
|
|
178
|
+
print(f"error: invalid JSON: {exc}", file=sys.stderr)
|
|
179
|
+
return 1
|
|
180
|
+
|
|
181
|
+
analyzer = ContentAnalyzer()
|
|
182
|
+
analysis = analyzer.analyze_spec(data)
|
|
183
|
+
print(analysis.summary)
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _validate(args: argparse.Namespace) -> int:
|
|
188
|
+
try:
|
|
189
|
+
from .adapters.pydantic_schema import DocumentSpec
|
|
190
|
+
except ImportError:
|
|
191
|
+
print(
|
|
192
|
+
"error: pydantic is required.\n pip install emboss-pdf[llm]",
|
|
193
|
+
file=sys.stderr,
|
|
194
|
+
)
|
|
195
|
+
return 1
|
|
196
|
+
|
|
197
|
+
if args.input == "-":
|
|
198
|
+
raw = sys.stdin.read()
|
|
199
|
+
else:
|
|
200
|
+
raw = Path(args.input).read_text(encoding="utf-8")
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
data = json.loads(raw)
|
|
204
|
+
except json.JSONDecodeError as exc:
|
|
205
|
+
print(f"error: invalid JSON: {exc}", file=sys.stderr)
|
|
206
|
+
return 1
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
spec = DocumentSpec.model_validate(data)
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
print(f"invalid: {exc}", file=sys.stderr)
|
|
212
|
+
return 1
|
|
213
|
+
|
|
214
|
+
print(
|
|
215
|
+
f"valid: {spec.title!r}, "
|
|
216
|
+
f"{len(spec.content)} blocks, "
|
|
217
|
+
f"style={spec.style}"
|
|
218
|
+
)
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main(argv: list[str] | None = None) -> int:
|
|
223
|
+
parser = argparse.ArgumentParser(
|
|
224
|
+
prog="emboss",
|
|
225
|
+
description="Emboss — constraint-driven PDF generation for LLM pipelines",
|
|
226
|
+
)
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
"--version", action="version",
|
|
229
|
+
version=f"%(prog)s {_get_version()}",
|
|
230
|
+
)
|
|
231
|
+
sub = parser.add_subparsers(dest="command")
|
|
232
|
+
|
|
233
|
+
render_p = sub.add_parser(
|
|
234
|
+
"render",
|
|
235
|
+
help="Render a JSON document spec to PDF",
|
|
236
|
+
description="Convert a JSON document specification to a professionally typeset PDF.",
|
|
237
|
+
)
|
|
238
|
+
render_p.add_argument("input", help="JSON spec file path, or '-' for stdin")
|
|
239
|
+
render_p.add_argument("-o", "--output", default="output.pdf", help="Output PDF path (default: output.pdf)")
|
|
240
|
+
render_p.add_argument("-q", "--quiet", action="store_true", help="Suppress status output")
|
|
241
|
+
|
|
242
|
+
schema_p = sub.add_parser(
|
|
243
|
+
"schema",
|
|
244
|
+
help="Export the JSON Schema for LLM prompt engineering",
|
|
245
|
+
description="Generate the JSON Schema that LLMs use to produce valid document specs.",
|
|
246
|
+
)
|
|
247
|
+
schema_p.add_argument("-o", "--output", default=None, help="Write to file instead of stdout")
|
|
248
|
+
schema_p.add_argument("-q", "--quiet", action="store_true")
|
|
249
|
+
|
|
250
|
+
verify_p = sub.add_parser(
|
|
251
|
+
"verify",
|
|
252
|
+
help="Verify an existing PDF's structural integrity",
|
|
253
|
+
)
|
|
254
|
+
verify_p.add_argument("input", help="PDF file path")
|
|
255
|
+
|
|
256
|
+
export_p = sub.add_parser(
|
|
257
|
+
"export",
|
|
258
|
+
help="Export a JSON spec to HTML, Markdown, or Office-ready JSON",
|
|
259
|
+
description="Convert a document spec to alternative formats for preview, editing, or PPTX/DOCX conversion.",
|
|
260
|
+
)
|
|
261
|
+
export_p.add_argument("input", help="JSON spec file path, or '-' for stdin")
|
|
262
|
+
export_p.add_argument(
|
|
263
|
+
"-f", "--format",
|
|
264
|
+
choices=["html", "markdown", "office-json"],
|
|
265
|
+
default="html",
|
|
266
|
+
help="Output format (default: html)",
|
|
267
|
+
)
|
|
268
|
+
export_p.add_argument("-o", "--output", default=None, help="Write to file instead of stdout")
|
|
269
|
+
export_p.add_argument("-q", "--quiet", action="store_true")
|
|
270
|
+
|
|
271
|
+
analyze_p = sub.add_parser(
|
|
272
|
+
"analyze",
|
|
273
|
+
help="Run content intelligence analysis on a document spec",
|
|
274
|
+
description="Detect document type, classify table columns, and report content intelligence.",
|
|
275
|
+
)
|
|
276
|
+
analyze_p.add_argument("input", help="JSON spec file, or '-' for stdin")
|
|
277
|
+
|
|
278
|
+
validate_p = sub.add_parser(
|
|
279
|
+
"validate",
|
|
280
|
+
help="Validate a JSON document spec without rendering",
|
|
281
|
+
)
|
|
282
|
+
validate_p.add_argument("input", help="JSON spec file, or '-' for stdin")
|
|
283
|
+
|
|
284
|
+
args = parser.parse_args(argv)
|
|
285
|
+
|
|
286
|
+
if args.command is None:
|
|
287
|
+
parser.print_help()
|
|
288
|
+
return 0
|
|
289
|
+
|
|
290
|
+
handlers = {
|
|
291
|
+
"render": _render,
|
|
292
|
+
"schema": _schema,
|
|
293
|
+
"verify": _verify,
|
|
294
|
+
"export": _export,
|
|
295
|
+
"analyze": _analyze,
|
|
296
|
+
"validate": _validate,
|
|
297
|
+
}
|
|
298
|
+
return handlers[args.command](args)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _get_version() -> str:
|
|
302
|
+
try:
|
|
303
|
+
from . import __version__
|
|
304
|
+
|
|
305
|
+
return __version__
|
|
306
|
+
except Exception:
|
|
307
|
+
return "0.1.0"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == "__main__":
|
|
311
|
+
sys.exit(main())
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Adapters for external systems and format conversion.
|
|
2
|
+
|
|
3
|
+
pydantic_schema - Pydantic v2 models for LLM structured output
|
|
4
|
+
html_export - Semantic HTML for preview and interchange
|
|
5
|
+
markdown_export - Markdown for LLM round-trips and pandoc conversion
|
|
6
|
+
docx_export - Structured dict for DOCX/PPTX conversion
|
|
7
|
+
"""
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Export a Document to a structured representation for DOCX/PPTX conversion.
|
|
2
|
+
|
|
3
|
+
Rather than depending on python-docx or python-pptx directly (which would
|
|
4
|
+
add heavy dependencies), this module exports a clean intermediate dict
|
|
5
|
+
that any conversion tool can consume:
|
|
6
|
+
|
|
7
|
+
from emboss.adapters.docx_export import to_office_dict
|
|
8
|
+
data = to_office_dict(document)
|
|
9
|
+
# Feed to python-docx, pandoc, or any converter
|
|
10
|
+
|
|
11
|
+
The dict mirrors the Open XML structure closely enough that conversion
|
|
12
|
+
is mechanical, but uses plain Python types so no Office libraries are
|
|
13
|
+
required at export time.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..spec import Document
|
|
22
|
+
|
|
23
|
+
__all__ = ["to_office_dict"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def to_office_dict(document: "Document") -> dict:
|
|
27
|
+
"""Convert a Document to a structured dict for Office format conversion.
|
|
28
|
+
|
|
29
|
+
Returns a dict with:
|
|
30
|
+
- metadata: title, author, subject, keywords, language
|
|
31
|
+
- styles: resolved style presets as CSS-like properties
|
|
32
|
+
- content: ordered list of typed blocks with full formatting
|
|
33
|
+
"""
|
|
34
|
+
from ..spec import (
|
|
35
|
+
BibliographyBlock,
|
|
36
|
+
BulletList,
|
|
37
|
+
Callout,
|
|
38
|
+
Chart,
|
|
39
|
+
CodeBlock,
|
|
40
|
+
Footnote,
|
|
41
|
+
Heading,
|
|
42
|
+
HorizontalRule,
|
|
43
|
+
Image,
|
|
44
|
+
MathBlock,
|
|
45
|
+
PageBreak,
|
|
46
|
+
Paragraph,
|
|
47
|
+
Table,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
sheet = document.stylesheet
|
|
51
|
+
blocks = []
|
|
52
|
+
|
|
53
|
+
for element in document.content:
|
|
54
|
+
if isinstance(element, Heading):
|
|
55
|
+
blocks.append({
|
|
56
|
+
"type": "heading",
|
|
57
|
+
"level": element.level,
|
|
58
|
+
"text": element.text,
|
|
59
|
+
"numbering": element.numbering,
|
|
60
|
+
"runs": _serialize_runs(element.runs),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
elif isinstance(element, Paragraph):
|
|
64
|
+
style = sheet.resolved(sheet.body, element.style)
|
|
65
|
+
blocks.append({
|
|
66
|
+
"type": "paragraph",
|
|
67
|
+
"runs": _serialize_runs(element.runs),
|
|
68
|
+
"align": style.require("align"),
|
|
69
|
+
"indent_first": style.indent_first or 0,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
elif isinstance(element, BulletList):
|
|
73
|
+
items = []
|
|
74
|
+
for item_runs in element.item_runs:
|
|
75
|
+
items.append({"runs": _serialize_runs(item_runs)})
|
|
76
|
+
blocks.append({
|
|
77
|
+
"type": "list",
|
|
78
|
+
"bullet": element.bullet,
|
|
79
|
+
"items": items,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
elif isinstance(element, Table):
|
|
83
|
+
blocks.append(_serialize_table(element))
|
|
84
|
+
|
|
85
|
+
elif isinstance(element, Image):
|
|
86
|
+
blocks.append({
|
|
87
|
+
"type": "image",
|
|
88
|
+
"source": element.source if isinstance(element.source, str) else "<bytes>",
|
|
89
|
+
"alt_text": element.alt_text,
|
|
90
|
+
"width": element.width,
|
|
91
|
+
"height": element.height,
|
|
92
|
+
"caption": element.caption,
|
|
93
|
+
"align": element.align,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
elif isinstance(element, Chart):
|
|
97
|
+
blocks.append({
|
|
98
|
+
"type": "chart",
|
|
99
|
+
"chart_type": element.chart_type,
|
|
100
|
+
"labels": list(element.labels),
|
|
101
|
+
"values": list(element.values),
|
|
102
|
+
"title": element.title,
|
|
103
|
+
"width": element.width,
|
|
104
|
+
"height": element.height,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
elif isinstance(element, Footnote):
|
|
108
|
+
blocks.append({
|
|
109
|
+
"type": "footnote",
|
|
110
|
+
"marker": element.marker,
|
|
111
|
+
"runs": _serialize_runs(element.runs),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
elif isinstance(element, Callout):
|
|
115
|
+
blocks.append({
|
|
116
|
+
"type": "callout",
|
|
117
|
+
"variant": element.variant,
|
|
118
|
+
"title": element.title,
|
|
119
|
+
"runs": _serialize_runs(element.runs),
|
|
120
|
+
"background": element.background,
|
|
121
|
+
"border_color": element.border_color,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
elif isinstance(element, CodeBlock):
|
|
125
|
+
blocks.append({
|
|
126
|
+
"type": "code_block",
|
|
127
|
+
"code": element.code,
|
|
128
|
+
"language": element.language,
|
|
129
|
+
"line_numbers": element.line_numbers,
|
|
130
|
+
"caption": element.caption,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
elif isinstance(element, MathBlock):
|
|
134
|
+
blocks.append({
|
|
135
|
+
"type": "math",
|
|
136
|
+
"source": element.source,
|
|
137
|
+
"display": element.display,
|
|
138
|
+
"caption": element.caption,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
elif isinstance(element, BibliographyBlock):
|
|
142
|
+
from ..bibliography import format_bibliography
|
|
143
|
+
blocks.append({
|
|
144
|
+
"type": "bibliography",
|
|
145
|
+
"title": element.title,
|
|
146
|
+
"heading_level": element.heading_level,
|
|
147
|
+
"entries": format_bibliography(
|
|
148
|
+
element.citations, element.bib_style
|
|
149
|
+
),
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
elif isinstance(element, HorizontalRule):
|
|
153
|
+
blocks.append({"type": "horizontal_rule"})
|
|
154
|
+
|
|
155
|
+
elif isinstance(element, PageBreak):
|
|
156
|
+
blocks.append({"type": "page_break"})
|
|
157
|
+
|
|
158
|
+
page = document.page
|
|
159
|
+
return {
|
|
160
|
+
"metadata": {
|
|
161
|
+
"title": document.title,
|
|
162
|
+
"author": document.author,
|
|
163
|
+
"subject": document.subject,
|
|
164
|
+
"keywords": document.keywords,
|
|
165
|
+
"language": document.language,
|
|
166
|
+
},
|
|
167
|
+
"page": {
|
|
168
|
+
"width_pt": page.width,
|
|
169
|
+
"height_pt": page.height,
|
|
170
|
+
"margin_top_pt": page.margin_top,
|
|
171
|
+
"margin_right_pt": page.margin_right,
|
|
172
|
+
"margin_bottom_pt": page.margin_bottom,
|
|
173
|
+
"margin_left_pt": page.margin_left,
|
|
174
|
+
},
|
|
175
|
+
"content": blocks,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _serialize_runs(runs: list) -> list[dict]:
|
|
180
|
+
return [
|
|
181
|
+
{
|
|
182
|
+
"text": run.text,
|
|
183
|
+
"bold": run.bold,
|
|
184
|
+
"italic": run.italic,
|
|
185
|
+
"font_size": run.font_size,
|
|
186
|
+
"font_family": run.font_family,
|
|
187
|
+
"color": run.color,
|
|
188
|
+
"link": run.link,
|
|
189
|
+
}
|
|
190
|
+
for run in runs
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _serialize_table(table) -> dict:
|
|
195
|
+
headers = []
|
|
196
|
+
for cell in table.header_cells:
|
|
197
|
+
headers.append({
|
|
198
|
+
"text": cell.plain_text,
|
|
199
|
+
"align": cell.align,
|
|
200
|
+
"bold": cell.bold,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
rows = []
|
|
204
|
+
for row in table.body_rows:
|
|
205
|
+
cells = []
|
|
206
|
+
for cell in row:
|
|
207
|
+
cells.append({
|
|
208
|
+
"text": cell.plain_text,
|
|
209
|
+
"align": cell.align,
|
|
210
|
+
"bold": cell.bold,
|
|
211
|
+
"background": cell.background,
|
|
212
|
+
})
|
|
213
|
+
rows.append(cells)
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"type": "table",
|
|
217
|
+
"headers": headers,
|
|
218
|
+
"rows": rows,
|
|
219
|
+
"caption": table.caption,
|
|
220
|
+
"stripe": table.stripe,
|
|
221
|
+
}
|