Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ check-all: ## Run all read-only local checks.
uv run --no-sync ty check
uv run --no-sync tach check
python3 scripts/check_tach_module_coverage.py
python3 scripts/check_max_lines.py
python3 scripts/check_max_lines.py --max-lines=1500
python3 .github/scripts/check_skill_routes.py
uv run --no-sync dprint check --config .github/dprint.json --config-discovery=false
uv lock --locked
Expand Down
18 changes: 13 additions & 5 deletions docling/backend/utils/image_resource_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,17 +312,25 @@ def is_absolute_path(loc: str) -> bool:
)

@staticmethod
def _local_base_path(value: str) -> str:
"""Return a filesystem path for a local path or ``file://`` URI."""
def normalize_local_base_path(value: str) -> str:
"""Convert a file URI to a native path; leave other bases unchanged.

Raise ValueError for an empty path or an unsupported authority.
This operation does not grant permission to read local files.
"""
parsed = urlparse(value)
if parsed.scheme.lower() != "file":
return value

path = parsed.path
if parsed.netloc and parsed.netloc.lower() != "localhost":
path = f"//{parsed.netloc}{path}"
if not path:
raise ValueError("Invalid file URI: empty path")
if parsed.netloc and parsed.netloc.lower() != "localhost":
if os.name != "nt":
raise ValueError(
"Non-localhost file URI authorities require Windows UNC paths"
)
path = f"//{parsed.netloc}{path}"
return url2pathname(path)

def resolve_relative_path(self, loc: str, base_path: Optional[str]) -> str:
Expand All @@ -335,7 +343,7 @@ def resolve_relative_path(self, loc: str, base_path: Optional[str]) -> str:
abs_loc = loc

if base_path:
base_path = self._local_base_path(base_path)
base_path = self.normalize_local_base_path(base_path)
if loc.startswith("//"):
abs_loc = "https:" + loc
elif not loc.startswith(("http://", "https://", "data:", "#")):
Expand Down
31 changes: 23 additions & 8 deletions docling/backend/xml/jats_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from io import BytesIO
from pathlib import Path
from typing import Final, cast
from urllib.parse import urlsplit

from docling_core.types.doc import (
DocItemLabel,
Expand Down Expand Up @@ -962,11 +963,21 @@ def _add_figure_captions(

def _load_figure_image(self, node: etree._Element) -> ImageRef | None:
"""Load an explicitly enabled local JATS figure image."""
if (
not self.options.fetch_images
or self.base_path is None
or not ImageResourceLoader.is_local_path(self.base_path)
):
if not self.options.fetch_images or self.base_path is None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: JATS images require enable_local_fetch=True but the gate does not check it

The old guard checked is_local_path(self.base_path); the new guard skips the enable_local_fetch flag entirely. The permission is still enforced later inside ImageResourceLoader.load_image_data (which raises OperationNotAllowed, caught in create_image_ref and silently swallowed). But the function now calls normalize_local_base_path, Path.is_file, and emits warnings for root-URI cases even when enable_local_fetch=False. The test test_local_permission_prevents_image_bytes covers only the case where fetch_images=True and enable_local_fetch=False; it patches builtins.open to prove bytes are not read, which is correct. The normalize_local_base_path and is_file calls are not patched, so they do run. This is not a security regression (bytes are never read without the flag) but it is a behavioral change: the function now touches the filesystem even when local fetch is disabled. Confirm this is acceptable, or add an early return when not self.options.enable_local_fetch.

return None

is_file_uri = urlsplit(self.base_path).scheme.lower() == "file"
if not is_file_uri and not ImageResourceLoader.is_local_path(self.base_path):
return None
try:
local_base = Path(
self._image_loader.normalize_local_base_path(self.base_path)
)
# AnyUrl normalizes empty file URIs to a root directory URI.
if is_file_uri and local_base.parent == local_base:
raise ValueError("JATS file URI must identify a source document")
except ValueError as e:
warnings.warn(f"Could not process JATS figure images: {e}")
return None

graphic_nodes = node.xpath("graphic | alternatives/graphic")
Expand Down Expand Up @@ -1018,9 +1029,13 @@ def _load_figure_image(self, node: etree._Element) -> ImageRef | None:
# loader.
if Path(resolved_href).is_file():
found_candidate = True
image_ref = self._image_loader.create_image_ref(
resolved_href, self.base_path
)
try:
image_ref = self._image_loader.create_image_ref(
resolved_href, self.base_path
)
except OSError as e:
warnings.warn(f"Could not process an image from {href}: {e}")
continue
if image_ref is not None:
return image_ref

Expand Down
18 changes: 18 additions & 0 deletions docs/usage/advanced_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,24 @@ Relative image paths are resolved against the current working directory. The
in JSON input; save the document with `ImageRefMode.EMBEDDED` if it has to go
through the CLI again with its images.

### Load local JATS figure images

JATS figure images require both `fetch_images=True` and
`enable_local_fetch=True` on `JatsBackendOptions`. For a `DocumentStream`, set
`source_uri` to the source document path or its `file://` URI. The source
document need not exist, but its directory must contain the referenced images.
Relative figure paths stay within that directory. Remote image references and
remote source URIs do not enable downloads, even with `enable_remote_fetch=True`.

File URIs with no authority or with `localhost` work on all supported platforms.
Other authorities, such as `file://server/share/article.nxml`, require Windows
UNC paths. JATS skips file URIs that identify only a filesystem root or a UNC
share root, including empty file URIs normalized to `file:///`. Use a document
location such as `file:///path/to/article.nxml` instead.

Missing or damaged images produce a warning. The figure, its caption, and the
rest of the article remain in the converted document. SVG figures are skipped.

### Fetch HTML images from remote hosts

The HTML backend only downloads images referenced by a page when you opt in with
Expand Down
Loading
Loading