Allow URLs for cv.photo field

This commit is contained in:
Sina Atalay
2026-03-02 17:35:30 +03:00
parent 78d359f0d7
commit b4bea9e948
5 changed files with 56 additions and 11 deletions

View File

@@ -99,14 +99,11 @@ def copy_photo_next_to_typst_file(
rendercv_model: CV model containing photo path.
typst_path: Path to Typst source file.
"""
if rendercv_model.cv.photo:
photo_path = rendercv_model.cv.photo
photo_path = rendercv_model.cv.photo
if isinstance(photo_path, pathlib.Path):
copy_to = typst_path.parent / photo_path.name
if photo_path != copy_to:
shutil.copy(
rendercv_model.cv.photo,
typst_path.parent / rendercv_model.cv.photo.name,
)
shutil.copy(photo_path, copy_to)
@functools.lru_cache(maxsize=1)

View File

@@ -1,6 +1,10 @@
import pathlib
import urllib.parse
import urllib.request
from collections.abc import Callable
from typing import Literal
from rendercv.exception import RenderCVUserError
from rendercv.schema.models.cv.section import Entry
from rendercv.schema.models.rendercv_model import RenderCVModel
@@ -16,6 +20,44 @@ from .string_processor import (
)
def download_photo_from_url(rendercv_model: RenderCVModel) -> None:
"""Download photo from URL to output directory and update model to local path.
Why:
Templates and Typst compiler require cv.photo to be a local pathlib.Path.
When user provides a URL, this downloads the image before template
rendering, preserving the local-path invariant for all downstream code.
Args:
rendercv_model: CV model whose photo URL will be downloaded in-place.
"""
if rendercv_model.cv.photo is None or isinstance(
rendercv_model.cv.photo, pathlib.Path
):
return
url_str = str(rendercv_model.cv.photo)
parsed = urllib.parse.urlparse(url_str)
filename = pathlib.PurePosixPath(parsed.path).name
if not filename or "." not in filename:
filename = "photo.jpg"
output_dir = rendercv_model.settings.render_command.output_folder
output_dir.mkdir(parents=True, exist_ok=True)
destination = output_dir / filename
if not destination.exists():
try:
urllib.request.urlretrieve(url_str, destination)
except Exception as e:
raise RenderCVUserError(
message=f"Failed to download photo from {url_str}: {e}"
) from e
rendercv_model.cv.photo = destination
def process_model(
rendercv_model: RenderCVModel, file_type: Literal["typst", "markdown"]
) -> RenderCVModel:

View File

@@ -8,7 +8,7 @@ import jinja2
from rendercv.schema.models.rendercv_model import RenderCVModel
from .markdown_parser import markdown_to_html
from .model_processor import process_model
from .model_processor import download_photo_from_url, process_model
from .string_processor import clean_url
templates_directory = pathlib.Path(__file__).parent / "templates"
@@ -79,6 +79,7 @@ def render_full_template(
"markdown": "md",
}[file_type]
download_photo_from_url(rendercv_model)
rendercv_model = process_model(rendercv_model, file_type)
header = render_single_template(

View File

@@ -49,10 +49,11 @@ class Cv(BaseModelWithoutExtraKeys):
["john.doe.1@example.com", "john.doe.2@example.com"],
],
)
photo: ExistingPathRelativeToInput | None = pydantic.Field(
photo: ExistingPathRelativeToInput | pydantic.HttpUrl | None = pydantic.Field(
default=None,
description="Photo file path, relative to the YAML file.",
examples=["photo.jpg", "images/profile.png"],
union_mode="left_to_right",
description="Photo file path (relative to the YAML file) or a URL.",
examples=["photo.jpg", "images/profile.png", "https://example.com/photo.jpg"],
)
phone: (
pydantic_phone_numbers.PhoneNumber

View File

@@ -4,7 +4,11 @@ import rendercv_fonts
from rendercv.schema.models.design.font_family import available_font_families
icon_font_families = {"Font Awesome 7"}
typst_built_in_font_families = {"Libertinus Serif", "New Computer Modern", "DejaVu Sans Mono"}
typst_built_in_font_families = {
"Libertinus Serif",
"New Computer Modern",
"DejaVu Sans Mono",
}
@pytest.mark.parametrize(