feat: implemented wrapper and pdf doc page

This commit is contained in:
Furqan-14
2026-05-22 15:47:48 +05:30
parent d4fe33b007
commit fe627f4e19
55 changed files with 5074 additions and 101 deletions
+1
View File
@@ -65,6 +65,7 @@ if(PDFENGINE_BUILD_TESTS)
endif()
add_subdirectory(engine)
add_subdirectory(bindings)
message(STATUS "PdfEngine ${PROJECT_VERSION} configured")
+27
View File
@@ -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"
)
+125
View File
@@ -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
View File
@@ -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.
+50
View File
@@ -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
+48
View File
@@ -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
+68
View File
@@ -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
+81
View File
@@ -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
+54
View File
@@ -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
+122
View File
@@ -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
+57
View File
@@ -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
+395
View File
@@ -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
+162
View File
@@ -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
+109
View File
@@ -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.
+70
View File
@@ -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.
+165
View File
@@ -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
+63
View File
@@ -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.
+70
View File
@@ -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
+37
View File
@@ -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
+71
View File
@@ -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
+175
View File
@@ -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.
+70
View File
@@ -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
+70
View File
@@ -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.
+69
View File
@@ -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
+146
View File
@@ -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
+4
View File
@@ -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)
+78 -20
View File
@@ -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;
};
}
+433
View File
@@ -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
}
}
+77
View File
@@ -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;
};
}
+6
View File
@@ -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)
+346
View File
@@ -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());
}
}
+2
View File
@@ -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
+96 -24
View File
@@ -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}
+53 -10
View File
@@ -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)
+50 -14
View File
@@ -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)
+46
View File
@@ -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.
+9 -3
View File
@@ -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
-29
View File
@@ -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()
+159
View File
@@ -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"
+38
View File
@@ -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
+68
View File
@@ -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"
+10
View File
@@ -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
}
+15
View File
@@ -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
+10
View File
@@ -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
View File
@@ -13,7 +13,8 @@
"freetype",
"harfbuzz",
"spdlog",
"gtest"
"gtest",
"pybind11"
],
"builtin-baseline": "495848814af4cc2760e70f7440c2dbe66d3ff196"
}