Skip to content
Merged
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
34 changes: 34 additions & 0 deletions app/pybind_parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,23 @@ PYBIND11_MODULE(pdf_parsers, m) {

Returns:
List[str]: A list of keys for the currently loaded documents.)")

.def("take_load_error",
[](docling::docling_parser &self, const std::string &key) -> std::map<std::string, std::string> {
return self.take_load_error(key);
},
pybind11::arg("key"),
R"(
Return and clear why the last load of `key` failed.

Parameters:
key (str): The key passed to the failed load call.

Returns:
dict[str, str]: {"category": ..., "message": ...}, or an empty dict when
no failure is recorded for the key. category is one of "password",
"damaged", "unsupported", "io" and "unknown"; message is the text of
the underlying (qpdf) error.)")

.def("load_document",
[](docling::docling_parser &self,
Expand Down Expand Up @@ -1116,6 +1133,23 @@ PYBIND11_MODULE(pdf_parsers, m) {
Returns:
bool: True if the document was successfully loaded.)")

.def("take_load_error",
[](docling::docling_threaded_parser& self, const std::string& key) -> std::map<std::string, std::string> {
return self.take_load_error(key);
},
pybind11::arg("key"),
R"(
Return and clear why the last load of `key` failed.

Parameters:
key (str): The key passed to the failed load call.

Returns:
dict[str, str]: {"category": ..., "message": ...}, or an empty dict when
no failure is recorded for the key. category is one of "password",
"damaged", "unsupported", "io" and "unknown"; message is the text of
the underlying (qpdf) error.)")

.def("load_document_from_bytesio",
[](docling::docling_threaded_parser& self,
const std::string& key,
Expand Down
91 changes: 88 additions & 3 deletions docling_parse/pdf_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import hashlib
import logging
import math
from enum import IntEnum
from enum import Enum, IntEnum
from io import BytesIO
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union
Expand Down Expand Up @@ -77,6 +77,78 @@
_log = logging.getLogger(__name__)


class PdfLoadErrorCategory(str, Enum):
"""Why docling-parse could not load a document.

The values are the categories the native loader reports, see
`src/parse/qpdf/load_error.h`.
"""

PASSWORD = "password"
"""The document is encrypted and the password is missing or wrong."""
DAMAGED = "damaged"
"""qpdf could not repair the file."""
UNSUPPORTED = "unsupported"
"""The file uses a feature qpdf does not implement."""
IO = "io"
"""The file is missing or unreadable."""
UNKNOWN = "unknown"


class PdfLoadError(RuntimeError):
"""docling-parse could not load a document.

A RuntimeError, so handlers written for the former generic error keep
working, and the message keeps its former prefix. `category` says what
went wrong; `detail` is the message of the underlying (qpdf) failure.
"""

def __init__(
self,
key: str,
category: PdfLoadErrorCategory,
detail: str,
*,
reason: str | None = None,
):
self.key = key
self.category = category
self.detail = detail

message = f"Failed to load document with key {key}"
description = reason or detail
if description:
message = f"{message}: {description}"
super().__init__(message)


class PdfPasswordError(PdfLoadError):
"""The document is encrypted and was opened without a valid password.

`password_supplied` tells whether the caller gave a (wrong) password or
none at all: qpdf reports both as the same invalid-password error.
"""

def __init__(self, key: str, detail: str, *, password_supplied: bool):
self.password_supplied = password_supplied
if password_supplied:
reason = "the supplied password is incorrect"
else:
reason = "the document is encrypted and requires a password"
super().__init__(key, PdfLoadErrorCategory.PASSWORD, detail, reason=reason)


def _load_error(native_parser, key: str, password: str | None) -> PdfLoadError:
"""Build the exception for a failed load from the native parser's record."""
info = native_parser.take_load_error(key)
category = PdfLoadErrorCategory(info.get("category", PdfLoadErrorCategory.UNKNOWN))
detail = info.get("message", "")

if category is PdfLoadErrorCategory.PASSWORD:
return PdfPasswordError(key, detail, password_supplied=password is not None)
return PdfLoadError(key, category, detail)


class PdfAnnotations(BaseModel):
"""PDF document annotations including form fields, language, metadata, and table of contents.

Expand Down Expand Up @@ -1004,7 +1076,14 @@ def load(
decode_config: DecodeConfig | None = None,
content_config: ContentConfig | None = None,
) -> PdfDocument:
"""Load a document and return a handle to its pages.

Raises:
PdfPasswordError: The document is encrypted and `password` is
missing or wrong.
PdfLoadError: The document could not be loaded for another
reason; see its `category` and `detail`.
"""
if isinstance(path_or_stream, str):
path_or_stream = Path(path_or_stream)

Expand Down Expand Up @@ -1048,7 +1127,7 @@ def load(

return result_doc
else:
raise RuntimeError(f"Failed to load document with key {key}")
raise _load_error(self.parser, key, password)

def _load_document(
self,
Expand Down Expand Up @@ -1571,6 +1650,12 @@ def load(

Returns:
str: The document key.

Raises:
PdfPasswordError: The document is encrypted and `password` is
missing or wrong.
PdfLoadError: The document could not be loaded for another
reason; see its `category` and `detail`.
"""
if page_numbers is not None and page_range is not None:
raise ValueError("page_numbers and page_range are mutually exclusive")
Expand Down Expand Up @@ -1608,7 +1693,7 @@ def load(
)

if not success:
raise RuntimeError(f"Failed to load document with key {key}")
raise _load_error(self._parser, key, password)

self._page_counts[key] = self._parser.number_of_pages(key)
self._scheduled_page_counts[key] = self._parser.scheduled_number_of_pages(key)
Expand Down
33 changes: 33 additions & 0 deletions src/parse/pdf_decoders/document.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
#include <qpdf/Buffer.hh>
#include <qpdf/QPDFAcroFormDocumentHelper.hh>
#include <qpdf/QPDF.hh>
#include <qpdf/QPDFExc.hh>
#include <qpdf/QPDFWriter.hh>

#include <parse/qpdf/load_error.h>
#include <parse/qpdf/logger.h>
#include <parse/qpdf/page_integrity.h>

Expand All @@ -34,6 +36,12 @@ namespace pdflib

std::string get_filename() { return filename; }

// Why the last process_document_* call returned false: a category
// from load_error and the message of the underlying failure. Both
// are empty while no load has failed.
const std::string& get_load_error_category() const { return load_error_category; }
const std::string& get_load_error_message() const { return load_error_message; }

nlohmann::json get_annotations();

nlohmann::json get_meta_xml();
Expand Down Expand Up @@ -83,6 +91,8 @@ namespace pdflib
// object the file does not contain (docling-parse#374).
void ensure_page_content_present(int page_number);

void record_load_error(const std::string& category, const std::string& message);

void update_timings(pdf_timings& timings_, bool set_timer);

private:
Expand Down Expand Up @@ -112,6 +122,9 @@ namespace pdflib
// and read by the worker threads afterwards.
bool qpdf_reported_damage;

std::string load_error_category;
std::string load_error_message;

nlohmann::json json_annots;
bool annots_loaded;

Expand Down Expand Up @@ -235,6 +248,7 @@ namespace pdflib
std::ifstream ifs(fs_path, std::ios::binary | std::ios::ate);
if(!ifs.is_open())
{
record_load_error(load_error::IO, "could not open file: " + filename);
LOG_S(ERROR) << "could not open file: " << filename;
return false;
}
Expand All @@ -245,6 +259,7 @@ namespace pdflib
auto file_buffer = std::make_shared<std::string>(file_size, '\0');
if(!ifs.read(file_buffer->data(), file_size))
{
record_load_error(load_error::IO, "could not read file: " + filename);
LOG_S(ERROR) << "could not read file: " << filename;
return false;
}
Expand All @@ -270,6 +285,9 @@ namespace pdflib
buffer = _buffer;
password = _password;

load_error_category.clear();
load_error_message.clear();

LOG_S(INFO) << "start processing buffer of size " << buffer->size() << " by qpdf ...";

utils::timer timer;
Expand Down Expand Up @@ -308,8 +326,16 @@ namespace pdflib

LOG_S(INFO) << "#-pages: " << number_of_pages;
}
catch(const QPDFExc & exc)
{
// qpdf says why: an invalid password, a file it could not repair, ...
record_load_error(load_error::category_of(exc.getErrorCode()), exc.what());
LOG_S(ERROR) << "could not process buffer by qpdf: " << exc.what();
return false;
}
catch(const std::exception & exc)
{
record_load_error(load_error::UNKNOWN, exc.what());
LOG_S(ERROR) << "could not process buffer by qpdf: " << exc.what();
return false;
}
Expand All @@ -321,6 +347,13 @@ namespace pdflib
return true;
}

void pdf_decoder<DOCUMENT>::record_load_error(const std::string& category,
const std::string& message)
{
load_error_category = category;
load_error_message = message;
}

std::pair<int, std::shared_ptr<std::string> > pdf_decoder<DOCUMENT>::get_thread_safe_page_buffer(
int page_ind,
bool keep_qpdf_warnings)
Expand Down
36 changes: 36 additions & 0 deletions src/parse/qpdf/load_error.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//-*-C++-*-

#ifndef QPDF_LOAD_ERROR_H
#define QPDF_LOAD_ERROR_H

#include <string>

#include <qpdf/Constants.h>

namespace pdflib
{
// Why a document could not be loaded. The strings cross into Python as
// docling_parse.pdf_parser.PdfLoadErrorCategory, so they change together.
namespace load_error
{
constexpr const char* PASSWORD = "password"; // encrypted; password missing or wrong
constexpr const char* DAMAGED = "damaged"; // qpdf could not repair the file
constexpr const char* UNSUPPORTED = "unsupported"; // a feature qpdf does not implement
constexpr const char* IO = "io"; // file missing or unreadable
constexpr const char* UNKNOWN = "unknown";

inline std::string category_of(qpdf_error_code_e code)
{
switch(code)
{
case qpdf_e_password: return PASSWORD;
case qpdf_e_damaged_pdf: return DAMAGED;
case qpdf_e_unsupported: return UNSUPPORTED;
case qpdf_e_system: return IO;
default: return UNKNOWN;
}
}
}
}

#endif
Loading
Loading