simple-resume 0.1.9__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.
- simple_resume/__init__.py +132 -0
- simple_resume/core/__init__.py +47 -0
- simple_resume/core/colors.py +215 -0
- simple_resume/core/config.py +672 -0
- simple_resume/core/constants/__init__.py +207 -0
- simple_resume/core/constants/colors.py +98 -0
- simple_resume/core/constants/files.py +28 -0
- simple_resume/core/constants/layout.py +58 -0
- simple_resume/core/dependencies.py +258 -0
- simple_resume/core/effects.py +154 -0
- simple_resume/core/exceptions.py +261 -0
- simple_resume/core/file_operations.py +68 -0
- simple_resume/core/generate/__init__.py +21 -0
- simple_resume/core/generate/exceptions.py +69 -0
- simple_resume/core/generate/html.py +233 -0
- simple_resume/core/generate/pdf.py +659 -0
- simple_resume/core/generate/plan.py +131 -0
- simple_resume/core/hydration.py +55 -0
- simple_resume/core/importers/__init__.py +3 -0
- simple_resume/core/importers/json_resume.py +284 -0
- simple_resume/core/latex/__init__.py +60 -0
- simple_resume/core/latex/context.py +56 -0
- simple_resume/core/latex/conversion.py +227 -0
- simple_resume/core/latex/escaping.py +68 -0
- simple_resume/core/latex/fonts.py +93 -0
- simple_resume/core/latex/formatting.py +81 -0
- simple_resume/core/latex/sections.py +218 -0
- simple_resume/core/latex/types.py +84 -0
- simple_resume/core/markdown.py +127 -0
- simple_resume/core/models.py +102 -0
- simple_resume/core/palettes/__init__.py +38 -0
- simple_resume/core/palettes/common.py +73 -0
- simple_resume/core/palettes/data/default_palettes.json +58 -0
- simple_resume/core/palettes/exceptions.py +33 -0
- simple_resume/core/palettes/fetch_types.py +52 -0
- simple_resume/core/palettes/generators.py +137 -0
- simple_resume/core/palettes/registry.py +76 -0
- simple_resume/core/palettes/resolution.py +123 -0
- simple_resume/core/palettes/sources.py +162 -0
- simple_resume/core/paths.py +21 -0
- simple_resume/core/protocols.py +134 -0
- simple_resume/core/py.typed +0 -0
- simple_resume/core/render/__init__.py +37 -0
- simple_resume/core/render/manage.py +199 -0
- simple_resume/core/render/plan.py +405 -0
- simple_resume/core/result.py +226 -0
- simple_resume/core/resume.py +609 -0
- simple_resume/core/skills.py +60 -0
- simple_resume/core/validation.py +321 -0
- simple_resume/py.typed +0 -0
- simple_resume/shell/__init__.py +3 -0
- simple_resume/shell/assets/static/css/README.md +213 -0
- simple_resume/shell/assets/static/css/common.css +641 -0
- simple_resume/shell/assets/static/css/fonts.css +42 -0
- simple_resume/shell/assets/static/css/preview.css +82 -0
- simple_resume/shell/assets/static/css/print.css +99 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Book.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Light.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Medium.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Oblique.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Roman.otf +0 -0
- simple_resume/shell/assets/static/fonts/fontawesome/Font Awesome 6 Brands-Regular-400.otf +0 -0
- simple_resume/shell/assets/static/fonts/fontawesome/Font Awesome 6 Free-Solid-900.otf +0 -0
- simple_resume/shell/assets/static/images/default_profile_1.jpg +0 -0
- simple_resume/shell/assets/static/images/default_profile_2.png +0 -0
- simple_resume/shell/assets/static/schema.json +236 -0
- simple_resume/shell/assets/static/themes/README.md +208 -0
- simple_resume/shell/assets/static/themes/bold.yaml +64 -0
- simple_resume/shell/assets/static/themes/classic.yaml +64 -0
- simple_resume/shell/assets/static/themes/executive.yaml +64 -0
- simple_resume/shell/assets/static/themes/minimal.yaml +64 -0
- simple_resume/shell/assets/static/themes/modern.yaml +64 -0
- simple_resume/shell/assets/templates/html/cover.html +129 -0
- simple_resume/shell/assets/templates/html/demo.html +13 -0
- simple_resume/shell/assets/templates/html/resume_base.html +453 -0
- simple_resume/shell/assets/templates/html/resume_no_bars.html +316 -0
- simple_resume/shell/assets/templates/html/resume_with_bars.html +362 -0
- simple_resume/shell/cli/__init__.py +35 -0
- simple_resume/shell/cli/main.py +975 -0
- simple_resume/shell/cli/palette.py +75 -0
- simple_resume/shell/cli/random_palette_demo.py +407 -0
- simple_resume/shell/config.py +96 -0
- simple_resume/shell/effect_executor.py +211 -0
- simple_resume/shell/file_opener.py +308 -0
- simple_resume/shell/generate/__init__.py +37 -0
- simple_resume/shell/generate/core.py +650 -0
- simple_resume/shell/generate/lazy.py +284 -0
- simple_resume/shell/io_utils.py +199 -0
- simple_resume/shell/palettes/__init__.py +1 -0
- simple_resume/shell/palettes/fetch.py +63 -0
- simple_resume/shell/palettes/loader.py +321 -0
- simple_resume/shell/palettes/remote.py +179 -0
- simple_resume/shell/pdf_executor.py +52 -0
- simple_resume/shell/py.typed +0 -0
- simple_resume/shell/render/__init__.py +1 -0
- simple_resume/shell/render/latex.py +308 -0
- simple_resume/shell/render/operations.py +240 -0
- simple_resume/shell/resume_extensions.py +737 -0
- simple_resume/shell/runtime/__init__.py +7 -0
- simple_resume/shell/runtime/content.py +190 -0
- simple_resume/shell/runtime/generate.py +497 -0
- simple_resume/shell/runtime/lazy.py +138 -0
- simple_resume/shell/runtime/lazy_import.py +173 -0
- simple_resume/shell/service_locator.py +80 -0
- simple_resume/shell/services.py +256 -0
- simple_resume/shell/session/__init__.py +6 -0
- simple_resume/shell/session/config.py +35 -0
- simple_resume/shell/session/manage.py +386 -0
- simple_resume/shell/strategies.py +181 -0
- simple_resume/shell/themes/__init__.py +35 -0
- simple_resume/shell/themes/loader.py +230 -0
- simple_resume-0.1.9.dist-info/METADATA +201 -0
- simple_resume-0.1.9.dist-info/RECORD +116 -0
- simple_resume-0.1.9.dist-info/WHEEL +4 -0
- simple_resume-0.1.9.dist-info/entry_points.txt +5 -0
- simple_resume-0.1.9.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
"""Provide PDF rendering helpers for the core resume pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
import simple_resume.core.latex.types as latex_types
|
|
10
|
+
from simple_resume.core.constants import RenderMode
|
|
11
|
+
from simple_resume.core.effects import Effect, MakeDirectory, RenderPdf, WriteFile
|
|
12
|
+
from simple_resume.core.exceptions import ConfigurationError
|
|
13
|
+
from simple_resume.core.generate.exceptions import TemplateError
|
|
14
|
+
from simple_resume.core.latex.types import LatexGenerationContext
|
|
15
|
+
from simple_resume.core.models import RenderPlan
|
|
16
|
+
from simple_resume.core.protocols import EffectExecutor, LaTeXRenderer, TemplateLocator
|
|
17
|
+
from simple_resume.core.render import get_template_environment
|
|
18
|
+
from simple_resume.core.result import GenerationMetadata, GenerationResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class _PdfGenerationParams:
|
|
23
|
+
"""Parameters for PDF generation to reduce function argument count."""
|
|
24
|
+
|
|
25
|
+
render_plan: RenderPlan
|
|
26
|
+
output_path: Path
|
|
27
|
+
resume_name: str
|
|
28
|
+
filename: str | None = None
|
|
29
|
+
template_locator: TemplateLocator | None = None
|
|
30
|
+
latex_renderer: LaTeXRenderer | None = None
|
|
31
|
+
effect_executor: EffectExecutor | None = None
|
|
32
|
+
existing_context: LatexGenerationContext | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_latex_functions(latex_renderer: LaTeXRenderer) -> tuple[Any, Any, Any]:
|
|
36
|
+
"""Get LaTeX functions from provided renderer.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
latex_renderer: LaTeX renderer implementation
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Tuple of (LatexCompilationError, compile_tex_to_pdf,
|
|
43
|
+
render_resume_latex_from_data)
|
|
44
|
+
Returns (None, None, None) if LaTeX module is not available.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
return latex_renderer.get_latex_functions()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PdfGeneratorFactory:
|
|
51
|
+
"""Factory for creating PDF generation functions with configured dependencies."""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
effect_executor: EffectExecutor | None = None,
|
|
56
|
+
template_locator: TemplateLocator | None = None,
|
|
57
|
+
latex_renderer: LaTeXRenderer | None = None,
|
|
58
|
+
):
|
|
59
|
+
"""Initialize factory with optional default dependencies.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
effect_executor: Default effect executor to use when none is injected
|
|
63
|
+
template_locator: Default template locator to use when none is injected
|
|
64
|
+
latex_renderer: Default LaTeX renderer to use when none is injected
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
self._effect_executor = effect_executor
|
|
68
|
+
self._template_locator = template_locator
|
|
69
|
+
self._latex_renderer = latex_renderer
|
|
70
|
+
|
|
71
|
+
def _get_effect_executor(self, injected: EffectExecutor | None) -> EffectExecutor:
|
|
72
|
+
"""Get effect executor, preferring injected over default."""
|
|
73
|
+
if injected is not None:
|
|
74
|
+
return injected
|
|
75
|
+
if self._effect_executor is not None:
|
|
76
|
+
return self._effect_executor
|
|
77
|
+
raise ConfigurationError(
|
|
78
|
+
"No effect executor available. "
|
|
79
|
+
"Either inject one or ensure factory is configured with a default."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _get_template_locator(
|
|
83
|
+
self, injected: TemplateLocator | None
|
|
84
|
+
) -> TemplateLocator:
|
|
85
|
+
"""Get template locator, preferring injected over default."""
|
|
86
|
+
if injected is not None:
|
|
87
|
+
return injected
|
|
88
|
+
if self._template_locator is not None:
|
|
89
|
+
return self._template_locator
|
|
90
|
+
raise ConfigurationError(
|
|
91
|
+
"No template locator available. "
|
|
92
|
+
"Inject one or configure the factory with a default."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _get_latex_renderer(self, injected: LaTeXRenderer | None) -> LaTeXRenderer:
|
|
96
|
+
"""Get LaTeX renderer, preferring injected over default."""
|
|
97
|
+
if injected is not None:
|
|
98
|
+
return injected
|
|
99
|
+
if self._latex_renderer is not None:
|
|
100
|
+
return self._latex_renderer
|
|
101
|
+
raise ConfigurationError(
|
|
102
|
+
"No LaTeX renderer available. "
|
|
103
|
+
"Inject one or configure the factory with a default."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def create_prepare_pdf_with_weasyprint_function(
|
|
107
|
+
self,
|
|
108
|
+
) -> Callable[..., tuple[str, list[Effect], GenerationMetadata]]:
|
|
109
|
+
"""Create a prepare_pdf_with_weasyprint function with factory's dependencies.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
A function that takes (render_plan, output_path, **kwargs) and returns
|
|
113
|
+
(html_content, effects, metadata)
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
factory = self
|
|
117
|
+
|
|
118
|
+
def prepare_pdf_with_weasyprint(
|
|
119
|
+
render_plan: RenderPlan,
|
|
120
|
+
output_path: Path,
|
|
121
|
+
*,
|
|
122
|
+
resume_name: str,
|
|
123
|
+
filename: str | None = None,
|
|
124
|
+
template_locator: TemplateLocator | None = None,
|
|
125
|
+
) -> tuple[str, list[Effect], GenerationMetadata]:
|
|
126
|
+
"""Prepare PDF generation using WeasyPrint (pure function).
|
|
127
|
+
|
|
128
|
+
This function performs NO I/O operations. It prepares HTML content and
|
|
129
|
+
returns a list of effects that the shell layer should execute.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
render_plan: Rendering configuration and context
|
|
133
|
+
output_path: Target PDF file path
|
|
134
|
+
resume_name: Name of the resume
|
|
135
|
+
filename: Source filename for error messages
|
|
136
|
+
template_locator: Optional template locator for dependency injection
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Tuple of (html_content, effects, metadata)
|
|
140
|
+
- html_content: Rendered HTML as string
|
|
141
|
+
- effects: List of effects to execute (MakeDirectory, WriteFile)
|
|
142
|
+
- metadata: Generation metadata
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
TemplateError: If render plan is invalid or uses LaTeX mode
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
params = _PdfGenerationParams(
|
|
149
|
+
render_plan=render_plan,
|
|
150
|
+
output_path=output_path,
|
|
151
|
+
resume_name=resume_name,
|
|
152
|
+
filename=filename,
|
|
153
|
+
template_locator=template_locator,
|
|
154
|
+
)
|
|
155
|
+
return _prepare_pdf_with_weasyprint_impl(
|
|
156
|
+
params,
|
|
157
|
+
factory,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return prepare_pdf_with_weasyprint
|
|
161
|
+
|
|
162
|
+
def create_generate_pdf_with_weasyprint_function(
|
|
163
|
+
self,
|
|
164
|
+
) -> Callable[..., tuple[GenerationResult, int | None]]:
|
|
165
|
+
"""Create a generate_pdf_with_weasyprint function with factory's dependencies.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
A function that takes (render_plan, output_path, **kwargs) and returns
|
|
169
|
+
(result, page_count)
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
factory = self
|
|
173
|
+
|
|
174
|
+
def generate_pdf_with_weasyprint(
|
|
175
|
+
render_plan: RenderPlan,
|
|
176
|
+
output_path: Path,
|
|
177
|
+
*,
|
|
178
|
+
resume_name: str,
|
|
179
|
+
filename: str | None = None,
|
|
180
|
+
effect_executor: EffectExecutor | None = None,
|
|
181
|
+
) -> tuple[GenerationResult, int | None]:
|
|
182
|
+
"""Generate PDF using WeasyPrint (shell execution).
|
|
183
|
+
|
|
184
|
+
This function executes effects produced by prepare_pdf_with_weasyprint
|
|
185
|
+
and returns result and page count.
|
|
186
|
+
"""
|
|
187
|
+
# Prepare PDF generation in core (pure)
|
|
188
|
+
html_content, effects, metadata = (
|
|
189
|
+
factory.create_prepare_pdf_with_weasyprint_function()(
|
|
190
|
+
render_plan=render_plan,
|
|
191
|
+
output_path=output_path,
|
|
192
|
+
resume_name=resume_name,
|
|
193
|
+
filename=filename,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Execute effects using injected or default executor
|
|
198
|
+
executor = factory._get_effect_executor(effect_executor)
|
|
199
|
+
page_count: int | None = None
|
|
200
|
+
for effect in effects:
|
|
201
|
+
result = executor.execute(effect)
|
|
202
|
+
if isinstance(effect, RenderPdf) and isinstance(result, int):
|
|
203
|
+
page_count = result
|
|
204
|
+
|
|
205
|
+
# Create and return result
|
|
206
|
+
result = GenerationResult(
|
|
207
|
+
output_path=output_path,
|
|
208
|
+
format_type="pdf",
|
|
209
|
+
metadata=metadata,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return result, page_count
|
|
213
|
+
|
|
214
|
+
return generate_pdf_with_weasyprint
|
|
215
|
+
|
|
216
|
+
def create_prepare_pdf_with_latex_function(
|
|
217
|
+
self,
|
|
218
|
+
) -> Callable[..., tuple[str, list[Effect], GenerationMetadata]]:
|
|
219
|
+
"""Create a prepare_pdf_with_latex function with factory's dependencies.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
A function that takes (render_plan, output_path, **kwargs) and returns
|
|
223
|
+
(tex_content, effects, metadata)
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
factory = self
|
|
227
|
+
|
|
228
|
+
def prepare_pdf_with_latex(
|
|
229
|
+
render_plan: RenderPlan,
|
|
230
|
+
output_path: Path,
|
|
231
|
+
config: PdfGenerationConfig,
|
|
232
|
+
) -> tuple[str, list[Effect], GenerationMetadata]:
|
|
233
|
+
"""Prepare PDF generation using LaTeX (pure function).
|
|
234
|
+
|
|
235
|
+
This function performs NO I/O operations. It prepares LaTeX content and
|
|
236
|
+
returns a list of effects that the shell layer should execute.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
render_plan: Rendering configuration and context
|
|
240
|
+
output_path: Target PDF file path
|
|
241
|
+
config: Configuration for PDF generation including resume details
|
|
242
|
+
and dependencies
|
|
243
|
+
resume_name: Name of the resume
|
|
244
|
+
filename: Source filename for error messages
|
|
245
|
+
template_locator: Optional template locator for dependency injection
|
|
246
|
+
latex_renderer: Optional LaTeX renderer for dependency injection
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Tuple of (tex_content, effects, metadata)
|
|
250
|
+
- tex_content: Rendered LaTeX as string
|
|
251
|
+
- effects: List of effects to execute (MakeDirectory, WriteFile)
|
|
252
|
+
- metadata: Generation metadata
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
TemplateError: If render plan is invalid or uses HTML mode
|
|
256
|
+
LaTeXCompilationError: If LaTeX compilation fails
|
|
257
|
+
FileNotFoundError: If required files are missing
|
|
258
|
+
|
|
259
|
+
"""
|
|
260
|
+
params = _PdfGenerationParams(
|
|
261
|
+
render_plan=render_plan,
|
|
262
|
+
output_path=output_path,
|
|
263
|
+
resume_name=config.resume_name,
|
|
264
|
+
filename=config.filename,
|
|
265
|
+
template_locator=config.template_locator,
|
|
266
|
+
latex_renderer=config.latex_renderer,
|
|
267
|
+
)
|
|
268
|
+
return _prepare_pdf_with_latex_impl(
|
|
269
|
+
params,
|
|
270
|
+
factory,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return prepare_pdf_with_latex
|
|
274
|
+
|
|
275
|
+
def create_generate_pdf_with_latex_function(
|
|
276
|
+
self,
|
|
277
|
+
) -> Callable[..., tuple[GenerationResult, int | None]]:
|
|
278
|
+
"""Create a generate_pdf_with_latex function with factory's dependencies.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
A function that takes (render_plan, output_path, **kwargs) and returns
|
|
282
|
+
(result, page_count)
|
|
283
|
+
|
|
284
|
+
"""
|
|
285
|
+
factory = self
|
|
286
|
+
|
|
287
|
+
def generate_pdf_with_latex( # noqa: PLR0913
|
|
288
|
+
render_plan: RenderPlan,
|
|
289
|
+
output_path: Path,
|
|
290
|
+
*,
|
|
291
|
+
resume_name: str,
|
|
292
|
+
filename: str | None = None,
|
|
293
|
+
template_locator: TemplateLocator | None = None,
|
|
294
|
+
latex_renderer: LaTeXRenderer | None = None,
|
|
295
|
+
effect_executor: EffectExecutor | None = None,
|
|
296
|
+
) -> tuple[GenerationResult, int | None]:
|
|
297
|
+
"""Generate PDF using LaTeX (shell execution).
|
|
298
|
+
|
|
299
|
+
This function executes effects produced by prepare_pdf_with_latex
|
|
300
|
+
and returns result and page count.
|
|
301
|
+
"""
|
|
302
|
+
# Prepare PDF generation in core (pure)
|
|
303
|
+
tex_content, effects, metadata = (
|
|
304
|
+
factory.create_prepare_pdf_with_latex_function()(
|
|
305
|
+
render_plan=render_plan,
|
|
306
|
+
output_path=output_path,
|
|
307
|
+
resume_name=resume_name,
|
|
308
|
+
filename=filename,
|
|
309
|
+
template_locator=template_locator,
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Execute effects using injected or default executor
|
|
314
|
+
executor = factory._get_effect_executor(effect_executor)
|
|
315
|
+
executor.execute_many(effects)
|
|
316
|
+
|
|
317
|
+
# Extract page count from prepared metadata
|
|
318
|
+
page_count = (
|
|
319
|
+
metadata.page_count if hasattr(metadata, "page_count") else None
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Create and return result
|
|
323
|
+
result = GenerationResult(
|
|
324
|
+
output_path=output_path,
|
|
325
|
+
format_type="pdf",
|
|
326
|
+
metadata=metadata,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
return result, page_count
|
|
330
|
+
|
|
331
|
+
return generate_pdf_with_latex
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _prepare_pdf_with_weasyprint_impl(
|
|
335
|
+
params: _PdfGenerationParams,
|
|
336
|
+
factory: PdfGeneratorFactory,
|
|
337
|
+
) -> tuple[str, list[Effect], GenerationMetadata]:
|
|
338
|
+
"""Implement WeasyPrint PDF generation using factory for dependencies.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
params: Parameters containing render_plan, output_path, resume_name,
|
|
342
|
+
filename, and template_locator
|
|
343
|
+
factory: PDF generator factory for dependency resolution
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Tuple of (html_content, effects, metadata)
|
|
347
|
+
|
|
348
|
+
Raises:
|
|
349
|
+
TemplateError: If render plan is invalid or uses LaTeX mode
|
|
350
|
+
|
|
351
|
+
"""
|
|
352
|
+
if params.render_plan.mode is RenderMode.LATEX:
|
|
353
|
+
raise TemplateError(
|
|
354
|
+
"LaTeX mode not supported in WeasyPrint generation method",
|
|
355
|
+
template_name="latex",
|
|
356
|
+
filename=params.filename,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if not params.render_plan.context or not params.render_plan.template_name:
|
|
360
|
+
raise TemplateError(
|
|
361
|
+
"HTML plan missing context or template_name",
|
|
362
|
+
filename=params.filename,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Resolve template location using factory
|
|
366
|
+
locator = factory._get_template_locator(params.template_locator)
|
|
367
|
+
template_loc = locator.get_template_location()
|
|
368
|
+
env = get_template_environment(str(template_loc))
|
|
369
|
+
html = (
|
|
370
|
+
env.get_template(params.render_plan.template_name)
|
|
371
|
+
.render(**params.render_plan.context)
|
|
372
|
+
.lstrip()
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Pure operations: Prepare CSS for page size
|
|
376
|
+
page_width = params.render_plan.config.page_width or 210
|
|
377
|
+
page_height = params.render_plan.config.page_height or 297
|
|
378
|
+
css_string = f"@page {{size: {page_width}mm {page_height}mm; margin: 0mm;}}"
|
|
379
|
+
|
|
380
|
+
# Create effects for shell execution (no PDF rendering here)
|
|
381
|
+
# Use static/css as base_url so font paths like "../fonts/AvenirLTStd-Light.otf"
|
|
382
|
+
# resolve correctly to assets/static/fonts/AvenirLTStd-Light.otf
|
|
383
|
+
css_base_url = template_loc.parent / "static" / "css"
|
|
384
|
+
effects: list[Effect] = [
|
|
385
|
+
MakeDirectory(path=params.output_path.parent, parents=True),
|
|
386
|
+
RenderPdf(
|
|
387
|
+
html=html,
|
|
388
|
+
css=css_string,
|
|
389
|
+
output_path=params.output_path,
|
|
390
|
+
base_url=str(css_base_url),
|
|
391
|
+
),
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
# Create metadata
|
|
395
|
+
metadata = GenerationMetadata(
|
|
396
|
+
format_type="pdf",
|
|
397
|
+
template_name=params.render_plan.template_name or "unknown",
|
|
398
|
+
generation_time=0.0,
|
|
399
|
+
file_size=0,
|
|
400
|
+
resume_name=params.resume_name,
|
|
401
|
+
palette_info=params.render_plan.palette_metadata,
|
|
402
|
+
page_count=None,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
return html, effects, metadata
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _prepare_pdf_with_latex_impl(
|
|
409
|
+
params: _PdfGenerationParams,
|
|
410
|
+
factory: PdfGeneratorFactory,
|
|
411
|
+
) -> tuple[str, list[Effect], GenerationMetadata]:
|
|
412
|
+
"""Implement LaTeX PDF generation using factory for dependencies.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
params: Parameters containing render_plan, output_path, resume_name,
|
|
416
|
+
filename, template_locator, and latex_renderer
|
|
417
|
+
factory: PDF generator factory for dependency resolution
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Tuple of (tex_content, effects, metadata)
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
ConfigurationError: If LaTeX renderer unavailable
|
|
424
|
+
|
|
425
|
+
"""
|
|
426
|
+
# Check latex renderer availability
|
|
427
|
+
renderer = factory._get_latex_renderer(params.latex_renderer)
|
|
428
|
+
LatexCompilationError, compile_tex_to_pdf, render_resume_latex_from_data = (
|
|
429
|
+
get_latex_functions(renderer)
|
|
430
|
+
)
|
|
431
|
+
if any(
|
|
432
|
+
func is None
|
|
433
|
+
for func in (
|
|
434
|
+
LatexCompilationError,
|
|
435
|
+
compile_tex_to_pdf,
|
|
436
|
+
render_resume_latex_from_data,
|
|
437
|
+
)
|
|
438
|
+
):
|
|
439
|
+
raise ConfigurationError(
|
|
440
|
+
"LaTeX renderer unavailable. "
|
|
441
|
+
"Inject a renderer that provides compilation functions."
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Prepare LaTeX generation context and resolve paths.
|
|
445
|
+
if params.existing_context is not None:
|
|
446
|
+
context = params.existing_context
|
|
447
|
+
elif latex_types.LatexGenerationContext.last_context is not None:
|
|
448
|
+
context = latex_types.LatexGenerationContext.last_context
|
|
449
|
+
else:
|
|
450
|
+
context = LatexGenerationContext(
|
|
451
|
+
resume_data=params.render_plan.context,
|
|
452
|
+
processed_data=params.render_plan.context or {},
|
|
453
|
+
output_path=params.output_path,
|
|
454
|
+
base_path=params.render_plan.base_path,
|
|
455
|
+
filename=params.filename,
|
|
456
|
+
)
|
|
457
|
+
resolved_paths = context.paths
|
|
458
|
+
if resolved_paths is None:
|
|
459
|
+
# Strictly require resolved paths to avoid implicit template resolution.
|
|
460
|
+
# Tests expect a configuration error when paths are missing so we fail
|
|
461
|
+
# fast instead of attempting to fall back to packaged assets.
|
|
462
|
+
raise ConfigurationError(
|
|
463
|
+
"LaTeX generation requires resolved paths (templates/static). "
|
|
464
|
+
"Provide Paths or configure the shell layer before rendering."
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Generate LaTeX content
|
|
468
|
+
try:
|
|
469
|
+
resume_data = (
|
|
470
|
+
context.processed_data
|
|
471
|
+
if isinstance(context.processed_data, dict)
|
|
472
|
+
else context.resume_data
|
|
473
|
+
if isinstance(context.resume_data, dict)
|
|
474
|
+
else params.render_plan.context or {}
|
|
475
|
+
)
|
|
476
|
+
tex_result = render_resume_latex_from_data(
|
|
477
|
+
resume_data,
|
|
478
|
+
paths=resolved_paths,
|
|
479
|
+
template_name=params.render_plan.template_name or "latex/basic.tex",
|
|
480
|
+
)
|
|
481
|
+
tex_content = getattr(tex_result, "tex", tex_result)
|
|
482
|
+
except Exception as exc:
|
|
483
|
+
if "No such file or directory" in str(exc):
|
|
484
|
+
raise FileNotFoundError(
|
|
485
|
+
f"Template not found: {resolved_paths.templates}"
|
|
486
|
+
) from exc
|
|
487
|
+
raise
|
|
488
|
+
|
|
489
|
+
# Resolve paths for effects
|
|
490
|
+
# resolved_paths is guaranteed by earlier guard
|
|
491
|
+
|
|
492
|
+
# Prepare effects for shell execution
|
|
493
|
+
effects: list[Effect] = [
|
|
494
|
+
MakeDirectory(path=params.output_path.parent, parents=True),
|
|
495
|
+
WriteFile(
|
|
496
|
+
path=params.output_path.with_suffix(".tex"),
|
|
497
|
+
content=tex_content,
|
|
498
|
+
encoding="utf-8",
|
|
499
|
+
),
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
# Create metadata
|
|
503
|
+
metadata = GenerationMetadata(
|
|
504
|
+
format_type="pdf",
|
|
505
|
+
template_name=params.render_plan.template_name or "latex",
|
|
506
|
+
generation_time=0.0,
|
|
507
|
+
file_size=len(tex_content.encode("utf-8")),
|
|
508
|
+
resume_name=params.resume_name,
|
|
509
|
+
palette_info=params.render_plan.palette_metadata,
|
|
510
|
+
page_count=context.metadata.page_count
|
|
511
|
+
if hasattr(context.metadata, "page_count")
|
|
512
|
+
else None,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
return tex_content, effects, metadata
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def prepare_pdf_with_weasyprint(
|
|
519
|
+
render_plan: RenderPlan,
|
|
520
|
+
output_path: Path,
|
|
521
|
+
*,
|
|
522
|
+
resume_name: str,
|
|
523
|
+
filename: str | None = None,
|
|
524
|
+
template_locator: TemplateLocator | None = None,
|
|
525
|
+
) -> tuple[str, list[Effect], GenerationMetadata]:
|
|
526
|
+
"""Prepare PDF generation using WeasyPrint (pure function).
|
|
527
|
+
|
|
528
|
+
This function performs NO I/O operations. It prepares HTML content and
|
|
529
|
+
returns a list of effects that the shell layer should execute.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
render_plan: Rendering configuration and context
|
|
533
|
+
output_path: Target PDF file path
|
|
534
|
+
resume_name: Name of the resume
|
|
535
|
+
filename: Source filename for error messages
|
|
536
|
+
template_locator: Optional template locator for dependency injection
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Tuple of (html_content, effects, metadata)
|
|
540
|
+
- html_content: Rendered HTML as string
|
|
541
|
+
- effects: List of effects to execute (MakeDirectory, WriteFile)
|
|
542
|
+
- metadata: Generation metadata
|
|
543
|
+
|
|
544
|
+
Raises:
|
|
545
|
+
TemplateError: If render plan is invalid or uses LaTeX mode
|
|
546
|
+
|
|
547
|
+
"""
|
|
548
|
+
factory = PdfGeneratorFactory()
|
|
549
|
+
params = _PdfGenerationParams(
|
|
550
|
+
render_plan=render_plan,
|
|
551
|
+
output_path=output_path,
|
|
552
|
+
resume_name=resume_name,
|
|
553
|
+
filename=filename,
|
|
554
|
+
template_locator=template_locator,
|
|
555
|
+
)
|
|
556
|
+
return _prepare_pdf_with_weasyprint_impl(params, factory)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class PdfGenerationConfig:
|
|
560
|
+
"""Configuration for PDF generation functions."""
|
|
561
|
+
|
|
562
|
+
def __init__(
|
|
563
|
+
self,
|
|
564
|
+
*,
|
|
565
|
+
resume_name: str,
|
|
566
|
+
filename: str | None = None,
|
|
567
|
+
template_locator: TemplateLocator | None = None,
|
|
568
|
+
latex_renderer: LaTeXRenderer | None = None,
|
|
569
|
+
effect_executor: EffectExecutor | None = None,
|
|
570
|
+
):
|
|
571
|
+
self.resume_name = resume_name
|
|
572
|
+
self.filename = filename
|
|
573
|
+
self.template_locator = template_locator
|
|
574
|
+
self.latex_renderer = latex_renderer
|
|
575
|
+
self.effect_executor = effect_executor
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def prepare_pdf_with_latex(
|
|
579
|
+
render_plan: RenderPlan,
|
|
580
|
+
output_path: Path,
|
|
581
|
+
config: PdfGenerationConfig | LatexGenerationContext,
|
|
582
|
+
) -> tuple[str, list[Effect], GenerationMetadata]:
|
|
583
|
+
"""Prepare PDF generation using LaTeX (pure function).
|
|
584
|
+
|
|
585
|
+
This function performs NO I/O operations. It prepares TeX content and
|
|
586
|
+
returns a list of effects that the shell layer should execute.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
render_plan: Rendering configuration and context
|
|
590
|
+
output_path: Target PDF file path
|
|
591
|
+
config: PDF generation configuration
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
Tuple of (tex_content, effects, metadata)
|
|
595
|
+
- tex_content: Rendered TeX source code
|
|
596
|
+
- effects: List of effects to execute
|
|
597
|
+
- metadata: Generation metadata
|
|
598
|
+
|
|
599
|
+
Raises:
|
|
600
|
+
ConfigurationError: If paths is None or LaTeX renderer unavailable
|
|
601
|
+
|
|
602
|
+
"""
|
|
603
|
+
factory = PdfGeneratorFactory()
|
|
604
|
+
if isinstance(config, LatexGenerationContext):
|
|
605
|
+
params = _PdfGenerationParams(
|
|
606
|
+
render_plan=render_plan,
|
|
607
|
+
output_path=output_path,
|
|
608
|
+
resume_name=render_plan.name,
|
|
609
|
+
filename=config.filename,
|
|
610
|
+
existing_context=config,
|
|
611
|
+
)
|
|
612
|
+
else:
|
|
613
|
+
params = _PdfGenerationParams(
|
|
614
|
+
render_plan=render_plan,
|
|
615
|
+
output_path=output_path,
|
|
616
|
+
resume_name=config.resume_name,
|
|
617
|
+
filename=config.filename,
|
|
618
|
+
template_locator=config.template_locator,
|
|
619
|
+
latex_renderer=config.latex_renderer,
|
|
620
|
+
effect_executor=config.effect_executor,
|
|
621
|
+
)
|
|
622
|
+
return _prepare_pdf_with_latex_impl(
|
|
623
|
+
params,
|
|
624
|
+
factory,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def generate_pdf_with_weasyprint(
|
|
629
|
+
render_plan: RenderPlan,
|
|
630
|
+
output_path: Path,
|
|
631
|
+
config: PdfGenerationConfig,
|
|
632
|
+
) -> tuple[GenerationResult, int | None]:
|
|
633
|
+
"""Generate PDF using WeasyPrint (shell execution).
|
|
634
|
+
|
|
635
|
+
This function executes the effects produced by prepare_pdf_with_weasyprint
|
|
636
|
+
and returns the result and page count.
|
|
637
|
+
"""
|
|
638
|
+
factory = PdfGeneratorFactory(
|
|
639
|
+
effect_executor=config.effect_executor,
|
|
640
|
+
template_locator=config.template_locator,
|
|
641
|
+
latex_renderer=config.latex_renderer,
|
|
642
|
+
)
|
|
643
|
+
generator = factory.create_generate_pdf_with_weasyprint_function()
|
|
644
|
+
return generator(
|
|
645
|
+
render_plan=render_plan,
|
|
646
|
+
output_path=output_path,
|
|
647
|
+
resume_name=config.resume_name,
|
|
648
|
+
filename=config.filename,
|
|
649
|
+
effect_executor=config.effect_executor,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
__all__ = [
|
|
654
|
+
"PdfGeneratorFactory",
|
|
655
|
+
"prepare_pdf_with_weasyprint",
|
|
656
|
+
"prepare_pdf_with_latex",
|
|
657
|
+
"generate_pdf_with_weasyprint",
|
|
658
|
+
"get_latex_functions",
|
|
659
|
+
]
|