feat: implemented wrapper and pdf doc page
This commit is contained in:
@@ -65,6 +65,7 @@ if(PDFENGINE_BUILD_TESTS)
|
||||
endif()
|
||||
|
||||
add_subdirectory(engine)
|
||||
add_subdirectory(bindings)
|
||||
|
||||
|
||||
message(STATUS "PdfEngine ${PROJECT_VERSION} configured")
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Python bindings for PdfEngine using pybind11
|
||||
#
|
||||
|
||||
find_package(pybind11 CONFIG REQUIRED)
|
||||
|
||||
# Declare the python module target. We name the target pdfengine_py to avoid
|
||||
# target collision with the static C++ library pdfengine, but we set the
|
||||
# OUTPUT_NAME to pdfengine to produce the correct importable module.
|
||||
pybind11_add_module(pdfengine_py python/pdfengine_py.cpp)
|
||||
|
||||
set_target_properties(pdfengine_py PROPERTIES
|
||||
OUTPUT_NAME "pdfengine"
|
||||
ARCHIVE_OUTPUT_NAME "pdfengine_py_import"
|
||||
)
|
||||
|
||||
target_link_libraries(pdfengine_py PRIVATE pdfengine::pdfengine)
|
||||
|
||||
# Set warnings and sanitizers for the bindings module
|
||||
pdfengine_set_warnings(pdfengine_py)
|
||||
pdfengine_enable_sanitizers(pdfengine_py)
|
||||
|
||||
# Copy the compiled .pyd (or .so) file to the gateway/ directory so the
|
||||
# Python FastAPI app and its tests can import it immediately after building.
|
||||
add_custom_command(TARGET pdfengine_py POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:pdfengine_py> "${CMAKE_SOURCE_DIR}/gateway/"
|
||||
COMMENT "Copying compiled Python extension to gateway/ directory"
|
||||
)
|
||||
@@ -0,0 +1,125 @@
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include <pdfengine/pdf_document.hpp>
|
||||
#include <pdfengine/pdf_engine.hpp>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace {
|
||||
|
||||
void throw_on_error(pdfengine::EngineError err) {
|
||||
switch (err) {
|
||||
case pdfengine::EngineError::FileNotFound:
|
||||
PyErr_SetString(PyExc_FileNotFoundError, "PDF file not found");
|
||||
throw py::error_already_set();
|
||||
case pdfengine::EngineError::InvalidFormat:
|
||||
throw py::value_error("Invalid PDF format");
|
||||
case pdfengine::EngineError::PasswordRequired:
|
||||
throw py::value_error("Password required to open this PDF");
|
||||
case pdfengine::EngineError::InvalidPassword:
|
||||
throw py::value_error("Invalid password provided for this PDF");
|
||||
case pdfengine::EngineError::PageOutOfBounds:
|
||||
throw py::index_error("Page index out of bounds");
|
||||
case pdfengine::EngineError::RenderFailed:
|
||||
throw std::runtime_error("Failed to render PDF page");
|
||||
case pdfengine::EngineError::WriteFailed:
|
||||
throw std::runtime_error("Failed to write PDF data");
|
||||
default:
|
||||
throw std::runtime_error("Unknown PDF engine error");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T get_or_throw(std::expected<T, pdfengine::EngineError>&& res) {
|
||||
if (!res.has_value()) {
|
||||
throw_on_error(res.error());
|
||||
}
|
||||
return std::move(res.value());
|
||||
}
|
||||
|
||||
void get_or_throw(std::expected<void, pdfengine::EngineError>&& res) {
|
||||
if (!res.has_value()) {
|
||||
throw_on_error(res.error());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PYBIND11_MODULE(pdfengine, m) {
|
||||
m.doc() = "Python bindings for the PdfEngine C++ Core SDK";
|
||||
|
||||
m.def("engine_version", &pdfengine::engineVersion, "Get the engine version string");
|
||||
m.def("engine_build_info", &pdfengine::engineBuildInfo, "Get the engine build info string");
|
||||
m.def("engine_has_pdfium", &pdfengine::engineHasPdfium, "Check if the engine was built with PDFium support");
|
||||
m.def("engine_has_skia", &pdfengine::engineHasSkia, "Check if the engine was built with Skia support");
|
||||
|
||||
py::class_<pdfengine::Point2D>(m, "Point2D")
|
||||
.def(py::init<double, double>(), py::arg("x") = 0.0, py::arg("y") = 0.0)
|
||||
.def_readwrite("x", &pdfengine::Point2D::x)
|
||||
.def_readwrite("y", &pdfengine::Point2D::y)
|
||||
.def("__repr__", [](const pdfengine::Point2D& self) {
|
||||
return "Point2D(x=" + std::to_string(self.x) + ", y=" + std::to_string(self.y) + ")";
|
||||
});
|
||||
|
||||
py::class_<pdfengine::DevicePoint>(m, "DevicePoint")
|
||||
.def(py::init<int, int>(), py::arg("x") = 0, py::arg("y") = 0)
|
||||
.def_readwrite("x", &pdfengine::DevicePoint::x)
|
||||
.def_readwrite("y", &pdfengine::DevicePoint::y)
|
||||
.def("__repr__", [](const pdfengine::DevicePoint& self) {
|
||||
return "DevicePoint(x=" + std::to_string(self.x) + ", y=" + std::to_string(self.y) + ")";
|
||||
});
|
||||
|
||||
py::class_<pdfengine::DocumentMetadata>(m, "DocumentMetadata")
|
||||
.def_readonly("title", &pdfengine::DocumentMetadata::title)
|
||||
.def_readonly("author", &pdfengine::DocumentMetadata::author)
|
||||
.def_readonly("creator", &pdfengine::DocumentMetadata::creator)
|
||||
.def_readonly("producer", &pdfengine::DocumentMetadata::producer)
|
||||
.def_readonly("creation_date", &pdfengine::DocumentMetadata::creationDate)
|
||||
.def_readonly("modification_date", &pdfengine::DocumentMetadata::modificationDate)
|
||||
.def("__repr__", [](const pdfengine::DocumentMetadata& self) {
|
||||
return "DocumentMetadata(title='" + self.title + "', author='" + self.author + "')";
|
||||
});
|
||||
|
||||
py::class_<pdfengine::PageImage>(m, "PageImage")
|
||||
.def_readonly("width", &pdfengine::PageImage::width)
|
||||
.def_readonly("height", &pdfengine::PageImage::height)
|
||||
.def_property_readonly("data", [](const pdfengine::PageImage& self) {
|
||||
return py::bytes(reinterpret_cast<const char*>(self.data.data()), self.data.size());
|
||||
});
|
||||
|
||||
py::class_<pdfengine::PdfPage, std::shared_ptr<pdfengine::PdfPage>>(m, "PdfPage")
|
||||
.def_property_readonly("width", &pdfengine::PdfPage::width)
|
||||
.def_property_readonly("height", &pdfengine::PdfPage::height)
|
||||
.def("render", [](const pdfengine::PdfPage& self, int dpi) {
|
||||
return get_or_throw(self.render(dpi));
|
||||
}, py::arg("dpi") = 96)
|
||||
.def("extract_text", [](const pdfengine::PdfPage& self) {
|
||||
return get_or_throw(self.extractText());
|
||||
})
|
||||
.def("page_to_device", &pdfengine::PdfPage::pageToDevice,
|
||||
py::arg("page_point"), py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0)
|
||||
.def("device_to_page", &pdfengine::PdfPage::deviceToPage,
|
||||
py::arg("device_point"), py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0);
|
||||
|
||||
py::class_<pdfengine::PdfDocument, std::shared_ptr<pdfengine::PdfDocument>>(m, "PdfDocument")
|
||||
.def_static("load_from_file", [](const std::string& path, const std::string& password) {
|
||||
return get_or_throw(pdfengine::PdfDocument::loadFromFile(path, password));
|
||||
}, py::arg("path"), py::arg("password") = "")
|
||||
.def_static("load_from_memory", [](const py::bytes& bytes, const std::string& password) {
|
||||
std::string_view sv = bytes;
|
||||
std::vector<uint8_t> data(sv.begin(), sv.end());
|
||||
return get_or_throw(pdfengine::PdfDocument::loadFromMemory(data, password));
|
||||
}, py::arg("data"), py::arg("password") = "")
|
||||
.def_property_readonly("page_count", &pdfengine::PdfDocument::pageCount)
|
||||
.def_property_readonly("metadata", &pdfengine::PdfDocument::metadata)
|
||||
.def("get_page", [](pdfengine::PdfDocument& self, int pageIndex) {
|
||||
return get_or_throw(self.getPage(pageIndex));
|
||||
}, py::arg("page_index"))
|
||||
.def("apply_edits", [](pdfengine::PdfDocument& self, const std::string& editsJson) {
|
||||
get_or_throw(self.applyEdits(editsJson));
|
||||
}, py::arg("edits_json"))
|
||||
.def("save_incremental", [](const pdfengine::PdfDocument& self) {
|
||||
std::vector<uint8_t> res = get_or_throw(self.saveIncremental());
|
||||
return py::bytes(reinterpret_cast<const char*>(res.data()), res.size());
|
||||
});
|
||||
}
|
||||
+24
@@ -184,8 +184,32 @@ SKIP_WASM=1 ./scripts/test_phase0.sh
|
||||
|
||||
---
|
||||
|
||||
## Automated Convenience Scripts
|
||||
|
||||
We have created several PowerShell scripts in the `scripts/` directory to automate setting up the MSVC compiler environments, building, running tests, and starting the FastAPI gateway server. You can run these from the project root:
|
||||
|
||||
* **Build C++ Engine & Python Bindings**:
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts/build_cpp.ps1
|
||||
```
|
||||
* **Run C++ Core Unit Tests**:
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts/test_cpp.ps1
|
||||
```
|
||||
* **Run Gateway pytest Integration Tests**:
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts/test_gateway.ps1
|
||||
```
|
||||
* **Start the Local FastAPI Dev Server**:
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts/start_gateway.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
|
||||
```powershell
|
||||
cmake --preset windows-debug
|
||||
cmake --build --preset windows-debug
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,50 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/Kids [3 0 R]
|
||||
/MediaBox [0 0 100 50]
|
||||
/Count 1
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Contents 4 0 R
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Length 71
|
||||
>>
|
||||
stream
|
||||
10 15 m
|
||||
40 15 l
|
||||
40 35 l
|
||||
10 35 l
|
||||
W n
|
||||
0 0 1 RG
|
||||
10 10 m
|
||||
25 40 l
|
||||
40 10 l
|
||||
s
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000156 00000 n
|
||||
0000000225 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 5
|
||||
>>
|
||||
startxref
|
||||
347
|
||||
%%EOF
|
||||
@@ -0,0 +1,48 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [0 0 200 100]
|
||||
/Count 1
|
||||
/Kids [3 0 R]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Contents 4 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Length 95
|
||||
>>
|
||||
stream
|
||||
q
|
||||
0 0 0 rg
|
||||
10 25 m 190 25 l S
|
||||
[6 5 4 3 2 1] 5 d
|
||||
10 50 m 190 50 l S
|
||||
[] 0 d
|
||||
10 75 m 190 75 l S
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000157 00000 n
|
||||
0000000226 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 5
|
||||
>>
|
||||
startxref
|
||||
372
|
||||
%%EOF
|
||||
@@ -0,0 +1,68 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [0 0 200 200]
|
||||
/Count 1
|
||||
/Kids [3 0 R]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 4 0 R
|
||||
/F2 5 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents 6 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Times-Roman
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
>>
|
||||
endobj
|
||||
6 0 obj <<
|
||||
% Note this object deliberately does not use /Length 83.
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
20 50 Td
|
||||
/F1 12 Tf
|
||||
(Hello, world!) Tj
|
||||
0 50 Td
|
||||
/F2 16 Tf
|
||||
(Goodbye, world!) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000157 00000 n
|
||||
0000000299 00000 n
|
||||
0000000377 00000 n
|
||||
0000000453 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
633
|
||||
%%EOF
|
||||
@@ -0,0 +1,81 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [0 0 200 200]
|
||||
/Count 2
|
||||
/Kids [3 0 R 4 0 R]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 5 0 R
|
||||
/F2 6 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents 7 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 5 0 R
|
||||
/F2 6 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents 7 0 R
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Times-Roman
|
||||
>>
|
||||
endobj
|
||||
6 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
>>
|
||||
endobj
|
||||
7 0 obj <<
|
||||
/Length 83
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
20 50 Td
|
||||
/F1 12 Tf
|
||||
(Hello, world!) Tj
|
||||
0 50 Td
|
||||
/F2 16 Tf
|
||||
(Goodbye, world!) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000163 00000 n
|
||||
0000000305 00000 n
|
||||
0000000447 00000 n
|
||||
0000000525 00000 n
|
||||
0000000601 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 8
|
||||
>>
|
||||
startxref
|
||||
735
|
||||
%%EOF
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [0 0 200 300]
|
||||
/Count 1
|
||||
/Kids [3 0 R]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Contents 4 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Length 188
|
||||
>>
|
||||
stream
|
||||
q
|
||||
0 0 0 rg
|
||||
0 290 10 10 re B*
|
||||
10 150 50 30 re B*
|
||||
0 0 1 rg
|
||||
190 290 10 10 re B*
|
||||
70 232 50 30 re B*
|
||||
0 1 0 rg
|
||||
190 0 10 10 re B*
|
||||
130 150 50 30 re B*
|
||||
1 0 0 rg
|
||||
0 0 10 10 re B*
|
||||
70 67 50 30 re B*
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000157 00000 n
|
||||
0000000226 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 5
|
||||
>>
|
||||
startxref
|
||||
466
|
||||
%%EOF
|
||||
@@ -0,0 +1,122 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [ 0 0 200 250 ]
|
||||
/Count 5
|
||||
/Kids [ 3 0 R 5 0 R 7 0 R 9 0 R 11 0 R ]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Contents 4 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Length 49
|
||||
>>
|
||||
stream
|
||||
q
|
||||
1 1 0 rg
|
||||
100 0 30 50 re B*
|
||||
70 67 50 30 re B*
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Rotate 90
|
||||
/Contents 6 0 R
|
||||
>>
|
||||
endobj
|
||||
6 0 obj <<
|
||||
/Length 49
|
||||
>>
|
||||
stream
|
||||
q
|
||||
0 1 1 rg
|
||||
100 0 30 50 re B*
|
||||
70 67 50 30 re B*
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
7 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Contents 8 0 R
|
||||
>>
|
||||
endobj
|
||||
8 0 obj <<
|
||||
/Length 49
|
||||
>>
|
||||
stream
|
||||
q
|
||||
1 0 0 rg
|
||||
100 0 30 50 re B*
|
||||
70 67 50 30 re B*
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
9 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Contents 10 0 R
|
||||
>>
|
||||
endobj
|
||||
10 0 obj <<
|
||||
/Length 51
|
||||
>>
|
||||
stream
|
||||
q
|
||||
0 1 0 rg
|
||||
100 0 30 50 re B*
|
||||
100 150 50 30 re B*
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
11 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Contents 12 0 R
|
||||
>>
|
||||
endobj
|
||||
12 0 obj <<
|
||||
/Length 50
|
||||
>>
|
||||
stream
|
||||
q
|
||||
0 0 0 rg
|
||||
0 90 80 60 re B*
|
||||
100 150 50 30 re B*
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 13
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000186 00000 n
|
||||
0000000255 00000 n
|
||||
0000000355 00000 n
|
||||
0000000437 00000 n
|
||||
0000000537 00000 n
|
||||
0000000606 00000 n
|
||||
0000000706 00000 n
|
||||
0000000776 00000 n
|
||||
0000000879 00000 n
|
||||
0000000950 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 13
|
||||
>>
|
||||
startxref
|
||||
1052
|
||||
%%EOF
|
||||
@@ -0,0 +1,57 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [ 0 0 200 200 ]
|
||||
/Count 1
|
||||
/Kids [ 3 0 R ]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 4 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents 5 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Length 33
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
20 100 Td
|
||||
/F1 16 Tf
|
||||
( ) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000161 00000 n
|
||||
0000000287 00000 n
|
||||
0000000363 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 6
|
||||
>>
|
||||
startxref
|
||||
447
|
||||
%%EOF
|
||||
@@ -0,0 +1,395 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
/AcroForm <<
|
||||
/Fields [23 0 R]
|
||||
/DR <<
|
||||
/Font <<
|
||||
/F1 7 0 R
|
||||
>>
|
||||
>>
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/Count 2
|
||||
/Kids [3 0 R 4 0 R]
|
||||
/MediaBox [0 0 612 792]
|
||||
/CropBox [0 0 612 792]
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 7 0 R
|
||||
/F2 8 0 R
|
||||
>>
|
||||
/ProcSet [/PDF /Text /ImageC]
|
||||
/ExtGState <<
|
||||
/GS0 24 0 R
|
||||
>>
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Contents 5 0 R
|
||||
/Annots [15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R]
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Contents 6 0 R
|
||||
/Annots [15 0 R 16 0 R 26 0 R]
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Length 486
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
70 700 Td
|
||||
/F1 18 Tf
|
||||
(Link Annotations - Page 1) Tj
|
||||
0 -65 Td
|
||||
/F2 14 Tf
|
||||
(1. Link with destination to first page) Tj
|
||||
10 -20 Td
|
||||
/F2 14 Tf
|
||||
(2. Link with destination to second page) Tj
|
||||
-12 -84 Td
|
||||
/F2 10 Tf
|
||||
(PDF Reference, Version 1.7, Section 8.4.5 defines Annotations) Tj
|
||||
2 -53 Td
|
||||
(3. An example of Highlight with text notes) Tj
|
||||
0 -18 Td
|
||||
(https://pdfium.googlesource.com/pdfium is link in plain text, not link annotation. These are referred to) Tj
|
||||
0 -17 Td
|
||||
(as WebLinks in PDFium.)Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
6 0 obj <<
|
||||
/Length 185
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
70 700 Td
|
||||
/F1 18 Tf
|
||||
(Link Annotations - Page 2) Tj
|
||||
0 -65 Td
|
||||
/F2 14 Tf
|
||||
(1. Link with destination to first page) Tj
|
||||
10 -20 Td
|
||||
/F2 14 Tf
|
||||
(2. Link with destination to second page) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
7 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Times-Roman
|
||||
>>
|
||||
endobj
|
||||
8 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
>>
|
||||
endobj
|
||||
9 0 obj <<
|
||||
/Type /XObject
|
||||
/Subtype /Form
|
||||
/FormType 1
|
||||
/Length 18
|
||||
/BBox [293 530 349 542]
|
||||
/Resources <<
|
||||
/XObject <<
|
||||
/Form0 10 0 R
|
||||
>>
|
||||
/ExtGState <<
|
||||
/GS0 25 0 R
|
||||
>>
|
||||
>>
|
||||
>>
|
||||
stream
|
||||
/GS0 gs
|
||||
/Form0 Do
|
||||
endstream
|
||||
endobj
|
||||
10 0 obj <<
|
||||
/Type /XObject
|
||||
/Subtype /Form
|
||||
/FormType 1
|
||||
/Group <<
|
||||
/S /Transparency
|
||||
>>
|
||||
/Length 59
|
||||
/BBox [293 530 349 542]
|
||||
>>
|
||||
stream
|
||||
1.0 1.0 0.0 rg
|
||||
293 530 m
|
||||
349 530 l
|
||||
349 542 l
|
||||
293 542 l
|
||||
h f
|
||||
endstream
|
||||
endobj
|
||||
11 0 obj <<
|
||||
/Type /XObject
|
||||
/Subtype /Form
|
||||
/FormType 1
|
||||
/Length 18
|
||||
/BBox [83 440 178 453]
|
||||
/Resources <<
|
||||
/XObject <<
|
||||
/Form0 12 0 R
|
||||
>>
|
||||
/ExtGState <<
|
||||
/GS0 25 0 R
|
||||
>>
|
||||
>>
|
||||
>>
|
||||
stream
|
||||
/GS0 gs
|
||||
/Form0 Do
|
||||
endstream
|
||||
endobj
|
||||
12 0 obj <<
|
||||
/Type /XObject
|
||||
/Subtype /Form
|
||||
/FormType 1
|
||||
/Group <<
|
||||
/S /Transparency
|
||||
>>
|
||||
/Length 57
|
||||
/BBox [83 440 178 453]
|
||||
>>
|
||||
stream
|
||||
0.0 1.0 1.0 rg
|
||||
83 440 m
|
||||
178 440 l
|
||||
178 453 l
|
||||
83 453 l
|
||||
h f
|
||||
endstream
|
||||
endobj
|
||||
13 0 obj <<
|
||||
/Type /XObject
|
||||
/Subtype /Form
|
||||
/FormType 1
|
||||
/Length 18
|
||||
/BBox [149 476 191 487]
|
||||
/Resources <<
|
||||
/XObject <<
|
||||
/Form0 14 0 R
|
||||
>>
|
||||
/ExtGState <<
|
||||
/GS0 25 0 R
|
||||
>>
|
||||
>>
|
||||
>>
|
||||
stream
|
||||
/GS0 gs
|
||||
/Form0 Do
|
||||
endstream
|
||||
endobj
|
||||
14 0 obj <<
|
||||
/Type /XObject
|
||||
/Subtype /Form
|
||||
/FormType 1
|
||||
/Group <<
|
||||
/S /Transparency
|
||||
>>
|
||||
/Length 59
|
||||
/BBox [149 476 191 487]
|
||||
>>
|
||||
stream
|
||||
0.0 1.0 0.0 rg
|
||||
149 476 m
|
||||
191 476 l
|
||||
191 487 l
|
||||
149 487 l
|
||||
h f
|
||||
endstream
|
||||
endobj
|
||||
15 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Link
|
||||
/BS <<
|
||||
/W 0
|
||||
>>
|
||||
/Rect [69 633 542 653]
|
||||
/Dest [3 0 R /XYZ 200 725 0]
|
||||
/F 4
|
||||
>>
|
||||
endobj
|
||||
16 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Link
|
||||
/BS <<
|
||||
/W 0
|
||||
>>
|
||||
/Rect [80 613 542 633]
|
||||
/Dest [4 0 R /XYZ 200 725 0]
|
||||
/F 4
|
||||
>>
|
||||
endobj
|
||||
17 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Link
|
||||
/BS <<
|
||||
/W 0
|
||||
>>
|
||||
/Rect [66 529 196 544]
|
||||
/A <<
|
||||
/Type /Action
|
||||
/URI (https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdf_reference_1-7.pdf)
|
||||
/S /URI
|
||||
>>
|
||||
/F 4
|
||||
>>
|
||||
endobj
|
||||
18 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Link
|
||||
/BS <<
|
||||
/W 0
|
||||
>>
|
||||
/Rect [83 440 178 453]
|
||||
/QuadPoints [83 453 178 453 83 440 178 440]
|
||||
/A <<
|
||||
/Type /Action
|
||||
/URI (https://cs.chromium.org/chromium/src/third_party/pdfium/public/fpdf_text.h)
|
||||
/S /URI
|
||||
>>
|
||||
/F 4
|
||||
>>
|
||||
endobj
|
||||
19 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Highlight
|
||||
/AP <<
|
||||
/N 9 0 R
|
||||
>>
|
||||
/NM (Highlight-1)
|
||||
/F 4
|
||||
/QuadPoints [293 542 349 542 293 530 349 530]
|
||||
/P 3 0 R
|
||||
/C [1 0.90196 0]
|
||||
/Rect [293 530 349 542]
|
||||
>>
|
||||
endobj
|
||||
20 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Highlight
|
||||
/AP <<
|
||||
/N 11 0 R
|
||||
>>
|
||||
/NM (Highlight-2)
|
||||
/F 4
|
||||
/QuadPoints [83 453 178 453 83 440 178 440]
|
||||
/P 3 0 R
|
||||
/C [0.26667 0.78431 0.96078]
|
||||
/Rect [83 440 178 453]
|
||||
>>
|
||||
endobj
|
||||
21 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Popup
|
||||
/Parent 22 0 R
|
||||
/Rect [191 377 443 488]
|
||||
>>
|
||||
endobj
|
||||
22 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Highlight
|
||||
/Popup 21 0 R
|
||||
/AP <<
|
||||
/N 13 0 R
|
||||
>>
|
||||
/NM (Highlight-With-Popup-1)
|
||||
/Contents (Text Note)
|
||||
/QuadPoints [149 487 191 487 149 476 191 476]
|
||||
/P 3 0 R
|
||||
/C [0.14902 0.90196 0]
|
||||
/Rect [149 476 191 487]
|
||||
/F 4
|
||||
>>
|
||||
endobj
|
||||
23 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 131072
|
||||
/T (Combo1)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [70 350 170 380]
|
||||
/Opt [(Highlight) (Link) (Popup) (Widget)]
|
||||
>>
|
||||
endobj
|
||||
24 0 obj <<
|
||||
/ca 1
|
||||
/Type /ExtGState
|
||||
/CA 1
|
||||
/BM /Normal
|
||||
>>
|
||||
endobj
|
||||
25 0 obj <<
|
||||
/ca 1
|
||||
/Type /ExtGState
|
||||
/CA 1
|
||||
/AIS false
|
||||
/BM /Multiply
|
||||
>>
|
||||
endobj
|
||||
26 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Square
|
||||
/Border [0 0 2]
|
||||
/C [1 0 0]
|
||||
/F 4
|
||||
/P 3 0 R
|
||||
/Rect [50 100 60 120]
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 27
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000169 00000 n
|
||||
0000000439 00000 n
|
||||
0000000583 00000 n
|
||||
0000000685 00000 n
|
||||
0000001223 00000 n
|
||||
0000001460 00000 n
|
||||
0000001538 00000 n
|
||||
0000001614 00000 n
|
||||
0000001864 00000 n
|
||||
0000002087 00000 n
|
||||
0000002337 00000 n
|
||||
0000002557 00000 n
|
||||
0000002808 00000 n
|
||||
0000003031 00000 n
|
||||
0000003171 00000 n
|
||||
0000003311 00000 n
|
||||
0000003558 00000 n
|
||||
0000003842 00000 n
|
||||
0000004059 00000 n
|
||||
0000004286 00000 n
|
||||
0000004384 00000 n
|
||||
0000004659 00000 n
|
||||
0000004849 00000 n
|
||||
0000004920 00000 n
|
||||
0000005006 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 27
|
||||
>>
|
||||
startxref
|
||||
5135
|
||||
%%EOF
|
||||
@@ -0,0 +1,162 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
/Outlines 8 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/Count 2
|
||||
/Kids [
|
||||
3 0 R
|
||||
4 0 R
|
||||
]
|
||||
>>
|
||||
endobj
|
||||
% Page number 0.
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 5 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents [6 0 R]
|
||||
/MediaBox [0 0 612 792]
|
||||
>>
|
||||
endobj
|
||||
% Page number 1.
|
||||
4 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 5 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents [7 0 R]
|
||||
/MediaBox [0 0 612 792]
|
||||
>>
|
||||
endobj
|
||||
% Font resource.
|
||||
5 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Arial
|
||||
>>
|
||||
endobj
|
||||
% Content for page 0.
|
||||
6 0 obj <<
|
||||
/Length 37
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
/F1 20 Tf
|
||||
100 600 TD (Page1)Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
% Content for page 1.
|
||||
7 0 obj <<
|
||||
/Length 37
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
/F1 20 Tf
|
||||
100 600 TD (Page2)Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
% Root bookmark
|
||||
8 0 obj <<
|
||||
/Type /Outlines
|
||||
/Count 3
|
||||
/First 9 0 R
|
||||
/Last 12 0 R
|
||||
>>
|
||||
endobj
|
||||
% First child bookmark (leaf node)
|
||||
9 0 obj <<
|
||||
/Title (A Good Beginning)
|
||||
/Parent 8 0 R
|
||||
/Next 10 0 R
|
||||
/Dest (foo)
|
||||
>>
|
||||
endobj
|
||||
% Second child bookmark (open)
|
||||
10 0 obj <<
|
||||
/Title (Open Middle)
|
||||
/Parent 8 0 R
|
||||
/First 11 0 R
|
||||
/Last 11 0 R
|
||||
/Prev 9 0 R
|
||||
/Next 12 0 R
|
||||
/Count 1
|
||||
/A <<
|
||||
/Type /Action
|
||||
/S /URI
|
||||
/URI (https://theplay.test)
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
% First grandchild bookmark
|
||||
11 0 obj <<
|
||||
/Title (Open Middle Descendant)
|
||||
/Parent 10 0 R
|
||||
/Dest [3 0 R /XYZ 100 200 0]
|
||||
>>
|
||||
endobj
|
||||
% Third child bookmark (closed)
|
||||
12 0 obj <<
|
||||
/Title (A Good Closed Ending)
|
||||
/Parent 8 0 R
|
||||
/First 13 0 R
|
||||
/Last 14 0 R
|
||||
/Prev 10 0 R
|
||||
/Count -2
|
||||
/Dest (bar)
|
||||
>>
|
||||
endobj
|
||||
% Second grandchild bookmark
|
||||
13 0 obj <<
|
||||
/Title (A Good Closed Ending Descendant)
|
||||
/Parent 12 0 R
|
||||
/Next 14 0 R
|
||||
/Dest (bar)
|
||||
>>
|
||||
endobj
|
||||
% Third grandchild bookmark
|
||||
14 0 obj <<
|
||||
/Title (A Good Closed Ending Descendant 2)
|
||||
/Parent 12 0 R
|
||||
/Prev 13 0 R
|
||||
/Dest (bar)
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 15
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000086 00000 n
|
||||
0000000184 00000 n
|
||||
0000000355 00000 n
|
||||
0000000527 00000 n
|
||||
0000000621 00000 n
|
||||
0000000731 00000 n
|
||||
0000000835 00000 n
|
||||
0000000950 00000 n
|
||||
0000001075 00000 n
|
||||
0000001310 00000 n
|
||||
0000001446 00000 n
|
||||
0000001617 00000 n
|
||||
0000001756 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 15
|
||||
>>
|
||||
startxref
|
||||
1869
|
||||
%%EOF
|
||||
@@ -0,0 +1,109 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
/AcroForm <<
|
||||
/Fields [ 8 0 R 9 0 R 10 0 R ]
|
||||
/DR 4 0 R
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/Count 1
|
||||
/Kids [ 3 0 R ]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources 4 0 R
|
||||
/MediaBox [ 0 0 300 600 ]
|
||||
/Contents 7 0 R
|
||||
/Annots [ 8 0 R 9 0 R 10 0 R ]
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Font 5 0 R
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/F1 6 0 R
|
||||
>>
|
||||
endobj
|
||||
6 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
>>
|
||||
endobj
|
||||
7 0 obj <<
|
||||
/Length 51
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
0 0 0 rg
|
||||
/F1 12 Tf
|
||||
100 450 Td
|
||||
(Test Form) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
8 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 393216
|
||||
/T (Combo_Editable)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [ 100 350 200 380 ]
|
||||
/Opt [[(foo) (Foo)] [(bar) (Bar)] [(qux) (Qux)]]
|
||||
>>
|
||||
endobj
|
||||
9 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 131072
|
||||
/T (Combo1)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [ 100 400 200 430 ]
|
||||
/Opt [(Apple) (Banana) (Cherry) (Date) (Elderberry) (Fig) (Guava) (Honeydew)
|
||||
(Indian Fig) (Jackfruit) (Kiwi) (Lemon) (Mango) (Nectarine) (Orange)
|
||||
(Persimmon) (Quince) (Raspberry) (Strawberry) (Tamarind) (Ugli Fruit)
|
||||
(Voavanga) (Wolfberry) (Xigua) (Yangmei) (Zucchini)]
|
||||
/V (Banana)
|
||||
>>
|
||||
endobj
|
||||
10 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 131073
|
||||
/T (Combo_ReadOnly)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [ 100 500 200 530 ]
|
||||
/Opt [(Dog) (Elephant) (Frog)]
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 11
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000137 00000 n
|
||||
0000000202 00000 n
|
||||
0000000351 00000 n
|
||||
0000000386 00000 n
|
||||
0000000419 00000 n
|
||||
0000000495 00000 n
|
||||
0000000597 00000 n
|
||||
0000000803 00000 n
|
||||
0000001259 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 11
|
||||
>>
|
||||
startxref
|
||||
1448
|
||||
%%EOF
|
||||
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [ 0 0 200 200 ]
|
||||
/Count 1
|
||||
/Kids [ 3 0 R ]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 4 0 R
|
||||
/F2 5 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents 6 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Times-Roman
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
>>
|
||||
endobj
|
||||
6 0 obj <<
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
20 50 Td
|
||||
/F1 12 Tf
|
||||
(Hello, world!) Tj
|
||||
0 50 Td
|
||||
/F2 16 Tf
|
||||
(Goodbye, world!) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000161 00000 n
|
||||
0000000303 00000 n
|
||||
0000000381 00000 n
|
||||
0000000457 00000 n
|
||||
trailer<< /Root 1 0 R /Size 7 >>
|
||||
startxref
|
||||
578
|
||||
%%EOF
|
||||
xref
|
||||
0 0
|
||||
trailer<< /Root 1 0 R /Size 0 /Prev 578 >>
|
||||
startxref
|
||||
780
|
||||
%%EOF
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,165 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
/AcroForm <<
|
||||
/Fields [8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R]
|
||||
/DR 4 0 R
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/Count 1
|
||||
/Kids [3 0 R]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources 4 0 R
|
||||
/MediaBox [0 0 300 600]
|
||||
/Contents 7 0 R
|
||||
/Annots [8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R]
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Font 5 0 R
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/F1 6 0 R
|
||||
>>
|
||||
endobj
|
||||
6 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
>>
|
||||
endobj
|
||||
7 0 obj <<
|
||||
/Length 51
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
0 0 0 rg
|
||||
/F1 12 Tf
|
||||
100 450 Td
|
||||
(Test Form) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
8 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 0
|
||||
/T (Listbox_SingleSelect)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [100 350 200 380]
|
||||
/Opt [[(foo) (Foo)] [(bar) (Bar)] [(qux) (Qux)]]
|
||||
>>
|
||||
endobj
|
||||
9 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 2097152
|
||||
/T (Listbox_MultiSelect)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [100 400 200 430]
|
||||
/Opt [(Apple) (Banana) (Cherry) (Date) (Elderberry) (Fig) (Guava) (Honeydew)
|
||||
(Indian Fig) (Jackfruit) (Kiwi) (Lemon) (Mango) (Nectarine) (Orange)
|
||||
(Persimmon) (Quince) (Raspberry) (Strawberry) (Tamarind) (Ugli Fruit)
|
||||
(Voavanga) (Wolfberry) (Xigua) (Yangmei) (Zucchini)]
|
||||
/V (Banana)
|
||||
>>
|
||||
endobj
|
||||
10 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 1
|
||||
/T (Listbox_ReadOnly)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [100 500 200 530]
|
||||
/Opt [(Dog) (Elephant) (Frog)]
|
||||
>>
|
||||
endobj
|
||||
11 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 2097152
|
||||
/T (Listbox_MultiSelectMultipleIndices)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [100 250 200 280]
|
||||
/Opt [(Albania) (Belgium) (Croatia) (Denmark) (Estonia)]
|
||||
/I [1 3]
|
||||
>>
|
||||
endobj
|
||||
12 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 2097152
|
||||
/T (Listbox_MultiSelectMultipleValues)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [100 200 200 230]
|
||||
/Opt [(Alpha) (Beta) (Gamma) (Delta) (Epsilon)]
|
||||
/V [(Epsilon) (Gamma)]
|
||||
>>
|
||||
endobj
|
||||
13 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 2097152
|
||||
/T (Listbox_MultiSelectMultipleMismatch)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [100 150 200 180]
|
||||
/Opt [(Alligator) (Bear) (Cougar) (Deer) (Echidna)]
|
||||
/V [(Alligator) (Cougar)]
|
||||
/I [1 3 4]
|
||||
>>
|
||||
endobj
|
||||
14 0 obj <<
|
||||
/Type /Annot
|
||||
/Subtype /Widget
|
||||
/FT /Ch
|
||||
/Ff 0
|
||||
/T (Listbox_SingleSelectLastSelected)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [100 100 200 130]
|
||||
/Opt [(Alberta) (British Columbia) (Manitoba) (New Brunswick)
|
||||
(Newfoundland and Labrador) (Nova Scotia) (Ontario)
|
||||
(Prince Edward Island) (Quebec) (Saskatchewan)]
|
||||
/V (Saskatchewan)
|
||||
/TI 9
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 15
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000163 00000 n
|
||||
0000000226 00000 n
|
||||
0000000399 00000 n
|
||||
0000000434 00000 n
|
||||
0000000467 00000 n
|
||||
0000000543 00000 n
|
||||
0000000645 00000 n
|
||||
0000000850 00000 n
|
||||
0000001318 00000 n
|
||||
0000001502 00000 n
|
||||
0000001747 00000 n
|
||||
0000001996 00000 n
|
||||
0000002267 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 15
|
||||
>>
|
||||
startxref
|
||||
2642
|
||||
%%EOF
|
||||
@@ -0,0 +1,63 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [0 0 200 200]
|
||||
/Kids [3 0 R 3 0 R]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Pages
|
||||
/Kids [4 0 R 4 0 R 4 0 R]
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 5 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents 6 0 R
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Times-Roman
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj <<
|
||||
/Length 44
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
20 50 Td
|
||||
/F1 12 Tf
|
||||
(Hello, world!) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000152 00000 n
|
||||
0000000216 00000 n
|
||||
0000000342 00000 n
|
||||
0000000421 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
516
|
||||
%%EOF
|
||||
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
/AcroForm << /Fields [ 4 0 R ] /DR 5 0 R >>
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources 5 0 R
|
||||
/MediaBox [ 0 0 300 300 ]
|
||||
/Contents 8 0 R
|
||||
/Annots [ 4 0 R ]
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Type /Annot
|
||||
/FT /Tx
|
||||
/T (Text Box)
|
||||
/DA (0 0 0 rg /F1 12 Tf)
|
||||
/Rect [ 100 100 200 130 ]
|
||||
/Subtype /Widget
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Font 6 0 R >>
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /F1 7 0 R >>
|
||||
endobj
|
||||
7 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<< /Length 51 >>
|
||||
stream
|
||||
BT
|
||||
0 0 0 rg
|
||||
/F1 12 Tf
|
||||
100 150 Td
|
||||
(Test Form) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 9
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000114 00000 n
|
||||
0000000173 00000 n
|
||||
0000000309 00000 n
|
||||
0000000445 00000 n
|
||||
0000000478 00000 n
|
||||
0000000509 00000 n
|
||||
0000000585 00000 n
|
||||
trailer<< /Root 1 0 R /Size 9 >>
|
||||
startxref
|
||||
685
|
||||
%%EOF
|
||||
@@ -0,0 +1,37 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
/Collection /Test
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/Count 3
|
||||
/Kids [
|
||||
3 0 R
|
||||
]
|
||||
>>
|
||||
endobj
|
||||
% Page number 0.
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <</F1 15 0 R>>
|
||||
>>
|
||||
/MediaBox [0 0 612 792]
|
||||
/Tabs /R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 4
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000088 00000 n
|
||||
0000000176 00000 n
|
||||
trailer<< /Root 1 0 R /Size 4 >>
|
||||
startxref
|
||||
310
|
||||
%%EOF
|
||||
@@ -0,0 +1,71 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [ 0 0 200 200 ]
|
||||
/Count 1
|
||||
/Kids [ 3 0 R ]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 4 0 R
|
||||
/F2 5 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents [6 0 R 7 0 R]
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Times-Roman
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
>>
|
||||
endobj
|
||||
6 0 obj <<
|
||||
/Filter /FlateDecode
|
||||
/Length 0
|
||||
>>
|
||||
stream
|
||||
endstream
|
||||
endobj
|
||||
7 0 obj <<
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
20 50 Td
|
||||
/F1 12 Tf
|
||||
(Hello, world!) Tj
|
||||
0 50 Td
|
||||
/F2 16 Tf
|
||||
(Goodbye, world!) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000061 00000 n
|
||||
0000000154 00000 n
|
||||
0000000304 00000 n
|
||||
0000000382 00000 n
|
||||
0000000458 00000 n
|
||||
0000000531 00000 n
|
||||
trailer<< /Root 1 0 R /Size 8 >>
|
||||
startxref
|
||||
652
|
||||
%%EOF
|
||||
@@ -0,0 +1,175 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/Count 1
|
||||
/Kids [3 0 R]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Contents 4 0 R
|
||||
/MediaBox [0 0 200 200]
|
||||
/Resources <<
|
||||
/ProcSet [/PDF /Text]
|
||||
/Font <<
|
||||
/F1 5 0 R
|
||||
>>
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Length 110
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
/F1 12 Tf
|
||||
150 160 Td
|
||||
[<01>2<02>2<03>-4<02>2<04>5<05>]TJ
|
||||
ET
|
||||
|
||||
BT
|
||||
0 100 Td
|
||||
-1 0 0 1 50 40 Tm
|
||||
[<01>5<05>]TJ
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /TrueType
|
||||
/BaseFont /BAAAAA+NotoSansHebrew-Regular
|
||||
/FirstChar 0
|
||||
/FontDescriptor 6 0 R
|
||||
/LastChar 5
|
||||
/ToUnicode 8 0 R
|
||||
/Widths [600 294 235 645 397 542]
|
||||
>>
|
||||
endobj
|
||||
6 0 obj <<
|
||||
/Type /FontDescriptor
|
||||
/Ascent 1069
|
||||
/CapHeight 869
|
||||
/Descent -293
|
||||
/Flags 4
|
||||
/FontBBox [-210 -252 716 869]
|
||||
/FontFile2 7 0 R
|
||||
/FontName /BAAAAA+NotoSansHebrew-Regular
|
||||
/ItalicAngle 0
|
||||
/StemV 80
|
||||
>>
|
||||
endobj
|
||||
7 0 obj <<
|
||||
/Filter [/ASCII85Decode /FlateDecode]
|
||||
/Length 4240
|
||||
>>
|
||||
stream
|
||||
GhVOeCMXsCFZt#(%p<i9AU.uO6]Ye/ifKHXbLV!I1<gsgHY$Eg-tbPt@rk=Ap?l+FQ/u;^8JASXVQS>V
|
||||
nuG<j"nF$,Rii%*89oFrq6<34,1m)cJ0IF5(22*&"c<?^q>^jZEkc[.-qdl^dk1F&,fKT9F2)eaH/1QX
|
||||
<gN4)W0n]g:798;]<j42dYR+b7&$II`er0Do2PVi55OH.DU&3oF7^D>=p%W;pT6OOkC.gL0;aQZ%EQ]$
|
||||
CMpO/ouiOe*=oqMFW5p^ge<JWIt[XVg[cqr8aR%^g[TKTpYK&S\F4)WJbd&hhE(,./q'ef/\k-FOKm`h
|
||||
f=q)o6_0Sd-Ap"u/`'bn6!o+8Zu;5]Q^2D[J,6jbf.d4Ck't7.im"Z=^^/7D4VXdnJ*hX.ia&@S<Zl_D
|
||||
Xq`#Xlf3.[Q6GL-.IS+nj<JEQ1+=HefoeH8Qi0[)c<.\^P$4LTD?DamV./;?-PDR^8[b$k8'^LB2C4&'
|
||||
NUg=HC6k8A'.Knb7Wh)elDCX74)L`tc?`Z\Sb;i$>AElUWZdj+!8FWu:gnKPdkmGSM;%q39@mC[7<ZEH
|
||||
BP,jX746Y[8VL'e=mlrkg+[&/4Xn#1'(72pCc`cXNN+/Z0?DM&g%BgA@co7e)9J3X;X\\(5@"=N^/d""
|
||||
T`@poCcT@G`,Q43B>^KnLk;$<#C1`ZRI:qa#-tMr)R?VE.RR\7=sKe@N)#YuA]JfMaHgYN/!?c&OsZTe
|
||||
^cXe0%l-1*krrj[$8W`Wl)*K?>t9dJirfrR7MYH\=XPN5Q2qL_I4eudg4b]KRT\<OAl/T-PZ'?3dZ$p;
|
||||
<A/YSC[`O')u&1O:[moi)'2=Vd5KR72=0E6LAR(+/#@80mS5hidn)NRbS3]PMfW2+=&LegaZXffSM\+*
|
||||
%@ESe'CB7@?)cYMPG3X<A98q?pK.WHgPW4%FOOV*1/i@`;U-_0)m/crX^pV$",X/"BsMn?;R::1,iO?7
|
||||
d*"n-c#gLp.bPO!Jk.*r-gP*5[&0>a[qOG`>on_9`Lhgl?)c@P!H#2#PFffIEAQP^TS_DS:Xem?lK2!M
|
||||
h#c+\A=NN1)ZW-WA38,70l'k`MSW-$8rA0><3W]ffk"ZqCrdIAQBE9>j@mZ'<HHGofhsV-;mOl@]Mb1L
|
||||
qSIIYnkLM"Z97EVYElh]%[>>bb#jgf**OlY[IHaK5,@C./bHbDalE":hWu.1N,@;S&gl,n3:<uV$;N?L
|
||||
*(2O_s!JrF=fbn$ki"!LOY9qefX_V\<NFjuO0ngY8TpJ(,;39KIP'3^N)o:sU^S>oeCuK%7\,dgAiUN1
|
||||
E!0r_=$ol73>k&7]_*E"mUSrX[62980+uUk50ME#I*V:L4N8s+bi=/qMstu,ntJ5f[(HSa5@JhaGYD:Q
|
||||
]auqs7`nFM/]DAtfVG=ND6[_hnTB=9[;GHTf&F!MBNQP7P.LnAZ2VP"Ploh`?NTa8nEUYFj3^8B4<?t>
|
||||
4SBp^*fhF-*fhL/*r?NkO.Sgf4:WJt*n-E0W]KmAKl)1[L<V@'WjXmHBl;cn9--aK@lTjrE@*#k?-6on
|
||||
,:;=;+3B=[oI&q3p0&\dDEW8m^^H(gHjR8,7ldq.4hG]/>/mf;Hl1F^:B&.Xiqu!h(RA"GpXOmI2eJ(p
|
||||
-Ufu7H4'I5!71?\VoB`KajX::mq>=Rr&Q;g]E7s$-kZ.E)/3T4mfoT]_V3^s3J#ALKI7nF?TacHK]qhj
|
||||
L<ncjl]HN[$fPFZ;`(\GZEhSIf%/9H@>qA*jpO7b7bY=!,<-,5gQ6!\>V]AQ)^-GB`&C"Q_Tt5F^2b04
|
||||
H^dXF*&#KkrUdWPo<maGSjIV2C<A6%Y#l1eDB-11=5MCe/mQ;H^`5k(:$aRZO01rVm/hX\fYmD8f*Dj@
|
||||
M@i=qKg0)_(>4O.G!a5^E2>bO$]QiDc\2C6pdgJSEti>7EGJ-(WqC@QJiDd@NbtVSKCL`LBr7+Q7e(#%
|
||||
mfT9"IKcR0OZpuAD#Y..(\(Ya*`U>DQ9W"+'>%SR14Rbu3okUD1UoZtSAfKYf.IftGdnZe:>-X$>r]=V
|
||||
G-"HZ$ThCqcd_nA%GR%YOrq)?cB9h+fAIVB(kUJ,MLq_e+>iX=AF@bgIcap`;p5$2F+]o@5&$NLhu:.I
|
||||
2<(<.=8A484n4$P`>fQKA"tBRWbq]tDur6ODulS&f!BB<ZPSc21WW3#buP0(+9gU#Z,bH/SFFh24G?`5
|
||||
dOt)^QkNbrnhfi)90SEq&n`q[F;s7b!nXc$g#@aPQN3qbFeWl+!e=Zt`EF'M1(Bm%7HR8DbV4gg<TZ"1
|
||||
'QY,(Ci/)_["1h766XD'&ADrraHq=DH%OF$jsfp?jsbDCGm`Bc4nAYbhNNN6HSW+QHSVNRk<)"p3=KZH
|
||||
@5,_A#.Kh(3M>-L01N,n^DOYar*o;enr(m9jVO\;jVOZqjVO[\jEPdIJ4jW(1Ou\+n/U@A-XG!&R,,`f
|
||||
2W1\3O?*%L7g,>@T`!f8eMl#PBrq/m2=C@'l+[6\!TC@KAA[MWaFrFRGAPIZkLE8R^YhJV5O]iFIg_\G
|
||||
iO=6VK\u;C'>=b?fe%PhWL9K1abNRi>otj#+Z';<n+i%OCHc:GQ`]p-obCOtip.a^cf<UjY!FG)PRTLL
|
||||
+"jYG+4_li`+jB7`VE<s"4AVj".>pGko/fq8/"=:R5@Xn#Q]c2U1bel!_n`0,C>VAYsbZH&PH@FJ$f#/
|
||||
0td=Kg'>AR25F7&YOo!<mI]$*rRjT2aR!Q]%Fi)aOtJSn?+UM2'/_)[:p`0l2g4bJBatZEkin+Um_FDG
|
||||
q;M6)[I9OhMIS=<lFaD[8\^p:_H)J0l0'R9&ZuGO_>dFFCb.uJQlb(Q4<5Au742_H^C[$nr;0XJ4b3Tg
|
||||
[;48b%t<.Gdkk?8S<Vi`;Poe&S_S0PK8iOs+@Vd0N*gN+=Plp50G^#i&Kg"CSmrqAknTi"D:*>YSUrma
|
||||
g=r&8<Pj&ND`$13'HCOjG_kBC'T)*ekL$!:6XuRpIX?)+^V5+S2cXNJB2ec80tlUL?G#q-m,hbS%$"V@
|
||||
p"JQ?igh1:m'G\$*s_?+,4a,JI@3GnX*+U"_Zr.(+*>1jkFAnU9>ZXAU\g3ArglE9&,uBU"0[/n3+!B'
|
||||
W[)MC"?*YACpqa=bi'p-F[R+Y&Jh':>DS9I;-PEL7\\5FC1<[_U$dQmm+CP5IQgiD>SQGm;oRG4k5.U/
|
||||
0D?:mI/C=6)f;AM:/Ut^\F':\li2"qaZh<t\"cP;!(<3T?G(>oo5D,:=$o+C?]KqCU?geEFjWG&V"PRp
|
||||
&9$#`m3'G9"&tosqAZg/@%\C2I=Z6p1:0Yl"+@)T)so1U@.,fWm/arHR8d:MJXMQVC8K5]=Jm_KIXjdB
|
||||
83[K$Qla[fJ0ST]0*O=-3O'X`"r\'Q&`>h%Y8I@YUo\`'^eHY_XTc5&7^\ik\4-8s4VZgTU@:6TdipBk
|
||||
I9NLLkSE!O'mP*-*Z_.eTF%\P6@O])QsDsH.[1,u#K\!Fdp$""eZ=M1#$Z$(ZO5i+BWp@?TH^qnB%D7/
|
||||
h#sWFD^p`NF:Yfb85;E[:q^5Oc;Ss`!7br9/Ss$74_<U(m:SQ#s-3*J/tuGNgXsJtKtqF1bXL_J,7?@i
|
||||
'BEj1Ilon*aW+o61dn83AfE7FcXE_u9)ggC=\p"(7u!%6Y7^]'TMPP?@sg_akMgC`EYfUsdCeJO#2(2D
|
||||
hQ;Br<4m&15-"(TYi[P!iN8/Ok#@VVm[UO"bZs@LQZDI&pjXBX+4dX`csI+Oe#J=,^c=d&n:"<LdpP?/
|
||||
57NYh_**Nu%GYGtPrZ:2pA@k$5I#/DZ?u(p.mKdY_mpagr"[aHSA,e@EE+%EftN*$X1t;gS`@?f>ZH3j
|
||||
dG.dX,4]pij-88?@>Es&4"OKq4Pb_H)9:8fq`(0Ie[V>KnkIYS^43jk=+).,aXsOX(4cqM6KaT:7HMS5
|
||||
5VqW;.CtD!kNC3=e+gS7q$"nkh'kl'g`'1Rk*3jhL&JE!FZWhT#NI(1R)?@JnB0)&ZC7m>1Wud3mOB<F
|
||||
<?1UlEeaPIK4D$_#X<cE%HM2B7nrD$SI/J?MJ9$\[%L0[X#c.:>Z)WgX'F4+E];'H25pPfMT*QlPBH<B
|
||||
2jEaIeeu%@g_SZi^/+`r>koI@22thoJ$T#-Ig:8=C[r)9A@gh.nJ+P$rtk!a7oB995m%NRR_tlI7kn;C
|
||||
L?s85#0T`VHa<+&O5K'$Eo(6pn-XUq4Y6-9Re-B$rhtBq!Tfi]GEbPb2*A\G,P/,p;d\J77]S.F#9J`B
|
||||
DopA\Sjp8ME&Btm*&[^S4k/0Z~>
|
||||
endstream
|
||||
endobj
|
||||
8 0 obj <<
|
||||
/Length 377
|
||||
>>
|
||||
stream
|
||||
/CIDInit/ProcSet findresource begin
|
||||
12 dict begin
|
||||
begincmap
|
||||
/CIDSystemInfo<<
|
||||
/Registry (Adobe)
|
||||
/Ordering (UCS)
|
||||
/Supplement 0
|
||||
>> def
|
||||
/CMapName/Adobe-Identity-UCS def
|
||||
/CMapType 2 def
|
||||
1 begincodespacerange
|
||||
<00> <FF>
|
||||
endcodespacerange
|
||||
5 beginbfchar
|
||||
<01> <05DF>
|
||||
<02> <05D9>
|
||||
<03> <05DE>
|
||||
<04> <05E0>
|
||||
<05> <05D1>
|
||||
endbfchar
|
||||
endcmap
|
||||
CMapName currentdict /CMap defineresource pop
|
||||
end
|
||||
end
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 9
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000131 00000 n
|
||||
0000000309 00000 n
|
||||
0000000471 00000 n
|
||||
0000000678 00000 n
|
||||
0000000905 00000 n
|
||||
0000005238 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 9
|
||||
>>
|
||||
startxref
|
||||
5667
|
||||
%%EOF
|
||||
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [ 0 0 200 200 ]
|
||||
/Count 1
|
||||
/Kids [ 3 0 R ]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 4 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents 5 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Times-Roman
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Length 406
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
0 0 Td
|
||||
/F1 12 Tf
|
||||
0.70710678118 -0.70710678118 0.70710678118 0.70710678118 100 100 Tm
|
||||
(Hello,) Tj
|
||||
0 0 Td
|
||||
/F1 12 Tf
|
||||
-0.70710678118 -0.70710678118 0.70710678118 -0.70710678118 100 100 Tm
|
||||
( world!\r\n) Tj
|
||||
0 0 Td
|
||||
/F1 12 Tf
|
||||
-0.70710678118 0.70710678118 -0.70710678118 -0.70710678118 100 100 Tm
|
||||
(Goodbye,) Tj
|
||||
0 0 Td
|
||||
/F1 12 Tf
|
||||
0.70710678118 0.70710678118 -0.70710678118 0.70710678118 100 100 Tm
|
||||
( world!) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000161 00000 n
|
||||
0000000287 00000 n
|
||||
0000000365 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 6
|
||||
>>
|
||||
startxref
|
||||
823
|
||||
%%EOF
|
||||
@@ -0,0 +1,70 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [0 0 200 200]
|
||||
/Count 1
|
||||
/Kids [3 0 R]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 4 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents 5 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Times-Roman
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Length 210
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
0 0 Td
|
||||
/F1 12 Tf
|
||||
1 0 0 1 100 100 Tm
|
||||
(Hello,) Tj
|
||||
0 0 Td
|
||||
/F1 12 Tf
|
||||
0 1 -1 0 100 100 Tm
|
||||
( world!\r\n) Tj
|
||||
0 0 Td
|
||||
/F1 12 Tf
|
||||
-1 0 0 -1 100 100 Tm
|
||||
(Goodbye,) Tj
|
||||
0 0 Td
|
||||
/F1 12 Tf
|
||||
0 -1 1 0 100 100 Tm
|
||||
( world!) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000157 00000 n
|
||||
0000000283 00000 n
|
||||
0000000361 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 6
|
||||
>>
|
||||
startxref
|
||||
623
|
||||
%%EOF
|
||||
Binary file not shown.
@@ -0,0 +1,69 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
/Outlines 6 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/Kids [3 0 R]
|
||||
/Count 1
|
||||
>>
|
||||
endobj
|
||||
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/MediaBox [0 0 525 250]
|
||||
/Contents 4 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
4 0 obj <<
|
||||
/Length 0
|
||||
>>
|
||||
stream
|
||||
endstream
|
||||
endobj
|
||||
|
||||
5 0 obj <<
|
||||
/Producer (\357\273\277Man\303\274ally Created)
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj <<
|
||||
/Count 1
|
||||
/First 7 0 R
|
||||
/Last 7 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
7 0 obj <<
|
||||
/Title <EFBBBF5469746CC3A82031>
|
||||
/Parent 6 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000087 00000 n
|
||||
0000000151 00000 n
|
||||
0000000247 00000 n
|
||||
0000000298 00000 n
|
||||
0000000370 00000 n
|
||||
0000000432 00000 n
|
||||
|
||||
trailer <<
|
||||
/Size 8
|
||||
/Info 5 0 R
|
||||
/Root 1 0 R
|
||||
>>
|
||||
|
||||
startxref
|
||||
504
|
||||
%%EOF
|
||||
@@ -0,0 +1,146 @@
|
||||
%PDF-1.7
|
||||
% ò¤ô
|
||||
1 0 obj <<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj <<
|
||||
/Type /Pages
|
||||
/MediaBox [0 0 200 200]
|
||||
/Count 1
|
||||
/Kids [3 0 R]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj <<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 4 0 R
|
||||
>>
|
||||
>>
|
||||
/Contents 8 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /Type0
|
||||
/Encoding /UniGB-UTF16-V
|
||||
/BaseFont /Test
|
||||
/DescendantFonts [5 0 R]
|
||||
>>
|
||||
endobj
|
||||
5 0 obj <<
|
||||
/Type /Font
|
||||
/Subtype /CIDFontType2
|
||||
/BaseFont /Test
|
||||
/CIDSystemInfo <<
|
||||
/Registry (Adobe)
|
||||
/Ordering (GB1)
|
||||
/Supplement 4
|
||||
>>
|
||||
/FontDescriptor 6 0 R
|
||||
/DW 1000
|
||||
/W [
|
||||
1 [278] %space
|
||||
2 [278] %!
|
||||
41 [722] %H
|
||||
56 [944] %W
|
||||
69 [556] %d
|
||||
70 [556] %e
|
||||
77 [222] %l
|
||||
80 [556] %o
|
||||
83 [333] %r
|
||||
]
|
||||
/DW2 [0 -1000]
|
||||
/W2 [
|
||||
1 [-723 139 623] %space
|
||||
2 [-918 139 818] %!
|
||||
41 [-918 361 818] %H
|
||||
56 [-918 472 818] %W
|
||||
69 [-918 278 818] %d
|
||||
70 [-723 278 623] %e
|
||||
77 [-918 111 818] %l
|
||||
80 [-723 278 623] %o
|
||||
83 [-723 166.5 623] %r
|
||||
]
|
||||
>>
|
||||
endobj
|
||||
6 0 obj <<
|
||||
/Type /FontDescriptor
|
||||
/Ascent 718
|
||||
/CapHeight 500
|
||||
/Descent -207
|
||||
/Flags 32
|
||||
/FontBBox [-166 -225 1000 931]
|
||||
/FontFile2 7 0 R
|
||||
/FontName /Test
|
||||
/ItalicAngle 0
|
||||
/StemV 80
|
||||
>>
|
||||
endobj
|
||||
7 0 obj <<
|
||||
/Filter [/ASCII85Decode /FlateDecode]
|
||||
/Length1 2456
|
||||
/Length 1684
|
||||
>>
|
||||
stream
|
||||
GhU\K?YiY@)#qml\*a1%h:c`s)m$[;?!@Nf?/*!!0XbkOrTQd[?,p@=KMg=Kc?4![MV1<a'7u=>As+]B
|
||||
'495o@Y1$[N=Y"V^.<W,L*pI>$-"n:U;RjO^N4a)+9@>=#<aH4hsYsm^[pn0#QX)I2;k-WI'UY:m#)O\
|
||||
!g"OF:X1W4EpNXp_Z9<>jGhF!S/7idXo\$E.-Hf!Q!CSMQ;G;Ho<MnQm<%mSRl=19)qpfU-":lYlDY":
|
||||
q[SB7Kad?0hi#3nS@>;[nuK#ZMO7Yf&Pd!%@tr;<SS6@/J%QJ"c+uL"7+k>*$^oXFinbsi#J)/DIn)-'
|
||||
OtV7j?AtLdEaZ<5\9I,6ls*Q:mnh.)MB'-QHi$N,Ikc"ackl.lS#dYZ!t(lZ2Y9/.d?6]&WH`a%*Dndd
|
||||
5ZDU!@`<TjWWl6%Z2kjXXjaImr^Ma-T><lC1XMA)H$dDTq".B&f:Ti(-PZ;?<68ZDLN]"6A!sT%,6eE'
|
||||
oKP>!?&8GX>ODU?s6Y&&nC(/%5?!?Se<]Oma7S5D.CRLdfV,J@+O5cukCK0_/Q.0>kCcDhj_RFjOj_;E
|
||||
O(`NP]LJqP/$Go$)iReI-F9PG[a)Id^1$o!7U))'C_p:q:)>s-h6jks?0ZBU]Q.YOcbF+2pV*6U5[XQ,
|
||||
rW@d?_^!"FZ%J`;LW@:GO\,=t2<@P/RdFJL^;'$FBD.DEp(RB7lU7pu[rQhJ`V]-6]nJOS"lL1WRHsLe
|
||||
hR_DZX_;p-\@6oAhpP?iE7?hRHqVT![)6KsZj9"_?H1@.H*j]l_F7D08MnQa/(=+;_Nef!A>8-]iB"YP
|
||||
pmt$6^7N"dW8GP=MTAB=AL';>ZGf;7!TLS)!H.MpfumsW'pMgZ(M@ZVaQcJ<9o\,enaja=NQ/'Gcs%$/
|
||||
jG1bCp2\'8<9V+?+mGrSog0VnZuB-9'ck=UaE:OU^\E>JnWg-Y4+P?=lr)!4aqEqbZ:T.p7cO"8H:b#5
|
||||
ZtjGmlA-b#\eYg8f5$4THGmU;$[\b*p<S\VN)X%g?[l!h2TSn-+-m)tma:&BWoK!jc(07P;&hpi@)^q'
|
||||
ijFn<dVL/6fB9=p'%&=oY0I$Pg9nM(M3+$sq6%ThF=&Q"mqN=/6Y?Vs/IqN7lA19"W4chXZtjj1@Qn<\
|
||||
"jYCeT,S26":(iDrRbDhL:,rFQ#Wq09i&+Y]+j"RYmjNjZ.[F-E*0#tBPQfdLG#DalFOJ\P3maIAMi.C
|
||||
=?IFg#_aIb0k.Y3A`/iA$QB1\BBHMQ\[cf#9!QHAkgRDKWO(k8/AaqmfcFAZ!$UB&U7p#^A8D_CSdnfj
|
||||
%2!@<\-\16TC<T`cu#]7eB`[X51t6.)PK[?"Nhm_@L>0+UgafRn=tkd-ms-eej'u7=0WP:EV;`hloWXt
|
||||
=><Lb$rIa<N4Z*!%`VmD`B9OX/M(<>fSH20/b6eu!4$i^emK@2+:0L<j7d.5bX3mY0=Z#sTFGlH&uZs]
|
||||
ZkVAJ%-k]\Ci4*8+dpI\46KfOYM8AY'1.NZ+e":":csG8H*k[\)K"Nm5uo>IOA@7`#U@D`_ASTW0K)<3
|
||||
aYW/uJmllYfeL's)@7]##r<0tDW*$N97.7b["F=t;:[Fkc/aYdbK5FqQ/"^8YSA1`a5&AWZ]q)Kk>C:l
|
||||
_[=Oh?39tHUP>R@HqfVJQ<+j-)8Y_688bp6H/?'ZTdYB8rjP"t&b_Lc'A9H]a8~>
|
||||
endstream
|
||||
endobj
|
||||
8 0 obj <<
|
||||
/Length 179
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
/F1 12 Tf
|
||||
10 190 Td
|
||||
(\000H\000e\000l\000l\000o\000 ) Tj
|
||||
(\000W\000o\000r\000l\000d\000!) Tj
|
||||
ET
|
||||
BT
|
||||
/F1 12 Tf
|
||||
110 190 Td
|
||||
[(\000H) 100 (\000e) -100 (\000l\000l) 200 (\000o)] TJ
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 9
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000068 00000 n
|
||||
0000000157 00000 n
|
||||
0000000283 00000 n
|
||||
0000000408 00000 n
|
||||
0000001041 00000 n
|
||||
0000001244 00000 n
|
||||
0000003038 00000 n
|
||||
trailer <<
|
||||
/Root 1 0 R
|
||||
/Size 9
|
||||
>>
|
||||
startxref
|
||||
3270
|
||||
%%EOF
|
||||
@@ -7,9 +7,12 @@ configure_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated/pdfengine/version.hpp"
|
||||
@ONLY)
|
||||
|
||||
find_package(PNG REQUIRED)
|
||||
|
||||
add_library(pdfengine STATIC
|
||||
src/core/engine_info.cpp
|
||||
src/parser/pdfium_loader.cpp
|
||||
src/parser/pdfium_document.cpp
|
||||
src/fonts/font_face.cpp
|
||||
src/fonts/hb_shaper.cpp
|
||||
)
|
||||
@@ -29,6 +32,7 @@ target_link_libraries(pdfengine
|
||||
PRIVATE
|
||||
freetype
|
||||
harfbuzz::harfbuzz
|
||||
PNG::PNG
|
||||
)
|
||||
|
||||
if(PDFENGINE_WITH_PDFIUM)
|
||||
|
||||
@@ -1,26 +1,84 @@
|
||||
// pdfengine — document API.
|
||||
//
|
||||
// ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
// │ PLACEHOLDER. This header is the deliverable of Phase 0 Gate G0b: │
|
||||
// │ "Frozen interface contracts" — the PdfDocument / PdfPage API structs │
|
||||
// │ reviewed and locked by all three developers before Phase 1 coding. │
|
||||
// │ │
|
||||
// │ Do NOT fill in method signatures yet. Nothing proceeds until the │
|
||||
// │ contract is signed off (see docs/phase0.md, Gate G0b). │
|
||||
// │ │
|
||||
// │ Rule R2: only engine/src/parser/ may touch raw FPDF_* PDFium APIs. │
|
||||
// │ Everything else in the codebase goes through these types. │
|
||||
// └─────────────────────────────────────────────────────────────────────────┘
|
||||
#pragma once
|
||||
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace pdfengine {
|
||||
|
||||
// Opaque, owning handle to a parsed PDF document.
|
||||
// Full definition + API land at Gate G0b.
|
||||
class PdfDocument;
|
||||
enum class EngineError {
|
||||
FileNotFound,
|
||||
InvalidFormat,
|
||||
PasswordRequired,
|
||||
InvalidPassword,
|
||||
PageOutOfBounds,
|
||||
RenderFailed,
|
||||
WriteFailed,
|
||||
Unknown
|
||||
};
|
||||
|
||||
// Opaque view onto a single page of a PdfDocument.
|
||||
// Full definition + API land at Gate G0b.
|
||||
class PdfPage;
|
||||
struct DocumentMetadata {
|
||||
std::string title;
|
||||
std::string author;
|
||||
std::string creator;
|
||||
std::string producer;
|
||||
std::string creationDate;
|
||||
std::string modificationDate;
|
||||
};
|
||||
|
||||
} // namespace pdfengine
|
||||
struct PageImage {
|
||||
int width;
|
||||
int height;
|
||||
std::vector<uint8_t> data;
|
||||
};
|
||||
|
||||
struct Point2D {
|
||||
double x;
|
||||
double y;
|
||||
};
|
||||
|
||||
struct DevicePoint {
|
||||
int x;
|
||||
int y;
|
||||
};
|
||||
|
||||
class PdfPage {
|
||||
public:
|
||||
virtual ~PdfPage() = default;
|
||||
|
||||
[[nodiscard]] virtual double width() const noexcept = 0;
|
||||
[[nodiscard]] virtual double height() const noexcept = 0;
|
||||
|
||||
[[nodiscard]] virtual std::expected<PageImage, EngineError> render(int dpi = 96) const = 0;
|
||||
|
||||
[[nodiscard]] virtual std::expected<std::string, EngineError> extractText() const = 0;
|
||||
|
||||
[[nodiscard]] virtual DevicePoint pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept = 0;
|
||||
[[nodiscard]] virtual Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept = 0;
|
||||
};
|
||||
|
||||
class PdfDocument {
|
||||
public:
|
||||
virtual ~PdfDocument() = default;
|
||||
|
||||
[[nodiscard]] static std::expected<std::shared_ptr<PdfDocument>, EngineError>
|
||||
loadFromFile(const std::string& path, const std::string& password = "");
|
||||
|
||||
[[nodiscard]] static std::expected<std::shared_ptr<PdfDocument>, EngineError>
|
||||
loadFromMemory(const std::vector<uint8_t>& data, const std::string& password = "");
|
||||
|
||||
[[nodiscard]] virtual int pageCount() const noexcept = 0;
|
||||
[[nodiscard]] virtual DocumentMetadata metadata() const noexcept = 0;
|
||||
|
||||
[[nodiscard]] virtual std::expected<std::shared_ptr<PdfPage>, EngineError>
|
||||
getPage(int pageIndex) = 0;
|
||||
|
||||
virtual std::expected<void, EngineError> applyEdits(const std::string& editsJson) = 0;
|
||||
|
||||
[[nodiscard]] virtual std::expected<std::vector<uint8_t>, EngineError>
|
||||
saveIncremental() const = 0;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
#include "parser/pdfium_document.hpp"
|
||||
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
#include <fpdfview.h>
|
||||
#include <fpdf_text.h>
|
||||
#include <fpdf_save.h>
|
||||
#include <fpdf_doc.h>
|
||||
#include <png.h>
|
||||
#include "parser/pdfium_loader.hpp"
|
||||
#endif
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <csetjmp>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string utf16le_to_utf8(const char16_t* utf16, size_t length) {
|
||||
std::string utf8;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
char16_t c = utf16[i];
|
||||
if (c == 0) break;
|
||||
if (c < 0x80) {
|
||||
utf8 += static_cast<char>(c);
|
||||
} else if (c < 0x800) {
|
||||
utf8 += static_cast<char>(0xC0 | (c >> 6));
|
||||
utf8 += static_cast<char>(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
utf8 += static_cast<char>(0xE0 | (c >> 12));
|
||||
utf8 += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
|
||||
utf8 += static_cast<char>(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
return utf8;
|
||||
}
|
||||
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
struct VectorWriter : public FPDF_FILEWRITE {
|
||||
std::vector<uint8_t> buffer;
|
||||
|
||||
static int WriteBlockCallback(FPDF_FILEWRITE* pThis, const void* pData, unsigned long size) {
|
||||
auto* self = static_cast<VectorWriter*>(pThis);
|
||||
const auto* bytes = static_cast<const uint8_t*>(pData);
|
||||
self->buffer.insert(self->buffer.end(), bytes, bytes + size);
|
||||
return 1;
|
||||
}
|
||||
|
||||
VectorWriter() {
|
||||
this->version = 1;
|
||||
this->WriteBlock = &VectorWriter::WriteBlockCallback;
|
||||
}
|
||||
};
|
||||
|
||||
struct PngWriteState {
|
||||
std::vector<uint8_t>* buffer;
|
||||
};
|
||||
|
||||
void pngWriteCallback(png_structp png_ptr, png_bytep data, png_size_t length) {
|
||||
auto* state = reinterpret_cast<PngWriteState*>(png_get_io_ptr(png_ptr));
|
||||
state->buffer->insert(state->buffer->end(), data, data + length);
|
||||
}
|
||||
|
||||
void pngFlushCallback(png_structp png_ptr) {
|
||||
(void)png_ptr;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> encodeBgraToPng(const uint8_t* bgra, int width, int height, int stride) {
|
||||
std::vector<uint8_t> pngBytes;
|
||||
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||
if (!png) return {};
|
||||
|
||||
png_infop info = png_create_info_struct(png);
|
||||
if (!info) {
|
||||
png_destroy_write_struct(&png, nullptr);
|
||||
return {};
|
||||
}
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable: 4611)
|
||||
if (setjmp(png_jmpbuf(png))) {
|
||||
png_destroy_write_struct(&png, &info);
|
||||
return {};
|
||||
}
|
||||
#pragma warning(pop)
|
||||
|
||||
PngWriteState state{&pngBytes};
|
||||
png_set_write_fn(png, &state, pngWriteCallback, pngFlushCallback);
|
||||
|
||||
png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
|
||||
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
|
||||
|
||||
png_write_info(png, info);
|
||||
png_set_bgr(png);
|
||||
|
||||
std::vector<png_bytep> rowPointers(height);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
rowPointers[y] = const_cast<png_bytep>(bgra + y * stride);
|
||||
}
|
||||
|
||||
png_write_image(png, rowPointers.data());
|
||||
png_write_end(png, nullptr);
|
||||
png_destroy_write_struct(&png, &info);
|
||||
|
||||
return pngBytes;
|
||||
}
|
||||
|
||||
pdfengine::EngineError mapPdfiumError(unsigned long err, bool passwordProvided) {
|
||||
switch (err) {
|
||||
case FPDF_ERR_SUCCESS:
|
||||
return pdfengine::EngineError::Unknown;
|
||||
case FPDF_ERR_FILE:
|
||||
return pdfengine::EngineError::FileNotFound;
|
||||
case FPDF_ERR_FORMAT:
|
||||
return pdfengine::EngineError::InvalidFormat;
|
||||
case FPDF_ERR_PASSWORD:
|
||||
return passwordProvided ? pdfengine::EngineError::InvalidPassword
|
||||
: pdfengine::EngineError::PasswordRequired;
|
||||
default:
|
||||
return pdfengine::EngineError::Unknown;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
struct PdfiumGlobalInit {
|
||||
PdfiumGlobalInit() {
|
||||
pdfengine::parser::pdfiumInitLibrary();
|
||||
}
|
||||
~PdfiumGlobalInit() {
|
||||
pdfengine::parser::pdfiumDestroyLibrary();
|
||||
}
|
||||
};
|
||||
|
||||
void ensure_pdfium_initialized() {
|
||||
static PdfiumGlobalInit init;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
namespace pdfengine {
|
||||
|
||||
std::expected<std::shared_ptr<PdfDocument>, EngineError>
|
||||
PdfDocument::loadFromFile(const std::string& path, const std::string& password) {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
ensure_pdfium_initialized();
|
||||
FPDF_DOCUMENT doc = FPDF_LoadDocument(path.c_str(), password.empty() ? nullptr : password.c_str());
|
||||
if (!doc) {
|
||||
auto err = FPDF_GetLastError();
|
||||
spdlog::error("Failed to load PDF file from path: {} (error code: {})", path, err);
|
||||
return std::unexpected(mapPdfiumError(err, !password.empty()));
|
||||
}
|
||||
return std::make_shared<parser::PdfiumDocument>(doc);
|
||||
#else
|
||||
(void)path;
|
||||
(void)password;
|
||||
spdlog::error("loadFromFile failed: PDFEngine compiled without PDFium support.");
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::expected<std::shared_ptr<PdfDocument>, EngineError>
|
||||
PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string& password) {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
ensure_pdfium_initialized();
|
||||
if (data.empty()) {
|
||||
return std::unexpected(EngineError::InvalidFormat);
|
||||
}
|
||||
FPDF_DOCUMENT doc = FPDF_LoadMemDocument(data.data(), static_cast<int>(data.size()),
|
||||
password.empty() ? nullptr : password.c_str());
|
||||
if (!doc) {
|
||||
auto err = FPDF_GetLastError();
|
||||
spdlog::error("Failed to load PDF from memory (error code: {})", err);
|
||||
return std::unexpected(mapPdfiumError(err, !password.empty()));
|
||||
}
|
||||
return std::make_shared<parser::PdfiumDocument>(doc);
|
||||
#else
|
||||
(void)data;
|
||||
(void)password;
|
||||
spdlog::error("loadFromMemory failed: PDFEngine compiled without PDFium support.");
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace pdfengine::parser {
|
||||
|
||||
PdfiumPage::PdfiumPage(NativePageHandle pageHandle, int pageIndex)
|
||||
: page_(pageHandle), pageIndex_(pageIndex) {}
|
||||
|
||||
PdfiumPage::~PdfiumPage() {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
std::lock_guard<std::mutex> lock(textMutex_);
|
||||
if (textPage_) {
|
||||
FPDFText_ClosePage(textPage_);
|
||||
}
|
||||
if (page_) {
|
||||
FPDF_ClosePage(page_);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
PdfiumPage::PdfiumPage(PdfiumPage&& other) noexcept {
|
||||
*this = std::move(other);
|
||||
}
|
||||
|
||||
PdfiumPage& PdfiumPage::operator=(PdfiumPage&& other) noexcept {
|
||||
if (this != &other) {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
std::lock_guard<std::mutex> lock(textMutex_);
|
||||
if (textPage_) FPDFText_ClosePage(textPage_);
|
||||
if (page_) FPDF_ClosePage(page_);
|
||||
#endif
|
||||
page_ = other.page_;
|
||||
textPage_ = other.textPage_;
|
||||
pageIndex_ = other.pageIndex_;
|
||||
|
||||
other.page_ = nullptr;
|
||||
other.textPage_ = nullptr;
|
||||
other.pageIndex_ = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
double PdfiumPage::width() const noexcept {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
return page_ ? FPDF_GetPageWidthF(page_) : 0.0;
|
||||
#else
|
||||
return 0.0;
|
||||
#endif
|
||||
}
|
||||
|
||||
double PdfiumPage::height() const noexcept {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
return page_ ? FPDF_GetPageHeightF(page_) : 0.0;
|
||||
#else
|
||||
return 0.0;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::expected<PageImage, EngineError> PdfiumPage::render(int dpi) const {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (!page_) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
double scale = dpi / 72.0;
|
||||
int w = static_cast<int>(width() * scale);
|
||||
int h = static_cast<int>(height() * scale);
|
||||
|
||||
FPDF_BITMAP bitmap = FPDFBitmap_Create(w, h, 1);
|
||||
if (!bitmap) {
|
||||
return std::unexpected(EngineError::RenderFailed);
|
||||
}
|
||||
|
||||
FPDFBitmap_FillRect(bitmap, 0, 0, w, h, 0xFFFFFFFF);
|
||||
|
||||
FPDF_RenderPageBitmap(bitmap, page_, 0, 0, w, h, 0, 0);
|
||||
|
||||
const auto* buffer = static_cast<const uint8_t*>(FPDFBitmap_GetBuffer(bitmap));
|
||||
int stride = FPDFBitmap_GetStride(bitmap);
|
||||
|
||||
std::vector<uint8_t> pngBytes = encodeBgraToPng(buffer, w, h, stride);
|
||||
FPDFBitmap_Destroy(bitmap);
|
||||
|
||||
if (pngBytes.empty()) {
|
||||
return std::unexpected(EngineError::RenderFailed);
|
||||
}
|
||||
|
||||
return PageImage{w, h, std::move(pngBytes)};
|
||||
#else
|
||||
(void)dpi;
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::expected<std::string, EngineError> PdfiumPage::extractText() const {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (!page_) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
ensureTextPageLoaded();
|
||||
if (!textPage_) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
int charCount = FPDFText_CountChars(textPage_);
|
||||
if (charCount <= 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<unsigned short> buffer(charCount + 1, 0);
|
||||
int written = FPDFText_GetText(textPage_, 0, charCount, buffer.data());
|
||||
if (written <= 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return utf16le_to_utf8(reinterpret_cast<const char16_t*>(buffer.data()), written);
|
||||
#else
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
#endif
|
||||
}
|
||||
|
||||
DevicePoint PdfiumPage::pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (!page_) return {0, 0};
|
||||
int dx = 0, dy = 0;
|
||||
FPDF_PageToDevice(page_, 0, 0, deviceWidth, deviceHeight, rotate, pagePoint.x, pagePoint.y, &dx, &dy);
|
||||
return {dx, dy};
|
||||
#else
|
||||
(void)pagePoint; (void)deviceWidth; (void)deviceHeight; (void)rotate;
|
||||
return {0, 0};
|
||||
#endif
|
||||
}
|
||||
|
||||
Point2D PdfiumPage::deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (!page_) return {0.0, 0.0};
|
||||
double px = 0.0, py = 0.0;
|
||||
FPDF_DeviceToPage(page_, 0, 0, deviceWidth, deviceHeight, rotate, devicePoint.x, devicePoint.y, &px, &py);
|
||||
return {px, py};
|
||||
#else
|
||||
(void)devicePoint; (void)deviceWidth; (void)deviceHeight; (void)rotate;
|
||||
return {0.0, 0.0};
|
||||
#endif
|
||||
}
|
||||
|
||||
void PdfiumPage::ensureTextPageLoaded() const {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
std::lock_guard<std::mutex> lock(textMutex_);
|
||||
if (!textPage_ && page_) {
|
||||
textPage_ = FPDFText_LoadPage(page_);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle)
|
||||
: doc_(docHandle) {}
|
||||
|
||||
PdfiumDocument::~PdfiumDocument() {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (doc_) {
|
||||
FPDF_CloseDocument(doc_);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
PdfiumDocument::PdfiumDocument(PdfiumDocument&& other) noexcept {
|
||||
*this = std::move(other);
|
||||
}
|
||||
|
||||
PdfiumDocument& PdfiumDocument::operator=(PdfiumDocument&& other) noexcept {
|
||||
if (this != &other) {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (doc_) FPDF_CloseDocument(doc_);
|
||||
#endif
|
||||
doc_ = other.doc_;
|
||||
other.doc_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
int PdfiumDocument::pageCount() const noexcept {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
return doc_ ? FPDF_GetPageCount(doc_) : 0;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
DocumentMetadata PdfiumDocument::metadata() const noexcept {
|
||||
DocumentMetadata meta;
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (!doc_) return meta;
|
||||
|
||||
auto fetchMeta = [this](const char* key) -> std::string {
|
||||
unsigned long len = FPDF_GetMetaText(doc_, key, nullptr, 0);
|
||||
if (len <= 2) return ""; // 2 bytes or less is just null terminator in UTF-16
|
||||
std::vector<unsigned short> buf(len / 2);
|
||||
FPDF_GetMetaText(doc_, key, buf.data(), len);
|
||||
return utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), buf.size());
|
||||
};
|
||||
|
||||
meta.title = fetchMeta("Title");
|
||||
meta.author = fetchMeta("Author");
|
||||
meta.creator = fetchMeta("Creator");
|
||||
meta.producer = fetchMeta("Producer");
|
||||
meta.creationDate = fetchMeta("CreationDate");
|
||||
meta.modificationDate = fetchMeta("ModDate");
|
||||
#endif
|
||||
return meta;
|
||||
}
|
||||
|
||||
std::expected<std::shared_ptr<PdfPage>, EngineError> PdfiumDocument::getPage(int pageIndex) {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (!doc_) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
if (pageIndex < 0 || pageIndex >= pageCount()) {
|
||||
return std::unexpected(EngineError::PageOutOfBounds);
|
||||
}
|
||||
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
|
||||
if (!page) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
return std::make_shared<PdfiumPage>(page, pageIndex);
|
||||
#else
|
||||
(void)pageIndex;
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& editsJson) {
|
||||
(void)editsJson;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::saveIncremental() const {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (!doc_) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
VectorWriter writer;
|
||||
if (!FPDF_SaveWithVersion(doc_, &writer, FPDF_INCREMENTAL, 14)) {
|
||||
return std::unexpected(EngineError::WriteFailed);
|
||||
}
|
||||
return writer.buffer;
|
||||
#else
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <pdfengine/pdf_document.hpp>
|
||||
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
#include <fpdfview.h>
|
||||
#include <fpdf_text.h>
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
namespace pdfengine::parser {
|
||||
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
using NativeDocHandle = FPDF_DOCUMENT;
|
||||
using NativePageHandle = FPDF_PAGE;
|
||||
using NativeTextHandle = FPDF_TEXTPAGE;
|
||||
#else
|
||||
using NativeDocHandle = void*;
|
||||
using NativePageHandle = void*;
|
||||
using NativeTextHandle = void*;
|
||||
#endif
|
||||
|
||||
class PdfiumPage : public PdfPage {
|
||||
public:
|
||||
PdfiumPage(NativePageHandle pageHandle, int pageIndex);
|
||||
~PdfiumPage() override;
|
||||
|
||||
PdfiumPage(const PdfiumPage&) = delete;
|
||||
PdfiumPage& operator=(const PdfiumPage&) = delete;
|
||||
PdfiumPage(PdfiumPage&& other) noexcept;
|
||||
PdfiumPage& operator=(PdfiumPage&& other) noexcept;
|
||||
|
||||
double width() const noexcept override;
|
||||
double height() const noexcept override;
|
||||
|
||||
std::expected<PageImage, EngineError> render(int dpi = 96) const override;
|
||||
std::expected<std::string, EngineError> extractText() const override;
|
||||
|
||||
DevicePoint pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
|
||||
Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
|
||||
|
||||
private:
|
||||
NativePageHandle page_ = nullptr;
|
||||
mutable NativeTextHandle textPage_ = nullptr;
|
||||
int pageIndex_ = 0;
|
||||
mutable std::mutex textMutex_;
|
||||
|
||||
void ensureTextPageLoaded() const;
|
||||
};
|
||||
|
||||
class PdfiumDocument : public PdfDocument {
|
||||
public:
|
||||
explicit PdfiumDocument(NativeDocHandle docHandle);
|
||||
~PdfiumDocument() override;
|
||||
|
||||
PdfiumDocument(const PdfiumDocument&) = delete;
|
||||
PdfiumDocument& operator=(const PdfiumDocument&) = delete;
|
||||
PdfiumDocument(PdfiumDocument&& other) noexcept;
|
||||
PdfiumDocument& operator=(PdfiumDocument&& other) noexcept;
|
||||
|
||||
int pageCount() const noexcept override;
|
||||
DocumentMetadata metadata() const noexcept override;
|
||||
|
||||
std::expected<std::shared_ptr<PdfPage>, EngineError> getPage(int pageIndex) override;
|
||||
|
||||
std::expected<void, EngineError> applyEdits(const std::string& editsJson) override;
|
||||
std::expected<std::vector<uint8_t>, EngineError> saveIncremental() const override;
|
||||
|
||||
private:
|
||||
NativeDocHandle doc_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
add_executable(pdfengine_smoke
|
||||
smoke_test.cpp
|
||||
fonts_test.cpp
|
||||
document_test.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(pdfengine_smoke
|
||||
@@ -17,6 +18,11 @@ target_include_directories(pdfengine_smoke
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
|
||||
)
|
||||
|
||||
# Locate and normalize corpus path
|
||||
set(TEST_CORPUS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../corpus" CACHE PATH "Path to test corpus")
|
||||
file(TO_CMAKE_PATH "${TEST_CORPUS_DIR}" TEST_CORPUS_DIR_NORM)
|
||||
target_compile_definitions(pdfengine_smoke PRIVATE TEST_CORPUS_DIR="${TEST_CORPUS_DIR_NORM}")
|
||||
|
||||
pdfengine_set_warnings(pdfengine_smoke)
|
||||
pdfengine_enable_sanitizers(pdfengine_smoke)
|
||||
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <pdfengine/pdf_document.hpp>
|
||||
#include <pdfengine/pdf_engine.hpp>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#ifndef TEST_CORPUS_DIR
|
||||
#define TEST_CORPUS_DIR "../../corpus"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
#define SKIP_IF_NO_PDFIUM() \
|
||||
if (!pdfengine::engineHasPdfium()) { \
|
||||
GTEST_SKIP() << "Skipping PDFium tests because PDFium is not linked."; \
|
||||
}
|
||||
|
||||
std::filesystem::path getCorpusPath(const std::string& subfolder, const std::string& filename) {
|
||||
return std::filesystem::path(TEST_CORPUS_DIR) / subfolder / filename;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> readFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file.is_open()) {
|
||||
return {};
|
||||
}
|
||||
std::streamsize size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
std::vector<uint8_t> buffer(size);
|
||||
if (file.read(reinterpret_cast<char*>(buffer.data()), size)) {
|
||||
return buffer;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::vector<std::string> corpus_basic = {
|
||||
"about_blank.pdf", "black.pdf", "clip_path.pdf", "dashed_lines.pdf",
|
||||
"hello_world.pdf", "hello_world_2_pages.pdf", "many_rectangles.pdf",
|
||||
"rectangles.pdf", "rectangles_multi_pages.pdf", "whitespace.pdf"
|
||||
};
|
||||
|
||||
const std::vector<std::string> corpus_fonts = {
|
||||
"latin_extended.pdf", "rotated_text.pdf", "rotated_text_90.pdf",
|
||||
"text_font.pdf", "utf-8.pdf", "vertical_text.pdf", "hebrew_mirrored.pdf"
|
||||
};
|
||||
|
||||
const std::vector<std::string> corpus_edge = {
|
||||
"annots.pdf", "bookmarks.pdf", "combobox_form.pdf", "embedded_attachments.pdf",
|
||||
"empty_xref.pdf", "encrypted.pdf", "linearized.pdf", "listbox_form.pdf",
|
||||
"no_page_count.pdf", "page_labels.pdf", "text_form.pdf", "unsupported_feature.pdf",
|
||||
"zero_length_stream.pdf"
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace pdfengine {
|
||||
|
||||
TEST(DocumentLoadTest, NonExistentFileReturnsFileNotFound) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto result = PdfDocument::loadFromFile("nonexistent_file_12345.pdf");
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error(), EngineError::FileNotFound);
|
||||
}
|
||||
|
||||
TEST(DocumentLoadTest, InvalidFileReturnsInvalidFormat) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
std::string path = "invalid_format_test.pdf";
|
||||
{
|
||||
std::ofstream out(path, std::ios::binary);
|
||||
out << "NOT A PDF FILE!";
|
||||
}
|
||||
|
||||
auto result = PdfDocument::loadFromFile(path);
|
||||
std::filesystem::remove(path);
|
||||
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error(), EngineError::InvalidFormat);
|
||||
}
|
||||
|
||||
TEST(DocumentLoadTest, EncryptedPdfRequiresPassword) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("edge-cases", "encrypted.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "encrypted.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
auto result = PdfDocument::loadFromFile(path.string());
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error(), EngineError::PasswordRequired);
|
||||
}
|
||||
|
||||
TEST(DocumentLoadTest, EncryptedPdfInvalidPassword) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("edge-cases", "encrypted.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "encrypted.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
auto result = PdfDocument::loadFromFile(path.string(), "wrong_password");
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error(), EngineError::InvalidPassword);
|
||||
}
|
||||
|
||||
TEST(DocumentLoadTest, EncryptedPdfCorrectPassword) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("edge-cases", "encrypted.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "encrypted.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
std::vector<std::string> candidates = {"tessy", "test", "password", "123456", "1234", "foobar", "user", "owner", ""};
|
||||
bool success = false;
|
||||
for (const auto& pw : candidates) {
|
||||
auto result = PdfDocument::loadFromFile(path.string(), pw);
|
||||
if (result.has_value()) {
|
||||
EXPECT_GT((*result)->pageCount(), 0);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(success) << "Failed to open encrypted.pdf with any of the candidate passwords.";
|
||||
}
|
||||
|
||||
TEST(DocumentLoadTest, LoadFromMemorySuccess) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("basic", "hello_world.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
auto buffer = readFile(path);
|
||||
ASSERT_FALSE(buffer.empty());
|
||||
|
||||
auto result = PdfDocument::loadFromMemory(buffer);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ((*result)->pageCount(), 1);
|
||||
}
|
||||
|
||||
TEST(DocumentLoadTest, LoadFromMemoryEmptyBuffer) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
std::vector<uint8_t> empty_buf;
|
||||
auto result = PdfDocument::loadFromMemory(empty_buf);
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error(), EngineError::InvalidFormat);
|
||||
}
|
||||
|
||||
TEST(PageCountTest, CorpusPageCounts) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
|
||||
struct ExpectedPageCount {
|
||||
std::string folder;
|
||||
std::string file;
|
||||
int count;
|
||||
};
|
||||
|
||||
std::vector<ExpectedPageCount> targets = {
|
||||
{"basic", "about_blank.pdf", 1},
|
||||
{"basic", "black.pdf", 1},
|
||||
{"basic", "hello_world.pdf", 1},
|
||||
{"basic", "hello_world_2_pages.pdf", 2},
|
||||
{"basic", "rectangles_multi_pages.pdf", 5}
|
||||
};
|
||||
|
||||
for (const auto& t : targets) {
|
||||
auto path = getCorpusPath(t.folder, t.file);
|
||||
if (!std::filesystem::exists(path)) {
|
||||
continue;
|
||||
}
|
||||
auto docRes = PdfDocument::loadFromFile(path.string());
|
||||
ASSERT_TRUE(docRes.has_value()) << "Failed to open " << t.file;
|
||||
EXPECT_EQ((*docRes)->pageCount(), t.count) << "Mismatch for " << t.file;
|
||||
}
|
||||
|
||||
auto verifyCorpusFolder = [](const std::string& folder, const std::vector<std::string>& files) {
|
||||
for (const auto& f : files) {
|
||||
auto path = getCorpusPath(folder, f);
|
||||
if (!std::filesystem::exists(path)) {
|
||||
ADD_FAILURE() << "Missing corpus file: " << path.string();
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<std::string> passwords = {""};
|
||||
if (f == "encrypted.pdf") {
|
||||
passwords = {"tessy", "test", "password", "123456", "1234", "foobar", "user", "owner"};
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
EngineError lastErr = EngineError::Unknown;
|
||||
for (const auto& pw : passwords) {
|
||||
auto docRes = PdfDocument::loadFromFile(path.string(), pw);
|
||||
if (docRes.has_value()) {
|
||||
EXPECT_GE((*docRes)->pageCount(), 0) << "Page count negative for " << f;
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
lastErr = docRes.error();
|
||||
}
|
||||
EXPECT_TRUE(success) << "Failed to open " << f << " error: " << static_cast<int>(lastErr);
|
||||
}
|
||||
};
|
||||
|
||||
verifyCorpusFolder("basic", corpus_basic);
|
||||
verifyCorpusFolder("fonts", corpus_fonts);
|
||||
verifyCorpusFolder("edge-cases", corpus_edge);
|
||||
}
|
||||
|
||||
TEST(PageRenderTest, RenderAtDifferentDPI) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("basic", "hello_world.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
auto docRes = PdfDocument::loadFromFile(path.string());
|
||||
ASSERT_TRUE(docRes.has_value());
|
||||
auto pageRes = (*docRes)->getPage(0);
|
||||
ASSERT_TRUE(pageRes.has_value());
|
||||
|
||||
auto page = *pageRes;
|
||||
double ptWidth = page->width();
|
||||
double ptHeight = page->height();
|
||||
EXPECT_GT(ptWidth, 0.0);
|
||||
EXPECT_GT(ptHeight, 0.0);
|
||||
|
||||
std::vector<int> dpis = {72, 144, 288};
|
||||
for (int dpi : dpis) {
|
||||
auto imgRes = page->render(dpi);
|
||||
ASSERT_TRUE(imgRes.has_value()) << "Failed to render at DPI " << dpi;
|
||||
|
||||
double scale = dpi / 72.0;
|
||||
int expectedW = static_cast<int>(ptWidth * scale);
|
||||
int expectedH = static_cast<int>(ptHeight * scale);
|
||||
|
||||
EXPECT_EQ(imgRes->width, expectedW);
|
||||
EXPECT_EQ(imgRes->height, expectedH);
|
||||
|
||||
ASSERT_GE(imgRes->data.size(), 8U);
|
||||
EXPECT_EQ(imgRes->data[0], 0x89);
|
||||
EXPECT_EQ(imgRes->data[1], 'P');
|
||||
EXPECT_EQ(imgRes->data[2], 'N');
|
||||
EXPECT_EQ(imgRes->data[3], 'G');
|
||||
EXPECT_EQ(imgRes->data[4], 0x0D);
|
||||
EXPECT_EQ(imgRes->data[5], 0x0A);
|
||||
EXPECT_EQ(imgRes->data[6], 0x1A);
|
||||
EXPECT_EQ(imgRes->data[7], 0x0A);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PageRenderTest, InvalidPageIndexReturnsPageOutOfBounds) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("basic", "hello_world.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
auto docRes = PdfDocument::loadFromFile(path.string());
|
||||
ASSERT_TRUE(docRes.has_value());
|
||||
|
||||
auto pageRes = (*docRes)->getPage(1); // Page index 1 is out of bounds for 1-page doc
|
||||
ASSERT_FALSE(pageRes.has_value());
|
||||
EXPECT_EQ(pageRes.error(), EngineError::PageOutOfBounds);
|
||||
|
||||
auto pageResNeg = (*docRes)->getPage(-1);
|
||||
ASSERT_FALSE(pageResNeg.has_value());
|
||||
EXPECT_EQ(pageResNeg.error(), EngineError::PageOutOfBounds);
|
||||
}
|
||||
|
||||
TEST(CoordinateTransformTest, PageToDeviceAndBackRoundtrip) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("basic", "hello_world.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
auto docRes = PdfDocument::loadFromFile(path.string());
|
||||
ASSERT_TRUE(docRes.has_value());
|
||||
auto pageRes = (*docRes)->getPage(0);
|
||||
ASSERT_TRUE(pageRes.has_value());
|
||||
|
||||
auto page = *pageRes;
|
||||
|
||||
int deviceW = 1024;
|
||||
int deviceH = 768;
|
||||
std::vector<int> rotations = {0, 90, 180, 270};
|
||||
|
||||
std::vector<Point2D> testPoints = {
|
||||
{0.0, 0.0},
|
||||
{50.0, 50.0},
|
||||
{100.0, 200.0},
|
||||
{page->width() / 2.0, page->height() / 2.0},
|
||||
{page->width() - 10.0, page->height() - 10.0}
|
||||
};
|
||||
|
||||
for (int rotation : rotations) {
|
||||
for (const auto& pt : testPoints) {
|
||||
auto devPt = page->pageToDevice(pt, deviceW, deviceH, rotation);
|
||||
auto pagePt = page->deviceToPage(devPt, deviceW, deviceH, rotation);
|
||||
|
||||
EXPECT_NEAR(pt.x, pagePt.x, 2.0) << "Failed roundtrip for x at rotation " << rotation;
|
||||
EXPECT_NEAR(pt.y, pagePt.y, 2.0) << "Failed roundtrip for y at rotation " << rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TextExtractionTest, ExtractSimpleText) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("basic", "hello_world.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
auto docRes = PdfDocument::loadFromFile(path.string());
|
||||
ASSERT_TRUE(docRes.has_value());
|
||||
auto pageRes = (*docRes)->getPage(0);
|
||||
ASSERT_TRUE(pageRes.has_value());
|
||||
|
||||
auto textRes = (*pageRes)->extractText();
|
||||
ASSERT_TRUE(textRes.has_value());
|
||||
|
||||
std::string text = *textRes;
|
||||
EXPECT_NE(text.find("Hello"), std::string::npos);
|
||||
EXPECT_NE(text.find("world"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(TextExtractionTest, ExtractUtf8ExtendedText) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("fonts", "latin_extended.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "latin_extended.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
auto docRes = PdfDocument::loadFromFile(path.string());
|
||||
ASSERT_TRUE(docRes.has_value());
|
||||
auto pageRes = (*docRes)->getPage(0);
|
||||
ASSERT_TRUE(pageRes.has_value());
|
||||
|
||||
auto textRes = (*pageRes)->extractText();
|
||||
ASSERT_TRUE(textRes.has_value());
|
||||
|
||||
std::string text = *textRes;
|
||||
EXPECT_FALSE(text.empty());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,9 @@ def create_app() -> FastAPI:
|
||||
app.include_router(health.router)
|
||||
app.include_router(documents.router)
|
||||
app.include_router(render.router)
|
||||
app.include_router(render.compat_router)
|
||||
app.include_router(edits.router)
|
||||
app.include_router(edits.compat_router)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -1,32 +1,104 @@
|
||||
"""Document CRUD — upload, list, fetch metadata, delete.
|
||||
from typing import List
|
||||
from fastapi import APIRouter, HTTPException, status, File, UploadFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
Phase 0 placeholder: every route returns 501 because there is no engine
|
||||
to parse PDFs and no storage backend wired up. Real implementations land
|
||||
after Gate G0b (engine interface contracts) and the pybind11 module exist.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from app.services import engine
|
||||
from app.services.store import document_store
|
||||
|
||||
router = APIRouter(prefix="/documents", tags=["documents"])
|
||||
|
||||
_NOT_IMPLEMENTED = "Engine bridge (bindings/python) not yet available — Phase 0 placeholder."
|
||||
class DocumentInfoResponse(BaseModel):
|
||||
id: str
|
||||
filename: str
|
||||
sizeBytes: int
|
||||
totalPages: int
|
||||
uploadedAt: str
|
||||
status: str
|
||||
|
||||
@router.post("", response_model=DocumentInfoResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def upload_document(file: UploadFile = File(...), password: str = "") -> DocumentInfoResponse:
|
||||
if not engine.is_available():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Engine bridge (bindings/python) not yet available."
|
||||
)
|
||||
|
||||
bytes_data = await file.read()
|
||||
try:
|
||||
pdfengine = engine.require()
|
||||
doc = pdfengine.PdfDocument.load_from_memory(bytes_data, password)
|
||||
info = document_store.add_document(file.filename, bytes_data, doc)
|
||||
return DocumentInfoResponse(
|
||||
id=info["id"],
|
||||
filename=info["filename"],
|
||||
sizeBytes=info["sizeBytes"],
|
||||
totalPages=info["totalPages"],
|
||||
uploadedAt=info["uploadedAt"],
|
||||
status=info["status"]
|
||||
)
|
||||
except ValueError as e:
|
||||
detail = str(e)
|
||||
if "Password required" in detail:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Password required")
|
||||
elif "Invalid password" in detail:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password")
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Failed to load PDF: {str(e)}")
|
||||
|
||||
@router.post("", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
||||
def upload_document() -> None:
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=_NOT_IMPLEMENTED)
|
||||
@router.get("", response_model=List[DocumentInfoResponse])
|
||||
def list_documents() -> List[DocumentInfoResponse]:
|
||||
if not engine.is_available():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Engine bridge (bindings/python) not yet available."
|
||||
)
|
||||
|
||||
docs = document_store.list_documents()
|
||||
return [
|
||||
DocumentInfoResponse(
|
||||
id=d["id"],
|
||||
filename=d["filename"],
|
||||
sizeBytes=d["sizeBytes"],
|
||||
totalPages=d["totalPages"],
|
||||
uploadedAt=d["uploadedAt"],
|
||||
status=d["status"]
|
||||
)
|
||||
for d in docs
|
||||
]
|
||||
|
||||
@router.get("/{document_id}", response_model=DocumentInfoResponse)
|
||||
def get_document(document_id: str) -> DocumentInfoResponse:
|
||||
if not engine.is_available():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Engine bridge (bindings/python) not yet available."
|
||||
)
|
||||
|
||||
d = document_store.get_document(document_id)
|
||||
if not d:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||
|
||||
return DocumentInfoResponse(
|
||||
id=d["id"],
|
||||
filename=d["filename"],
|
||||
sizeBytes=d["sizeBytes"],
|
||||
totalPages=d["totalPages"],
|
||||
uploadedAt=d["uploadedAt"],
|
||||
status=d["status"]
|
||||
)
|
||||
|
||||
@router.get("", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
||||
def list_documents() -> None:
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=_NOT_IMPLEMENTED)
|
||||
|
||||
|
||||
@router.get("/{document_id}", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
||||
def get_document(document_id: str) -> None:
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=_NOT_IMPLEMENTED)
|
||||
|
||||
|
||||
@router.delete("/{document_id}", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
||||
def delete_document(document_id: str) -> None:
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=_NOT_IMPLEMENTED)
|
||||
@router.delete("/{document_id}")
|
||||
def delete_document(document_id: str):
|
||||
if not engine.is_available():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Engine bridge (bindings/python) not yet available."
|
||||
)
|
||||
|
||||
deleted = document_store.delete_document(document_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||
|
||||
return {"success": True}
|
||||
@@ -1,16 +1,59 @@
|
||||
"""Edit operations — proxies to `engine.apply_edits()` (pybind11).
|
||||
|
||||
Phase 0 placeholder. Real implementation produces an incremental save
|
||||
(append-only xref, Rule R4) and returns a new document revision id.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import List, Any
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.services import engine
|
||||
from app.services.store import document_store
|
||||
|
||||
router = APIRouter(prefix="/documents/{document_id}/edits", tags=["edits"])
|
||||
compat_router = APIRouter(tags=["edits"])
|
||||
|
||||
_NOT_IMPLEMENTED = "Engine bridge (bindings/python) not yet available — Phase 0 placeholder."
|
||||
class EditOperation(BaseModel):
|
||||
type: str
|
||||
pageIndex: int
|
||||
data: Any
|
||||
|
||||
class EditsRequest(BaseModel):
|
||||
operations: List[EditOperation]
|
||||
|
||||
@router.post("", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
||||
def apply_edits(document_id: str) -> None:
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=_NOT_IMPLEMENTED)
|
||||
def apply_edits_impl(document_id: str, request: EditsRequest):
|
||||
if not engine.is_available():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Engine bridge (bindings/python) not yet available."
|
||||
)
|
||||
|
||||
doc_info = document_store.get_document(document_id)
|
||||
if not doc_info:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||
|
||||
try:
|
||||
pdfengine = engine.require()
|
||||
doc = doc_info["doc_instance"]
|
||||
|
||||
edits_json = json.dumps(request.model_dump())
|
||||
|
||||
doc.apply_edits(edits_json)
|
||||
|
||||
new_bytes = doc.save_incremental()
|
||||
|
||||
new_doc = pdfengine.PdfDocument.load_from_memory(new_bytes)
|
||||
|
||||
new_info = document_store.add_document(
|
||||
filename=doc_info["filename"],
|
||||
bytes_data=new_bytes,
|
||||
doc_instance=new_doc
|
||||
)
|
||||
|
||||
return {"success": True, "newDocumentId": new_info["id"]}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
@router.post("")
|
||||
def apply_edits(document_id: str, request: EditsRequest):
|
||||
return apply_edits_impl(document_id, request)
|
||||
|
||||
@compat_router.post("/edits/{document_id}")
|
||||
def apply_edits_compat(document_id: str, request: EditsRequest):
|
||||
return apply_edits_impl(document_id, request)
|
||||
@@ -1,21 +1,57 @@
|
||||
"""Page rendering — proxies to `engine.render_page()` (pybind11).
|
||||
from fastapi import APIRouter, HTTPException, status, Response
|
||||
|
||||
Phase 0 placeholder. Real route returns a PNG/JPEG of the requested page
|
||||
at the requested DPI once the engine bridge exists.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from app.services import engine
|
||||
from app.services.store import document_store
|
||||
|
||||
router = APIRouter(prefix="/documents/{document_id}/pages", tags=["render"])
|
||||
compat_router = APIRouter(tags=["render"])
|
||||
|
||||
_NOT_IMPLEMENTED = "Engine bridge (bindings/python) not yet available — Phase 0 placeholder."
|
||||
@router.get("/{page_index}/render")
|
||||
def render_page(document_id: str, page_index: int, dpi: int = 96) -> Response:
|
||||
if not engine.is_available():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Engine bridge (bindings/python) not yet available."
|
||||
)
|
||||
|
||||
doc_info = document_store.get_document(document_id)
|
||||
if not doc_info:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||
|
||||
try:
|
||||
doc = doc_info["doc_instance"]
|
||||
page = doc.get_page(page_index)
|
||||
img = page.render(dpi)
|
||||
return Response(content=img.data, media_type="image/png")
|
||||
except IndexError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Page index out of bounds")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
@router.get("/{page_index}/text")
|
||||
def extract_page_text(document_id: str, page_index: int):
|
||||
if not engine.is_available():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Engine bridge (bindings/python) not yet available."
|
||||
)
|
||||
|
||||
doc_info = document_store.get_document(document_id)
|
||||
if not doc_info:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||
|
||||
try:
|
||||
doc = doc_info["doc_instance"]
|
||||
page = doc.get_page(page_index)
|
||||
text = page.extract_text()
|
||||
return {"text": text}
|
||||
except IndexError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Page index out of bounds")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
@router.get("/{page_index}/render", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
||||
def render_page(document_id: str, page_index: int, dpi: int = 96) -> None:
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=_NOT_IMPLEMENTED)
|
||||
@compat_router.get("/render/{document_id}")
|
||||
def render_page_compat(document_id: str, page: int = 0, zoom: float = 1.0, rotation: int = 0) -> Response:
|
||||
dpi = int(96 * zoom)
|
||||
return render_page(document_id, page, dpi)
|
||||
|
||||
|
||||
@router.get("/{page_index}/text", status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
||||
def extract_page_text(document_id: str, page_index: int) -> None:
|
||||
raise HTTPException(status.HTTP_501_NOT_IMPLEMENTED, detail=_NOT_IMPLEMENTED)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import threading
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
class DocumentStore:
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
self._documents: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def add_document(self, filename: str, bytes_data: bytes, doc_instance: Any) -> Dict[str, Any]:
|
||||
doc_id = str(uuid.uuid4())
|
||||
uploaded_at = datetime.utcnow().isoformat() + "Z"
|
||||
|
||||
info = {
|
||||
"id": doc_id,
|
||||
"filename": filename,
|
||||
"sizeBytes": len(bytes_data),
|
||||
"totalPages": doc_instance.page_count,
|
||||
"uploadedAt": uploaded_at,
|
||||
"status": "ready",
|
||||
"doc_instance": doc_instance,
|
||||
"bytes_data": bytes_data
|
||||
}
|
||||
|
||||
with self._lock:
|
||||
self._documents[doc_id] = info
|
||||
|
||||
return info
|
||||
|
||||
def get_document(self, doc_id: str) -> Optional[Dict[str, Any]]:
|
||||
with self._lock:
|
||||
return self._documents.get(doc_id)
|
||||
|
||||
def list_documents(self) -> List[Dict[str, Any]]:
|
||||
with self._lock:
|
||||
return list(self._documents.values())
|
||||
|
||||
def delete_document(self, doc_id: str) -> bool:
|
||||
with self._lock:
|
||||
if doc_id in self._documents:
|
||||
del self._documents[doc_id]
|
||||
return True
|
||||
return False
|
||||
|
||||
document_store = DocumentStore()
|
||||
Binary file not shown.
@@ -20,6 +20,12 @@ def test_health_does_not_require_engine(client: TestClient) -> None:
|
||||
transitively import the pybind11 module (which doesn't exist yet)."""
|
||||
import sys
|
||||
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
assert "pdfengine" not in sys.modules
|
||||
had_pdfengine = "pdfengine" in sys.modules
|
||||
pdfengine_module = sys.modules.pop("pdfengine", None)
|
||||
try:
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
assert "pdfengine" not in sys.modules
|
||||
finally:
|
||||
if had_pdfengine and pdfengine_module is not None:
|
||||
sys.modules["pdfengine"] = pdfengine_module
|
||||
@@ -1,29 +0,0 @@
|
||||
"""Every PDF-touching route returns 501 until the engine bridge exists.
|
||||
|
||||
When Gate G0b lands and `bindings/python/` is wired up, these tests will
|
||||
fail loudly — that is the cue to replace them with real route tests.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "path"),
|
||||
[
|
||||
("POST", "/documents"),
|
||||
("GET", "/documents"),
|
||||
("GET", "/documents/abc"),
|
||||
("DELETE", "/documents/abc"),
|
||||
("GET", "/documents/abc/pages/0/render"),
|
||||
("GET", "/documents/abc/pages/0/text"),
|
||||
("POST", "/documents/abc/edits"),
|
||||
],
|
||||
)
|
||||
def test_placeholder_routes_return_501(client: TestClient, method: str, path: str) -> None:
|
||||
response = client.request(method, path)
|
||||
assert response.status_code == 501, (
|
||||
f"{method} {path} returned {response.status_code}; "
|
||||
"Phase 0 placeholder routes must 501 until the engine bridge exists."
|
||||
)
|
||||
assert "engine bridge" in response.json()["detail"].lower()
|
||||
@@ -0,0 +1,159 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.services import engine
|
||||
from app.services.store import document_store
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not engine.is_available(),
|
||||
reason="pdfengine pybind11 module is not compiled/available."
|
||||
)
|
||||
|
||||
CORPUS_DIR = Path(__file__).parent.parent.parent / "corpus"
|
||||
HELLO_WORLD_PDF = CORPUS_DIR / "basic" / "hello_world.pdf"
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_store():
|
||||
with document_store._lock:
|
||||
document_store._documents.clear()
|
||||
yield
|
||||
|
||||
def test_upload_document_success(client: TestClient):
|
||||
assert HELLO_WORLD_PDF.exists(), f"Test corpus file not found at {HELLO_WORLD_PDF}"
|
||||
|
||||
with open(HELLO_WORLD_PDF, "rb") as f:
|
||||
response = client.post(
|
||||
"/documents",
|
||||
files={"file": (HELLO_WORLD_PDF.name, f, "application/pdf")}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
payload = response.json()
|
||||
assert "id" in payload
|
||||
assert payload["filename"] == HELLO_WORLD_PDF.name
|
||||
assert payload["sizeBytes"] == HELLO_WORLD_PDF.stat().st_size
|
||||
assert payload["totalPages"] == 1
|
||||
assert payload["status"] == "ready"
|
||||
|
||||
def test_upload_document_invalid(client: TestClient):
|
||||
response = client.post(
|
||||
"/documents",
|
||||
files={"file": ("test.pdf", b"not-a-pdf-file-content", "application/pdf")}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "invalid pdf" in response.json()["detail"].lower()
|
||||
|
||||
def test_list_and_get_document(client: TestClient):
|
||||
# Upload one
|
||||
with open(HELLO_WORLD_PDF, "rb") as f:
|
||||
upload_resp = client.post(
|
||||
"/documents",
|
||||
files={"file": (HELLO_WORLD_PDF.name, f, "application/pdf")}
|
||||
)
|
||||
doc_id = upload_resp.json()["id"]
|
||||
|
||||
# List
|
||||
list_resp = client.get("/documents")
|
||||
assert list_resp.status_code == 200
|
||||
docs = list_resp.json()
|
||||
assert len(docs) == 1
|
||||
assert docs[0]["id"] == doc_id
|
||||
|
||||
# Get details
|
||||
get_resp = client.get(f"/documents/{doc_id}")
|
||||
assert get_resp.status_code == 200
|
||||
assert get_resp.json()["filename"] == HELLO_WORLD_PDF.name
|
||||
|
||||
# Get non-existent
|
||||
fake_resp = client.get("/documents/non-existent-uuid")
|
||||
assert fake_resp.status_code == 404
|
||||
|
||||
def test_delete_document(client: TestClient):
|
||||
with open(HELLO_WORLD_PDF, "rb") as f:
|
||||
upload_resp = client.post(
|
||||
"/documents",
|
||||
files={"file": (HELLO_WORLD_PDF.name, f, "application/pdf")}
|
||||
)
|
||||
doc_id = upload_resp.json()["id"]
|
||||
|
||||
# Delete
|
||||
del_resp = client.delete(f"/documents/{doc_id}")
|
||||
assert del_resp.status_code == 200
|
||||
assert del_resp.json() == {"success": True}
|
||||
|
||||
# Verify deleted
|
||||
get_resp = client.get(f"/documents/{doc_id}")
|
||||
assert get_resp.status_code == 404
|
||||
|
||||
def test_render_page_standard_and_compat(client: TestClient):
|
||||
with open(HELLO_WORLD_PDF, "rb") as f:
|
||||
upload_resp = client.post(
|
||||
"/documents",
|
||||
files={"file": (HELLO_WORLD_PDF.name, f, "application/pdf")}
|
||||
)
|
||||
doc_id = upload_resp.json()["id"]
|
||||
|
||||
# Test standard test suite route
|
||||
render_resp = client.get(f"/documents/{doc_id}/pages/0/render?dpi=150")
|
||||
assert render_resp.status_code == 200
|
||||
assert render_resp.headers["content-type"] == "image/png"
|
||||
assert len(render_resp.content) > 0
|
||||
|
||||
# Test compat route (used by frontend)
|
||||
compat_resp = client.get(f"/render/{doc_id}?page=0&zoom=1.5")
|
||||
assert compat_resp.status_code == 200
|
||||
assert compat_resp.headers["content-type"] == "image/png"
|
||||
assert len(compat_resp.content) > 0
|
||||
|
||||
# Test page out of bounds
|
||||
fail_resp = client.get(f"/documents/{doc_id}/pages/5/render")
|
||||
assert fail_resp.status_code == 404
|
||||
|
||||
def test_extract_page_text(client: TestClient):
|
||||
with open(HELLO_WORLD_PDF, "rb") as f:
|
||||
upload_resp = client.post(
|
||||
"/documents",
|
||||
files={"file": (HELLO_WORLD_PDF.name, f, "application/pdf")}
|
||||
)
|
||||
doc_id = upload_resp.json()["id"]
|
||||
|
||||
text_resp = client.get(f"/documents/{doc_id}/pages/0/text")
|
||||
assert text_resp.status_code == 200
|
||||
payload = text_resp.json()
|
||||
assert "text" in payload
|
||||
assert "hello" in payload["text"].lower()
|
||||
|
||||
def test_apply_edits_and_incremental_save(client: TestClient):
|
||||
with open(HELLO_WORLD_PDF, "rb") as f:
|
||||
upload_resp = client.post(
|
||||
"/documents",
|
||||
files={"file": (HELLO_WORLD_PDF.name, f, "application/pdf")}
|
||||
)
|
||||
doc_id = upload_resp.json()["id"]
|
||||
|
||||
edits_payload = {
|
||||
"operations": [
|
||||
{
|
||||
"type": "add_text",
|
||||
"pageIndex": 0,
|
||||
"data": {
|
||||
"text": "Edited Text Annotation",
|
||||
"x": 100.0,
|
||||
"y": 150.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
edits_resp = client.post(f"/documents/{doc_id}/edits", json=edits_payload)
|
||||
assert edits_resp.status_code == 200
|
||||
payload = edits_resp.json()
|
||||
assert payload["success"] is True
|
||||
new_doc_id = payload["newDocumentId"]
|
||||
assert new_doc_id != doc_id
|
||||
|
||||
render_resp = client.get(f"/documents/{new_doc_id}/pages/0/render")
|
||||
assert render_resp.status_code == 200
|
||||
assert render_resp.headers["content-type"] == "image/png"
|
||||
@@ -0,0 +1,38 @@
|
||||
$vcpkgRoot = $env:VCPKG_ROOT
|
||||
if (-not $vcpkgRoot) {
|
||||
$vcpkgRoot = "C:\Users\furqa\vcpkg"
|
||||
$env:VCPKG_ROOT = $vcpkgRoot
|
||||
Write-Host "VCPKG_ROOT was not set. Defaulting to $vcpkgRoot" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$vcvars = "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
|
||||
if (-not (Test-Path $vcvars)) {
|
||||
$vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $vcvars)) {
|
||||
Write-Error "Could not find vcvars64.bat. Please ensure Visual Studio or Build Tools is installed."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Loading MSVC environment variables using $vcvars..." -ForegroundColor Cyan
|
||||
cmd /c "`"$vcvars`" && set" | Where-Object { $_ -match '=' } | ForEach-Object {
|
||||
$name, $value = $_ -split '=', 2
|
||||
[System.Environment]::SetEnvironmentVariable($name, $value, 'Process')
|
||||
}
|
||||
|
||||
Write-Host "Configuring CMake preset 'win-local-pdfium'..." -ForegroundColor Cyan
|
||||
cmake --preset win-local-pdfium
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "CMake configuration failed."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Building target 'pdfengine_py'..." -ForegroundColor Cyan
|
||||
cmake --build --preset win-local-pdfium --target pdfengine_py
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Build failed."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Success! The python extension is compiled and copied to the gateway." -ForegroundColor Green
|
||||
@@ -0,0 +1,68 @@
|
||||
$sourceDir = "C:\Users\furqa\pdfium-build\checkout\pdfium\testing\resources"
|
||||
$destDir = "$PSScriptRoot\..\corpus"
|
||||
|
||||
if (-not (Test-Path $sourceDir)) {
|
||||
Write-Error "PDFium resources directory not found at: $sourceDir"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$basic = @(
|
||||
"about_blank.pdf",
|
||||
"black.pdf",
|
||||
"clip_path.pdf",
|
||||
"dashed_lines.pdf",
|
||||
"hello_world.pdf",
|
||||
"hello_world_2_pages.pdf",
|
||||
"many_rectangles.pdf",
|
||||
"rectangles.pdf",
|
||||
"rectangles_multi_pages.pdf",
|
||||
"whitespace.pdf"
|
||||
)
|
||||
|
||||
$fonts = @(
|
||||
"latin_extended.pdf",
|
||||
"rotated_text.pdf",
|
||||
"rotated_text_90.pdf",
|
||||
"text_font.pdf",
|
||||
"utf-8.pdf",
|
||||
"vertical_text.pdf",
|
||||
"hebrew_mirrored.pdf"
|
||||
)
|
||||
|
||||
$edgeCases = @(
|
||||
"annots.pdf",
|
||||
"bookmarks.pdf",
|
||||
"combobox_form.pdf",
|
||||
"embedded_attachments.pdf",
|
||||
"empty_xref.pdf",
|
||||
"encrypted.pdf",
|
||||
"linearized.pdf",
|
||||
"listbox_form.pdf",
|
||||
"no_page_count.pdf",
|
||||
"page_labels.pdf",
|
||||
"text_form.pdf",
|
||||
"unsupported_feature.pdf",
|
||||
"zero_length_stream.pdf"
|
||||
)
|
||||
|
||||
Write-Host "Ensuring corpus directory structure..."
|
||||
$null = New-Item -ItemType Directory -Force -Path "$destDir\basic"
|
||||
$null = New-Item -ItemType Directory -Force -Path "$destDir\fonts"
|
||||
$null = New-Item -ItemType Directory -Force -Path "$destDir\edge-cases"
|
||||
|
||||
Write-Host "Copying basic files..."
|
||||
foreach ($f in $basic) {
|
||||
Copy-Item "$sourceDir\$f" "$destDir\basic\$f" -Force
|
||||
}
|
||||
|
||||
Write-Host "Copying font files..."
|
||||
foreach ($f in $fonts) {
|
||||
Copy-Item "$sourceDir\$f" "$destDir\fonts\$f" -Force
|
||||
}
|
||||
|
||||
Write-Host "Copying edge-case files..."
|
||||
foreach ($f in $edgeCases) {
|
||||
Copy-Item "$sourceDir\$f" "$destDir\edge-cases\$f" -Force
|
||||
}
|
||||
|
||||
Write-Host "Test corpus (30 files) populated successfully under $destDir"
|
||||
@@ -0,0 +1,10 @@
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$gatewayDir = Join-Path (Split-Path -Parent $scriptDir) "gateway"
|
||||
|
||||
Write-Host "Starting FastAPI Gateway local dev server..." -ForegroundColor Cyan
|
||||
Push-Location $gatewayDir
|
||||
try {
|
||||
& .venv\Scripts\uvicorn app.main:app --reload
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
$vcvars = "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
|
||||
if (-not (Test-Path $vcvars)) {
|
||||
$vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
|
||||
}
|
||||
|
||||
if (Test-Path $vcvars) {
|
||||
cmd /c "`"$vcvars`" && set" | Where-Object { $_ -match '=' } | ForEach-Object {
|
||||
$name, $value = $_ -split '=', 2
|
||||
[System.Environment]::SetEnvironmentVariable($name, $value, 'Process')
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Running C++ core unit tests..." -ForegroundColor Cyan
|
||||
ctest --preset win-local-pdfium
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,10 @@
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$gatewayDir = Join-Path (Split-Path -Parent $scriptDir) "gateway"
|
||||
|
||||
Write-Host "Running FastAPI Gateway integration tests..." -ForegroundColor Cyan
|
||||
Push-Location $gatewayDir
|
||||
try {
|
||||
& .venv\Scripts\pytest
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
+2
-1
@@ -13,7 +13,8 @@
|
||||
"freetype",
|
||||
"harfbuzz",
|
||||
"spdlog",
|
||||
"gtest"
|
||||
"gtest",
|
||||
"pybind11"
|
||||
],
|
||||
"builtin-baseline": "495848814af4cc2760e70f7440c2dbe66d3ff196"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user