diff --git a/CMakeLists.txt b/CMakeLists.txt index 59c4502..6d206ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,7 @@ if(PDFENGINE_BUILD_TESTS) endif() add_subdirectory(engine) +add_subdirectory(bindings) message(STATUS "PdfEngine ${PROJECT_VERSION} configured") diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt new file mode 100644 index 0000000..7302c5c --- /dev/null +++ b/bindings/CMakeLists.txt @@ -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 $ "${CMAKE_SOURCE_DIR}/gateway/" + COMMENT "Copying compiled Python extension to gateway/ directory" +) diff --git a/bindings/python/pdfengine_py.cpp b/bindings/python/pdfengine_py.cpp new file mode 100644 index 0000000..1299b82 --- /dev/null +++ b/bindings/python/pdfengine_py.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include + +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 +T get_or_throw(std::expected&& res) { + if (!res.has_value()) { + throw_on_error(res.error()); + } + return std::move(res.value()); +} + +void get_or_throw(std::expected&& 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_(m, "Point2D") + .def(py::init(), 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_(m, "DevicePoint") + .def(py::init(), 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_(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_(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(self.data.data()), self.data.size()); + }); + + py::class_>(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_>(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 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 res = get_or_throw(self.saveIncremental()); + return py::bytes(reinterpret_cast(res.data()), res.size()); + }); +} \ No newline at end of file diff --git a/command.MD b/command.MD index 846735c..dad5e70 100644 --- a/command.MD +++ b/command.MD @@ -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 diff --git a/corpus/basic/about_blank.pdf b/corpus/basic/about_blank.pdf new file mode 100644 index 0000000..d640582 Binary files /dev/null and b/corpus/basic/about_blank.pdf differ diff --git a/corpus/basic/black.pdf b/corpus/basic/black.pdf new file mode 100644 index 0000000..8513d9f Binary files /dev/null and b/corpus/basic/black.pdf differ diff --git a/corpus/basic/clip_path.pdf b/corpus/basic/clip_path.pdf new file mode 100644 index 0000000..8d27bcb --- /dev/null +++ b/corpus/basic/clip_path.pdf @@ -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 diff --git a/corpus/basic/dashed_lines.pdf b/corpus/basic/dashed_lines.pdf new file mode 100644 index 0000000..f40a591 --- /dev/null +++ b/corpus/basic/dashed_lines.pdf @@ -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 diff --git a/corpus/basic/hello_world.pdf b/corpus/basic/hello_world.pdf new file mode 100644 index 0000000..48c714b --- /dev/null +++ b/corpus/basic/hello_world.pdf @@ -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 diff --git a/corpus/basic/hello_world_2_pages.pdf b/corpus/basic/hello_world_2_pages.pdf new file mode 100644 index 0000000..4942e3a --- /dev/null +++ b/corpus/basic/hello_world_2_pages.pdf @@ -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 diff --git a/corpus/basic/many_rectangles.pdf b/corpus/basic/many_rectangles.pdf new file mode 100644 index 0000000..8030501 --- /dev/null +++ b/corpus/basic/many_rectangles.pdf @@ -0,0 +1,1243 @@ +%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 << +>> +stream +q + +0.050000 0.033333 0.275000 rg +1.000000 1.000000 8.000000 8.000000 re B* +0.100000 0.033333 0.250000 rg +11.000000 1.000000 8.000000 8.000000 re B* +0.150000 0.033333 0.225000 rg +21.000000 1.000000 8.000000 8.000000 re B* +0.200000 0.033333 0.200000 rg +31.000000 1.000000 8.000000 8.000000 re B* +0.250000 0.033333 0.175000 rg +41.000000 1.000000 8.000000 8.000000 re B* +0.300000 0.033333 0.150000 rg +51.000000 1.000000 8.000000 8.000000 re B* +0.350000 0.033333 0.125000 rg +61.000000 1.000000 8.000000 8.000000 re B* +0.400000 0.033333 0.100000 rg +71.000000 1.000000 8.000000 8.000000 re B* +0.450000 0.033333 0.075000 rg +81.000000 1.000000 8.000000 8.000000 re B* +0.500000 0.033333 0.050000 rg +91.000000 1.000000 8.000000 8.000000 re B* +0.550000 0.033333 0.025000 rg +101.000000 1.000000 8.000000 8.000000 re B* +0.600000 0.033333 0.000000 rg +111.000000 1.000000 8.000000 8.000000 re B* +0.650000 0.033333 -0.025000 rg +121.000000 1.000000 8.000000 8.000000 re B* +0.700000 0.033333 -0.050000 rg +131.000000 1.000000 8.000000 8.000000 re B* +0.750000 0.033333 -0.075000 rg +141.000000 1.000000 8.000000 8.000000 re B* +0.800000 0.033333 -0.100000 rg +151.000000 1.000000 8.000000 8.000000 re B* +0.850000 0.033333 -0.125000 rg +161.000000 1.000000 8.000000 8.000000 re B* +0.900000 0.033333 -0.150000 rg +171.000000 1.000000 8.000000 8.000000 re B* +0.950000 0.033333 -0.175000 rg +181.000000 1.000000 8.000000 8.000000 re B* +1.000000 0.033333 -0.200000 rg +191.000000 1.000000 8.000000 8.000000 re B* +0.050000 0.066667 0.300000 rg +1.000000 11.000000 8.000000 8.000000 re B* +0.100000 0.066667 0.275000 rg +11.000000 11.000000 8.000000 8.000000 re B* +0.150000 0.066667 0.250000 rg +21.000000 11.000000 8.000000 8.000000 re B* +0.200000 0.066667 0.225000 rg +31.000000 11.000000 8.000000 8.000000 re B* +0.250000 0.066667 0.200000 rg +41.000000 11.000000 8.000000 8.000000 re B* +0.300000 0.066667 0.175000 rg +51.000000 11.000000 8.000000 8.000000 re B* +0.350000 0.066667 0.150000 rg +61.000000 11.000000 8.000000 8.000000 re B* +0.400000 0.066667 0.125000 rg +71.000000 11.000000 8.000000 8.000000 re B* +0.450000 0.066667 0.100000 rg +81.000000 11.000000 8.000000 8.000000 re B* +0.500000 0.066667 0.075000 rg +91.000000 11.000000 8.000000 8.000000 re B* +0.550000 0.066667 0.050000 rg +101.000000 11.000000 8.000000 8.000000 re B* +0.600000 0.066667 0.025000 rg +111.000000 11.000000 8.000000 8.000000 re B* +0.650000 0.066667 0.000000 rg +121.000000 11.000000 8.000000 8.000000 re B* +0.700000 0.066667 -0.025000 rg +131.000000 11.000000 8.000000 8.000000 re B* +0.750000 0.066667 -0.050000 rg +141.000000 11.000000 8.000000 8.000000 re B* +0.800000 0.066667 -0.075000 rg +151.000000 11.000000 8.000000 8.000000 re B* +0.850000 0.066667 -0.100000 rg +161.000000 11.000000 8.000000 8.000000 re B* +0.900000 0.066667 -0.125000 rg +171.000000 11.000000 8.000000 8.000000 re B* +0.950000 0.066667 -0.150000 rg +181.000000 11.000000 8.000000 8.000000 re B* +1.000000 0.066667 -0.175000 rg +191.000000 11.000000 8.000000 8.000000 re B* +0.050000 0.100000 0.325000 rg +1.000000 21.000000 8.000000 8.000000 re B* +0.100000 0.100000 0.300000 rg +11.000000 21.000000 8.000000 8.000000 re B* +0.150000 0.100000 0.275000 rg +21.000000 21.000000 8.000000 8.000000 re B* +0.200000 0.100000 0.250000 rg +31.000000 21.000000 8.000000 8.000000 re B* +0.250000 0.100000 0.225000 rg +41.000000 21.000000 8.000000 8.000000 re B* +0.300000 0.100000 0.200000 rg +51.000000 21.000000 8.000000 8.000000 re B* +0.350000 0.100000 0.175000 rg +61.000000 21.000000 8.000000 8.000000 re B* +0.400000 0.100000 0.150000 rg +71.000000 21.000000 8.000000 8.000000 re B* +0.450000 0.100000 0.125000 rg +81.000000 21.000000 8.000000 8.000000 re B* +0.500000 0.100000 0.100000 rg +91.000000 21.000000 8.000000 8.000000 re B* +0.550000 0.100000 0.075000 rg +101.000000 21.000000 8.000000 8.000000 re B* +0.600000 0.100000 0.050000 rg +111.000000 21.000000 8.000000 8.000000 re B* +0.650000 0.100000 0.025000 rg +121.000000 21.000000 8.000000 8.000000 re B* +0.700000 0.100000 0.000000 rg +131.000000 21.000000 8.000000 8.000000 re B* +0.750000 0.100000 -0.025000 rg +141.000000 21.000000 8.000000 8.000000 re B* +0.800000 0.100000 -0.050000 rg +151.000000 21.000000 8.000000 8.000000 re B* +0.850000 0.100000 -0.075000 rg +161.000000 21.000000 8.000000 8.000000 re B* +0.900000 0.100000 -0.100000 rg +171.000000 21.000000 8.000000 8.000000 re B* +0.950000 0.100000 -0.125000 rg +181.000000 21.000000 8.000000 8.000000 re B* +1.000000 0.100000 -0.150000 rg +191.000000 21.000000 8.000000 8.000000 re B* +0.050000 0.133333 0.350000 rg +1.000000 31.000000 8.000000 8.000000 re B* +0.100000 0.133333 0.325000 rg +11.000000 31.000000 8.000000 8.000000 re B* +0.150000 0.133333 0.300000 rg +21.000000 31.000000 8.000000 8.000000 re B* +0.200000 0.133333 0.275000 rg +31.000000 31.000000 8.000000 8.000000 re B* +0.250000 0.133333 0.250000 rg +41.000000 31.000000 8.000000 8.000000 re B* +0.300000 0.133333 0.225000 rg +51.000000 31.000000 8.000000 8.000000 re B* +0.350000 0.133333 0.200000 rg +61.000000 31.000000 8.000000 8.000000 re B* +0.400000 0.133333 0.175000 rg +71.000000 31.000000 8.000000 8.000000 re B* +0.450000 0.133333 0.150000 rg +81.000000 31.000000 8.000000 8.000000 re B* +0.500000 0.133333 0.125000 rg +91.000000 31.000000 8.000000 8.000000 re B* +0.550000 0.133333 0.100000 rg +101.000000 31.000000 8.000000 8.000000 re B* +0.600000 0.133333 0.075000 rg +111.000000 31.000000 8.000000 8.000000 re B* +0.650000 0.133333 0.050000 rg +121.000000 31.000000 8.000000 8.000000 re B* +0.700000 0.133333 0.025000 rg +131.000000 31.000000 8.000000 8.000000 re B* +0.750000 0.133333 0.000000 rg +141.000000 31.000000 8.000000 8.000000 re B* +0.800000 0.133333 -0.025000 rg +151.000000 31.000000 8.000000 8.000000 re B* +0.850000 0.133333 -0.050000 rg +161.000000 31.000000 8.000000 8.000000 re B* +0.900000 0.133333 -0.075000 rg +171.000000 31.000000 8.000000 8.000000 re B* +0.950000 0.133333 -0.100000 rg +181.000000 31.000000 8.000000 8.000000 re B* +1.000000 0.133333 -0.125000 rg +191.000000 31.000000 8.000000 8.000000 re B* +0.050000 0.166667 0.375000 rg +1.000000 41.000000 8.000000 8.000000 re B* +0.100000 0.166667 0.350000 rg +11.000000 41.000000 8.000000 8.000000 re B* +0.150000 0.166667 0.325000 rg +21.000000 41.000000 8.000000 8.000000 re B* +0.200000 0.166667 0.300000 rg +31.000000 41.000000 8.000000 8.000000 re B* +0.250000 0.166667 0.275000 rg +41.000000 41.000000 8.000000 8.000000 re B* +0.300000 0.166667 0.250000 rg +51.000000 41.000000 8.000000 8.000000 re B* +0.350000 0.166667 0.225000 rg +61.000000 41.000000 8.000000 8.000000 re B* +0.400000 0.166667 0.200000 rg +71.000000 41.000000 8.000000 8.000000 re B* +0.450000 0.166667 0.175000 rg +81.000000 41.000000 8.000000 8.000000 re B* +0.500000 0.166667 0.150000 rg +91.000000 41.000000 8.000000 8.000000 re B* +0.550000 0.166667 0.125000 rg +101.000000 41.000000 8.000000 8.000000 re B* +0.600000 0.166667 0.100000 rg +111.000000 41.000000 8.000000 8.000000 re B* +0.650000 0.166667 0.075000 rg +121.000000 41.000000 8.000000 8.000000 re B* +0.700000 0.166667 0.050000 rg +131.000000 41.000000 8.000000 8.000000 re B* +0.750000 0.166667 0.025000 rg +141.000000 41.000000 8.000000 8.000000 re B* +0.800000 0.166667 0.000000 rg +151.000000 41.000000 8.000000 8.000000 re B* +0.850000 0.166667 -0.025000 rg +161.000000 41.000000 8.000000 8.000000 re B* +0.900000 0.166667 -0.050000 rg +171.000000 41.000000 8.000000 8.000000 re B* +0.950000 0.166667 -0.075000 rg +181.000000 41.000000 8.000000 8.000000 re B* +1.000000 0.166667 -0.100000 rg +191.000000 41.000000 8.000000 8.000000 re B* +0.050000 0.200000 0.400000 rg +1.000000 51.000000 8.000000 8.000000 re B* +0.100000 0.200000 0.375000 rg +11.000000 51.000000 8.000000 8.000000 re B* +0.150000 0.200000 0.350000 rg +21.000000 51.000000 8.000000 8.000000 re B* +0.200000 0.200000 0.325000 rg +31.000000 51.000000 8.000000 8.000000 re B* +0.250000 0.200000 0.300000 rg +41.000000 51.000000 8.000000 8.000000 re B* +0.300000 0.200000 0.275000 rg +51.000000 51.000000 8.000000 8.000000 re B* +0.350000 0.200000 0.250000 rg +61.000000 51.000000 8.000000 8.000000 re B* +0.400000 0.200000 0.225000 rg +71.000000 51.000000 8.000000 8.000000 re B* +0.450000 0.200000 0.200000 rg +81.000000 51.000000 8.000000 8.000000 re B* +0.500000 0.200000 0.175000 rg +91.000000 51.000000 8.000000 8.000000 re B* +0.550000 0.200000 0.150000 rg +101.000000 51.000000 8.000000 8.000000 re B* +0.600000 0.200000 0.125000 rg +111.000000 51.000000 8.000000 8.000000 re B* +0.650000 0.200000 0.100000 rg +121.000000 51.000000 8.000000 8.000000 re B* +0.700000 0.200000 0.075000 rg +131.000000 51.000000 8.000000 8.000000 re B* +0.750000 0.200000 0.050000 rg +141.000000 51.000000 8.000000 8.000000 re B* +0.800000 0.200000 0.025000 rg +151.000000 51.000000 8.000000 8.000000 re B* +0.850000 0.200000 0.000000 rg +161.000000 51.000000 8.000000 8.000000 re B* +0.900000 0.200000 -0.025000 rg +171.000000 51.000000 8.000000 8.000000 re B* +0.950000 0.200000 -0.050000 rg +181.000000 51.000000 8.000000 8.000000 re B* +1.000000 0.200000 -0.075000 rg +191.000000 51.000000 8.000000 8.000000 re B* +0.050000 0.233333 0.425000 rg +1.000000 61.000000 8.000000 8.000000 re B* +0.100000 0.233333 0.400000 rg +11.000000 61.000000 8.000000 8.000000 re B* +0.150000 0.233333 0.375000 rg +21.000000 61.000000 8.000000 8.000000 re B* +0.200000 0.233333 0.350000 rg +31.000000 61.000000 8.000000 8.000000 re B* +0.250000 0.233333 0.325000 rg +41.000000 61.000000 8.000000 8.000000 re B* +0.300000 0.233333 0.300000 rg +51.000000 61.000000 8.000000 8.000000 re B* +0.350000 0.233333 0.275000 rg +61.000000 61.000000 8.000000 8.000000 re B* +0.400000 0.233333 0.250000 rg +71.000000 61.000000 8.000000 8.000000 re B* +0.450000 0.233333 0.225000 rg +81.000000 61.000000 8.000000 8.000000 re B* +0.500000 0.233333 0.200000 rg +91.000000 61.000000 8.000000 8.000000 re B* +0.550000 0.233333 0.175000 rg +101.000000 61.000000 8.000000 8.000000 re B* +0.600000 0.233333 0.150000 rg +111.000000 61.000000 8.000000 8.000000 re B* +0.650000 0.233333 0.125000 rg +121.000000 61.000000 8.000000 8.000000 re B* +0.700000 0.233333 0.100000 rg +131.000000 61.000000 8.000000 8.000000 re B* +0.750000 0.233333 0.075000 rg +141.000000 61.000000 8.000000 8.000000 re B* +0.800000 0.233333 0.050000 rg +151.000000 61.000000 8.000000 8.000000 re B* +0.850000 0.233333 0.025000 rg +161.000000 61.000000 8.000000 8.000000 re B* +0.900000 0.233333 0.000000 rg +171.000000 61.000000 8.000000 8.000000 re B* +0.950000 0.233333 -0.025000 rg +181.000000 61.000000 8.000000 8.000000 re B* +1.000000 0.233333 -0.050000 rg +191.000000 61.000000 8.000000 8.000000 re B* +0.050000 0.266667 0.450000 rg +1.000000 71.000000 8.000000 8.000000 re B* +0.100000 0.266667 0.425000 rg +11.000000 71.000000 8.000000 8.000000 re B* +0.150000 0.266667 0.400000 rg +21.000000 71.000000 8.000000 8.000000 re B* +0.200000 0.266667 0.375000 rg +31.000000 71.000000 8.000000 8.000000 re B* +0.250000 0.266667 0.350000 rg +41.000000 71.000000 8.000000 8.000000 re B* +0.300000 0.266667 0.325000 rg +51.000000 71.000000 8.000000 8.000000 re B* +0.350000 0.266667 0.300000 rg +61.000000 71.000000 8.000000 8.000000 re B* +0.400000 0.266667 0.275000 rg +71.000000 71.000000 8.000000 8.000000 re B* +0.450000 0.266667 0.250000 rg +81.000000 71.000000 8.000000 8.000000 re B* +0.500000 0.266667 0.225000 rg +91.000000 71.000000 8.000000 8.000000 re B* +0.550000 0.266667 0.200000 rg +101.000000 71.000000 8.000000 8.000000 re B* +0.600000 0.266667 0.175000 rg +111.000000 71.000000 8.000000 8.000000 re B* +0.650000 0.266667 0.150000 rg +121.000000 71.000000 8.000000 8.000000 re B* +0.700000 0.266667 0.125000 rg +131.000000 71.000000 8.000000 8.000000 re B* +0.750000 0.266667 0.100000 rg +141.000000 71.000000 8.000000 8.000000 re B* +0.800000 0.266667 0.075000 rg +151.000000 71.000000 8.000000 8.000000 re B* +0.850000 0.266667 0.050000 rg +161.000000 71.000000 8.000000 8.000000 re B* +0.900000 0.266667 0.025000 rg +171.000000 71.000000 8.000000 8.000000 re B* +0.950000 0.266667 0.000000 rg +181.000000 71.000000 8.000000 8.000000 re B* +1.000000 0.266667 -0.025000 rg +191.000000 71.000000 8.000000 8.000000 re B* +0.050000 0.300000 0.475000 rg +1.000000 81.000000 8.000000 8.000000 re B* +0.100000 0.300000 0.450000 rg +11.000000 81.000000 8.000000 8.000000 re B* +0.150000 0.300000 0.425000 rg +21.000000 81.000000 8.000000 8.000000 re B* +0.200000 0.300000 0.400000 rg +31.000000 81.000000 8.000000 8.000000 re B* +0.250000 0.300000 0.375000 rg +41.000000 81.000000 8.000000 8.000000 re B* +0.300000 0.300000 0.350000 rg +51.000000 81.000000 8.000000 8.000000 re B* +0.350000 0.300000 0.325000 rg +61.000000 81.000000 8.000000 8.000000 re B* +0.400000 0.300000 0.300000 rg +71.000000 81.000000 8.000000 8.000000 re B* +0.450000 0.300000 0.275000 rg +81.000000 81.000000 8.000000 8.000000 re B* +0.500000 0.300000 0.250000 rg +91.000000 81.000000 8.000000 8.000000 re B* +0.550000 0.300000 0.225000 rg +101.000000 81.000000 8.000000 8.000000 re B* +0.600000 0.300000 0.200000 rg +111.000000 81.000000 8.000000 8.000000 re B* +0.650000 0.300000 0.175000 rg +121.000000 81.000000 8.000000 8.000000 re B* +0.700000 0.300000 0.150000 rg +131.000000 81.000000 8.000000 8.000000 re B* +0.750000 0.300000 0.125000 rg +141.000000 81.000000 8.000000 8.000000 re B* +0.800000 0.300000 0.100000 rg +151.000000 81.000000 8.000000 8.000000 re B* +0.850000 0.300000 0.075000 rg +161.000000 81.000000 8.000000 8.000000 re B* +0.900000 0.300000 0.050000 rg +171.000000 81.000000 8.000000 8.000000 re B* +0.950000 0.300000 0.025000 rg +181.000000 81.000000 8.000000 8.000000 re B* +1.000000 0.300000 0.000000 rg +191.000000 81.000000 8.000000 8.000000 re B* +0.050000 0.333333 0.500000 rg +1.000000 91.000000 8.000000 8.000000 re B* +0.100000 0.333333 0.475000 rg +11.000000 91.000000 8.000000 8.000000 re B* +0.150000 0.333333 0.450000 rg +21.000000 91.000000 8.000000 8.000000 re B* +0.200000 0.333333 0.425000 rg +31.000000 91.000000 8.000000 8.000000 re B* +0.250000 0.333333 0.400000 rg +41.000000 91.000000 8.000000 8.000000 re B* +0.300000 0.333333 0.375000 rg +51.000000 91.000000 8.000000 8.000000 re B* +0.350000 0.333333 0.350000 rg +61.000000 91.000000 8.000000 8.000000 re B* +0.400000 0.333333 0.325000 rg +71.000000 91.000000 8.000000 8.000000 re B* +0.450000 0.333333 0.300000 rg +81.000000 91.000000 8.000000 8.000000 re B* +0.500000 0.333333 0.275000 rg +91.000000 91.000000 8.000000 8.000000 re B* +0.550000 0.333333 0.250000 rg +101.000000 91.000000 8.000000 8.000000 re B* +0.600000 0.333333 0.225000 rg +111.000000 91.000000 8.000000 8.000000 re B* +0.650000 0.333333 0.200000 rg +121.000000 91.000000 8.000000 8.000000 re B* +0.700000 0.333333 0.175000 rg +131.000000 91.000000 8.000000 8.000000 re B* +0.750000 0.333333 0.150000 rg +141.000000 91.000000 8.000000 8.000000 re B* +0.800000 0.333333 0.125000 rg +151.000000 91.000000 8.000000 8.000000 re B* +0.850000 0.333333 0.100000 rg +161.000000 91.000000 8.000000 8.000000 re B* +0.900000 0.333333 0.075000 rg +171.000000 91.000000 8.000000 8.000000 re B* +0.950000 0.333333 0.050000 rg +181.000000 91.000000 8.000000 8.000000 re B* +1.000000 0.333333 0.025000 rg +191.000000 91.000000 8.000000 8.000000 re B* +0.050000 0.366667 0.525000 rg +1.000000 101.000000 8.000000 8.000000 re B* +0.100000 0.366667 0.500000 rg +11.000000 101.000000 8.000000 8.000000 re B* +0.150000 0.366667 0.475000 rg +21.000000 101.000000 8.000000 8.000000 re B* +0.200000 0.366667 0.450000 rg +31.000000 101.000000 8.000000 8.000000 re B* +0.250000 0.366667 0.425000 rg +41.000000 101.000000 8.000000 8.000000 re B* +0.300000 0.366667 0.400000 rg +51.000000 101.000000 8.000000 8.000000 re B* +0.350000 0.366667 0.375000 rg +61.000000 101.000000 8.000000 8.000000 re B* +0.400000 0.366667 0.350000 rg +71.000000 101.000000 8.000000 8.000000 re B* +0.450000 0.366667 0.325000 rg +81.000000 101.000000 8.000000 8.000000 re B* +0.500000 0.366667 0.300000 rg +91.000000 101.000000 8.000000 8.000000 re B* +0.550000 0.366667 0.275000 rg +101.000000 101.000000 8.000000 8.000000 re B* +0.600000 0.366667 0.250000 rg +111.000000 101.000000 8.000000 8.000000 re B* +0.650000 0.366667 0.225000 rg +121.000000 101.000000 8.000000 8.000000 re B* +0.700000 0.366667 0.200000 rg +131.000000 101.000000 8.000000 8.000000 re B* +0.750000 0.366667 0.175000 rg +141.000000 101.000000 8.000000 8.000000 re B* +0.800000 0.366667 0.150000 rg +151.000000 101.000000 8.000000 8.000000 re B* +0.850000 0.366667 0.125000 rg +161.000000 101.000000 8.000000 8.000000 re B* +0.900000 0.366667 0.100000 rg +171.000000 101.000000 8.000000 8.000000 re B* +0.950000 0.366667 0.075000 rg +181.000000 101.000000 8.000000 8.000000 re B* +1.000000 0.366667 0.050000 rg +191.000000 101.000000 8.000000 8.000000 re B* +0.050000 0.400000 0.550000 rg +1.000000 111.000000 8.000000 8.000000 re B* +0.100000 0.400000 0.525000 rg +11.000000 111.000000 8.000000 8.000000 re B* +0.150000 0.400000 0.500000 rg +21.000000 111.000000 8.000000 8.000000 re B* +0.200000 0.400000 0.475000 rg +31.000000 111.000000 8.000000 8.000000 re B* +0.250000 0.400000 0.450000 rg +41.000000 111.000000 8.000000 8.000000 re B* +0.300000 0.400000 0.425000 rg +51.000000 111.000000 8.000000 8.000000 re B* +0.350000 0.400000 0.400000 rg +61.000000 111.000000 8.000000 8.000000 re B* +0.400000 0.400000 0.375000 rg +71.000000 111.000000 8.000000 8.000000 re B* +0.450000 0.400000 0.350000 rg +81.000000 111.000000 8.000000 8.000000 re B* +0.500000 0.400000 0.325000 rg +91.000000 111.000000 8.000000 8.000000 re B* +0.550000 0.400000 0.300000 rg +101.000000 111.000000 8.000000 8.000000 re B* +0.600000 0.400000 0.275000 rg +111.000000 111.000000 8.000000 8.000000 re B* +0.650000 0.400000 0.250000 rg +121.000000 111.000000 8.000000 8.000000 re B* +0.700000 0.400000 0.225000 rg +131.000000 111.000000 8.000000 8.000000 re B* +0.750000 0.400000 0.200000 rg +141.000000 111.000000 8.000000 8.000000 re B* +0.800000 0.400000 0.175000 rg +151.000000 111.000000 8.000000 8.000000 re B* +0.850000 0.400000 0.150000 rg +161.000000 111.000000 8.000000 8.000000 re B* +0.900000 0.400000 0.125000 rg +171.000000 111.000000 8.000000 8.000000 re B* +0.950000 0.400000 0.100000 rg +181.000000 111.000000 8.000000 8.000000 re B* +1.000000 0.400000 0.075000 rg +191.000000 111.000000 8.000000 8.000000 re B* +0.050000 0.433333 0.575000 rg +1.000000 121.000000 8.000000 8.000000 re B* +0.100000 0.433333 0.550000 rg +11.000000 121.000000 8.000000 8.000000 re B* +0.150000 0.433333 0.525000 rg +21.000000 121.000000 8.000000 8.000000 re B* +0.200000 0.433333 0.500000 rg +31.000000 121.000000 8.000000 8.000000 re B* +0.250000 0.433333 0.475000 rg +41.000000 121.000000 8.000000 8.000000 re B* +0.300000 0.433333 0.450000 rg +51.000000 121.000000 8.000000 8.000000 re B* +0.350000 0.433333 0.425000 rg +61.000000 121.000000 8.000000 8.000000 re B* +0.400000 0.433333 0.400000 rg +71.000000 121.000000 8.000000 8.000000 re B* +0.450000 0.433333 0.375000 rg +81.000000 121.000000 8.000000 8.000000 re B* +0.500000 0.433333 0.350000 rg +91.000000 121.000000 8.000000 8.000000 re B* +0.550000 0.433333 0.325000 rg +101.000000 121.000000 8.000000 8.000000 re B* +0.600000 0.433333 0.300000 rg +111.000000 121.000000 8.000000 8.000000 re B* +0.650000 0.433333 0.275000 rg +121.000000 121.000000 8.000000 8.000000 re B* +0.700000 0.433333 0.250000 rg +131.000000 121.000000 8.000000 8.000000 re B* +0.750000 0.433333 0.225000 rg +141.000000 121.000000 8.000000 8.000000 re B* +0.800000 0.433333 0.200000 rg +151.000000 121.000000 8.000000 8.000000 re B* +0.850000 0.433333 0.175000 rg +161.000000 121.000000 8.000000 8.000000 re B* +0.900000 0.433333 0.150000 rg +171.000000 121.000000 8.000000 8.000000 re B* +0.950000 0.433333 0.125000 rg +181.000000 121.000000 8.000000 8.000000 re B* +1.000000 0.433333 0.100000 rg +191.000000 121.000000 8.000000 8.000000 re B* +0.050000 0.466667 0.600000 rg +1.000000 131.000000 8.000000 8.000000 re B* +0.100000 0.466667 0.575000 rg +11.000000 131.000000 8.000000 8.000000 re B* +0.150000 0.466667 0.550000 rg +21.000000 131.000000 8.000000 8.000000 re B* +0.200000 0.466667 0.525000 rg +31.000000 131.000000 8.000000 8.000000 re B* +0.250000 0.466667 0.500000 rg +41.000000 131.000000 8.000000 8.000000 re B* +0.300000 0.466667 0.475000 rg +51.000000 131.000000 8.000000 8.000000 re B* +0.350000 0.466667 0.450000 rg +61.000000 131.000000 8.000000 8.000000 re B* +0.400000 0.466667 0.425000 rg +71.000000 131.000000 8.000000 8.000000 re B* +0.450000 0.466667 0.400000 rg +81.000000 131.000000 8.000000 8.000000 re B* +0.500000 0.466667 0.375000 rg +91.000000 131.000000 8.000000 8.000000 re B* +0.550000 0.466667 0.350000 rg +101.000000 131.000000 8.000000 8.000000 re B* +0.600000 0.466667 0.325000 rg +111.000000 131.000000 8.000000 8.000000 re B* +0.650000 0.466667 0.300000 rg +121.000000 131.000000 8.000000 8.000000 re B* +0.700000 0.466667 0.275000 rg +131.000000 131.000000 8.000000 8.000000 re B* +0.750000 0.466667 0.250000 rg +141.000000 131.000000 8.000000 8.000000 re B* +0.800000 0.466667 0.225000 rg +151.000000 131.000000 8.000000 8.000000 re B* +0.850000 0.466667 0.200000 rg +161.000000 131.000000 8.000000 8.000000 re B* +0.900000 0.466667 0.175000 rg +171.000000 131.000000 8.000000 8.000000 re B* +0.950000 0.466667 0.150000 rg +181.000000 131.000000 8.000000 8.000000 re B* +1.000000 0.466667 0.125000 rg +191.000000 131.000000 8.000000 8.000000 re B* +0.050000 0.500000 0.625000 rg +1.000000 141.000000 8.000000 8.000000 re B* +0.100000 0.500000 0.600000 rg +11.000000 141.000000 8.000000 8.000000 re B* +0.150000 0.500000 0.575000 rg +21.000000 141.000000 8.000000 8.000000 re B* +0.200000 0.500000 0.550000 rg +31.000000 141.000000 8.000000 8.000000 re B* +0.250000 0.500000 0.525000 rg +41.000000 141.000000 8.000000 8.000000 re B* +0.300000 0.500000 0.500000 rg +51.000000 141.000000 8.000000 8.000000 re B* +0.350000 0.500000 0.475000 rg +61.000000 141.000000 8.000000 8.000000 re B* +0.400000 0.500000 0.450000 rg +71.000000 141.000000 8.000000 8.000000 re B* +0.450000 0.500000 0.425000 rg +81.000000 141.000000 8.000000 8.000000 re B* +0.500000 0.500000 0.400000 rg +91.000000 141.000000 8.000000 8.000000 re B* +0.550000 0.500000 0.375000 rg +101.000000 141.000000 8.000000 8.000000 re B* +0.600000 0.500000 0.350000 rg +111.000000 141.000000 8.000000 8.000000 re B* +0.650000 0.500000 0.325000 rg +121.000000 141.000000 8.000000 8.000000 re B* +0.700000 0.500000 0.300000 rg +131.000000 141.000000 8.000000 8.000000 re B* +0.750000 0.500000 0.275000 rg +141.000000 141.000000 8.000000 8.000000 re B* +0.800000 0.500000 0.250000 rg +151.000000 141.000000 8.000000 8.000000 re B* +0.850000 0.500000 0.225000 rg +161.000000 141.000000 8.000000 8.000000 re B* +0.900000 0.500000 0.200000 rg +171.000000 141.000000 8.000000 8.000000 re B* +0.950000 0.500000 0.175000 rg +181.000000 141.000000 8.000000 8.000000 re B* +1.000000 0.500000 0.150000 rg +191.000000 141.000000 8.000000 8.000000 re B* +0.050000 0.533333 0.650000 rg +1.000000 151.000000 8.000000 8.000000 re B* +0.100000 0.533333 0.625000 rg +11.000000 151.000000 8.000000 8.000000 re B* +0.150000 0.533333 0.600000 rg +21.000000 151.000000 8.000000 8.000000 re B* +0.200000 0.533333 0.575000 rg +31.000000 151.000000 8.000000 8.000000 re B* +0.250000 0.533333 0.550000 rg +41.000000 151.000000 8.000000 8.000000 re B* +0.300000 0.533333 0.525000 rg +51.000000 151.000000 8.000000 8.000000 re B* +0.350000 0.533333 0.500000 rg +61.000000 151.000000 8.000000 8.000000 re B* +0.400000 0.533333 0.475000 rg +71.000000 151.000000 8.000000 8.000000 re B* +0.450000 0.533333 0.450000 rg +81.000000 151.000000 8.000000 8.000000 re B* +0.500000 0.533333 0.425000 rg +91.000000 151.000000 8.000000 8.000000 re B* +0.550000 0.533333 0.400000 rg +101.000000 151.000000 8.000000 8.000000 re B* +0.600000 0.533333 0.375000 rg +111.000000 151.000000 8.000000 8.000000 re B* +0.650000 0.533333 0.350000 rg +121.000000 151.000000 8.000000 8.000000 re B* +0.700000 0.533333 0.325000 rg +131.000000 151.000000 8.000000 8.000000 re B* +0.750000 0.533333 0.300000 rg +141.000000 151.000000 8.000000 8.000000 re B* +0.800000 0.533333 0.275000 rg +151.000000 151.000000 8.000000 8.000000 re B* +0.850000 0.533333 0.250000 rg +161.000000 151.000000 8.000000 8.000000 re B* +0.900000 0.533333 0.225000 rg +171.000000 151.000000 8.000000 8.000000 re B* +0.950000 0.533333 0.200000 rg +181.000000 151.000000 8.000000 8.000000 re B* +1.000000 0.533333 0.175000 rg +191.000000 151.000000 8.000000 8.000000 re B* +0.050000 0.566667 0.675000 rg +1.000000 161.000000 8.000000 8.000000 re B* +0.100000 0.566667 0.650000 rg +11.000000 161.000000 8.000000 8.000000 re B* +0.150000 0.566667 0.625000 rg +21.000000 161.000000 8.000000 8.000000 re B* +0.200000 0.566667 0.600000 rg +31.000000 161.000000 8.000000 8.000000 re B* +0.250000 0.566667 0.575000 rg +41.000000 161.000000 8.000000 8.000000 re B* +0.300000 0.566667 0.550000 rg +51.000000 161.000000 8.000000 8.000000 re B* +0.350000 0.566667 0.525000 rg +61.000000 161.000000 8.000000 8.000000 re B* +0.400000 0.566667 0.500000 rg +71.000000 161.000000 8.000000 8.000000 re B* +0.450000 0.566667 0.475000 rg +81.000000 161.000000 8.000000 8.000000 re B* +0.500000 0.566667 0.450000 rg +91.000000 161.000000 8.000000 8.000000 re B* +0.550000 0.566667 0.425000 rg +101.000000 161.000000 8.000000 8.000000 re B* +0.600000 0.566667 0.400000 rg +111.000000 161.000000 8.000000 8.000000 re B* +0.650000 0.566667 0.375000 rg +121.000000 161.000000 8.000000 8.000000 re B* +0.700000 0.566667 0.350000 rg +131.000000 161.000000 8.000000 8.000000 re B* +0.750000 0.566667 0.325000 rg +141.000000 161.000000 8.000000 8.000000 re B* +0.800000 0.566667 0.300000 rg +151.000000 161.000000 8.000000 8.000000 re B* +0.850000 0.566667 0.275000 rg +161.000000 161.000000 8.000000 8.000000 re B* +0.900000 0.566667 0.250000 rg +171.000000 161.000000 8.000000 8.000000 re B* +0.950000 0.566667 0.225000 rg +181.000000 161.000000 8.000000 8.000000 re B* +1.000000 0.566667 0.200000 rg +191.000000 161.000000 8.000000 8.000000 re B* +0.050000 0.600000 0.700000 rg +1.000000 171.000000 8.000000 8.000000 re B* +0.100000 0.600000 0.675000 rg +11.000000 171.000000 8.000000 8.000000 re B* +0.150000 0.600000 0.650000 rg +21.000000 171.000000 8.000000 8.000000 re B* +0.200000 0.600000 0.625000 rg +31.000000 171.000000 8.000000 8.000000 re B* +0.250000 0.600000 0.600000 rg +41.000000 171.000000 8.000000 8.000000 re B* +0.300000 0.600000 0.575000 rg +51.000000 171.000000 8.000000 8.000000 re B* +0.350000 0.600000 0.550000 rg +61.000000 171.000000 8.000000 8.000000 re B* +0.400000 0.600000 0.525000 rg +71.000000 171.000000 8.000000 8.000000 re B* +0.450000 0.600000 0.500000 rg +81.000000 171.000000 8.000000 8.000000 re B* +0.500000 0.600000 0.475000 rg +91.000000 171.000000 8.000000 8.000000 re B* +0.550000 0.600000 0.450000 rg +101.000000 171.000000 8.000000 8.000000 re B* +0.600000 0.600000 0.425000 rg +111.000000 171.000000 8.000000 8.000000 re B* +0.650000 0.600000 0.400000 rg +121.000000 171.000000 8.000000 8.000000 re B* +0.700000 0.600000 0.375000 rg +131.000000 171.000000 8.000000 8.000000 re B* +0.750000 0.600000 0.350000 rg +141.000000 171.000000 8.000000 8.000000 re B* +0.800000 0.600000 0.325000 rg +151.000000 171.000000 8.000000 8.000000 re B* +0.850000 0.600000 0.300000 rg +161.000000 171.000000 8.000000 8.000000 re B* +0.900000 0.600000 0.275000 rg +171.000000 171.000000 8.000000 8.000000 re B* +0.950000 0.600000 0.250000 rg +181.000000 171.000000 8.000000 8.000000 re B* +1.000000 0.600000 0.225000 rg +191.000000 171.000000 8.000000 8.000000 re B* +0.050000 0.633333 0.725000 rg +1.000000 181.000000 8.000000 8.000000 re B* +0.100000 0.633333 0.700000 rg +11.000000 181.000000 8.000000 8.000000 re B* +0.150000 0.633333 0.675000 rg +21.000000 181.000000 8.000000 8.000000 re B* +0.200000 0.633333 0.650000 rg +31.000000 181.000000 8.000000 8.000000 re B* +0.250000 0.633333 0.625000 rg +41.000000 181.000000 8.000000 8.000000 re B* +0.300000 0.633333 0.600000 rg +51.000000 181.000000 8.000000 8.000000 re B* +0.350000 0.633333 0.575000 rg +61.000000 181.000000 8.000000 8.000000 re B* +0.400000 0.633333 0.550000 rg +71.000000 181.000000 8.000000 8.000000 re B* +0.450000 0.633333 0.525000 rg +81.000000 181.000000 8.000000 8.000000 re B* +0.500000 0.633333 0.500000 rg +91.000000 181.000000 8.000000 8.000000 re B* +0.550000 0.633333 0.475000 rg +101.000000 181.000000 8.000000 8.000000 re B* +0.600000 0.633333 0.450000 rg +111.000000 181.000000 8.000000 8.000000 re B* +0.650000 0.633333 0.425000 rg +121.000000 181.000000 8.000000 8.000000 re B* +0.700000 0.633333 0.400000 rg +131.000000 181.000000 8.000000 8.000000 re B* +0.750000 0.633333 0.375000 rg +141.000000 181.000000 8.000000 8.000000 re B* +0.800000 0.633333 0.350000 rg +151.000000 181.000000 8.000000 8.000000 re B* +0.850000 0.633333 0.325000 rg +161.000000 181.000000 8.000000 8.000000 re B* +0.900000 0.633333 0.300000 rg +171.000000 181.000000 8.000000 8.000000 re B* +0.950000 0.633333 0.275000 rg +181.000000 181.000000 8.000000 8.000000 re B* +1.000000 0.633333 0.250000 rg +191.000000 181.000000 8.000000 8.000000 re B* +0.050000 0.666667 0.750000 rg +1.000000 191.000000 8.000000 8.000000 re B* +0.100000 0.666667 0.725000 rg +11.000000 191.000000 8.000000 8.000000 re B* +0.150000 0.666667 0.700000 rg +21.000000 191.000000 8.000000 8.000000 re B* +0.200000 0.666667 0.675000 rg +31.000000 191.000000 8.000000 8.000000 re B* +0.250000 0.666667 0.650000 rg +41.000000 191.000000 8.000000 8.000000 re B* +0.300000 0.666667 0.625000 rg +51.000000 191.000000 8.000000 8.000000 re B* +0.350000 0.666667 0.600000 rg +61.000000 191.000000 8.000000 8.000000 re B* +0.400000 0.666667 0.575000 rg +71.000000 191.000000 8.000000 8.000000 re B* +0.450000 0.666667 0.550000 rg +81.000000 191.000000 8.000000 8.000000 re B* +0.500000 0.666667 0.525000 rg +91.000000 191.000000 8.000000 8.000000 re B* +0.550000 0.666667 0.500000 rg +101.000000 191.000000 8.000000 8.000000 re B* +0.600000 0.666667 0.475000 rg +111.000000 191.000000 8.000000 8.000000 re B* +0.650000 0.666667 0.450000 rg +121.000000 191.000000 8.000000 8.000000 re B* +0.700000 0.666667 0.425000 rg +131.000000 191.000000 8.000000 8.000000 re B* +0.750000 0.666667 0.400000 rg +141.000000 191.000000 8.000000 8.000000 re B* +0.800000 0.666667 0.375000 rg +151.000000 191.000000 8.000000 8.000000 re B* +0.850000 0.666667 0.350000 rg +161.000000 191.000000 8.000000 8.000000 re B* +0.900000 0.666667 0.325000 rg +171.000000 191.000000 8.000000 8.000000 re B* +0.950000 0.666667 0.300000 rg +181.000000 191.000000 8.000000 8.000000 re B* +1.000000 0.666667 0.275000 rg +191.000000 191.000000 8.000000 8.000000 re B* +0.050000 0.700000 0.775000 rg +1.000000 201.000000 8.000000 8.000000 re B* +0.100000 0.700000 0.750000 rg +11.000000 201.000000 8.000000 8.000000 re B* +0.150000 0.700000 0.725000 rg +21.000000 201.000000 8.000000 8.000000 re B* +0.200000 0.700000 0.700000 rg +31.000000 201.000000 8.000000 8.000000 re B* +0.250000 0.700000 0.675000 rg +41.000000 201.000000 8.000000 8.000000 re B* +0.300000 0.700000 0.650000 rg +51.000000 201.000000 8.000000 8.000000 re B* +0.350000 0.700000 0.625000 rg +61.000000 201.000000 8.000000 8.000000 re B* +0.400000 0.700000 0.600000 rg +71.000000 201.000000 8.000000 8.000000 re B* +0.450000 0.700000 0.575000 rg +81.000000 201.000000 8.000000 8.000000 re B* +0.500000 0.700000 0.550000 rg +91.000000 201.000000 8.000000 8.000000 re B* +0.550000 0.700000 0.525000 rg +101.000000 201.000000 8.000000 8.000000 re B* +0.600000 0.700000 0.500000 rg +111.000000 201.000000 8.000000 8.000000 re B* +0.650000 0.700000 0.475000 rg +121.000000 201.000000 8.000000 8.000000 re B* +0.700000 0.700000 0.450000 rg +131.000000 201.000000 8.000000 8.000000 re B* +0.750000 0.700000 0.425000 rg +141.000000 201.000000 8.000000 8.000000 re B* +0.800000 0.700000 0.400000 rg +151.000000 201.000000 8.000000 8.000000 re B* +0.850000 0.700000 0.375000 rg +161.000000 201.000000 8.000000 8.000000 re B* +0.900000 0.700000 0.350000 rg +171.000000 201.000000 8.000000 8.000000 re B* +0.950000 0.700000 0.325000 rg +181.000000 201.000000 8.000000 8.000000 re B* +1.000000 0.700000 0.300000 rg +191.000000 201.000000 8.000000 8.000000 re B* +0.050000 0.733333 0.800000 rg +1.000000 211.000000 8.000000 8.000000 re B* +0.100000 0.733333 0.775000 rg +11.000000 211.000000 8.000000 8.000000 re B* +0.150000 0.733333 0.750000 rg +21.000000 211.000000 8.000000 8.000000 re B* +0.200000 0.733333 0.725000 rg +31.000000 211.000000 8.000000 8.000000 re B* +0.250000 0.733333 0.700000 rg +41.000000 211.000000 8.000000 8.000000 re B* +0.300000 0.733333 0.675000 rg +51.000000 211.000000 8.000000 8.000000 re B* +0.350000 0.733333 0.650000 rg +61.000000 211.000000 8.000000 8.000000 re B* +0.400000 0.733333 0.625000 rg +71.000000 211.000000 8.000000 8.000000 re B* +0.450000 0.733333 0.600000 rg +81.000000 211.000000 8.000000 8.000000 re B* +0.500000 0.733333 0.575000 rg +91.000000 211.000000 8.000000 8.000000 re B* +0.550000 0.733333 0.550000 rg +101.000000 211.000000 8.000000 8.000000 re B* +0.600000 0.733333 0.525000 rg +111.000000 211.000000 8.000000 8.000000 re B* +0.650000 0.733333 0.500000 rg +121.000000 211.000000 8.000000 8.000000 re B* +0.700000 0.733333 0.475000 rg +131.000000 211.000000 8.000000 8.000000 re B* +0.750000 0.733333 0.450000 rg +141.000000 211.000000 8.000000 8.000000 re B* +0.800000 0.733333 0.425000 rg +151.000000 211.000000 8.000000 8.000000 re B* +0.850000 0.733333 0.400000 rg +161.000000 211.000000 8.000000 8.000000 re B* +0.900000 0.733333 0.375000 rg +171.000000 211.000000 8.000000 8.000000 re B* +0.950000 0.733333 0.350000 rg +181.000000 211.000000 8.000000 8.000000 re B* +1.000000 0.733333 0.325000 rg +191.000000 211.000000 8.000000 8.000000 re B* +0.050000 0.766667 0.825000 rg +1.000000 221.000000 8.000000 8.000000 re B* +0.100000 0.766667 0.800000 rg +11.000000 221.000000 8.000000 8.000000 re B* +0.150000 0.766667 0.775000 rg +21.000000 221.000000 8.000000 8.000000 re B* +0.200000 0.766667 0.750000 rg +31.000000 221.000000 8.000000 8.000000 re B* +0.250000 0.766667 0.725000 rg +41.000000 221.000000 8.000000 8.000000 re B* +0.300000 0.766667 0.700000 rg +51.000000 221.000000 8.000000 8.000000 re B* +0.350000 0.766667 0.675000 rg +61.000000 221.000000 8.000000 8.000000 re B* +0.400000 0.766667 0.650000 rg +71.000000 221.000000 8.000000 8.000000 re B* +0.450000 0.766667 0.625000 rg +81.000000 221.000000 8.000000 8.000000 re B* +0.500000 0.766667 0.600000 rg +91.000000 221.000000 8.000000 8.000000 re B* +0.550000 0.766667 0.575000 rg +101.000000 221.000000 8.000000 8.000000 re B* +0.600000 0.766667 0.550000 rg +111.000000 221.000000 8.000000 8.000000 re B* +0.650000 0.766667 0.525000 rg +121.000000 221.000000 8.000000 8.000000 re B* +0.700000 0.766667 0.500000 rg +131.000000 221.000000 8.000000 8.000000 re B* +0.750000 0.766667 0.475000 rg +141.000000 221.000000 8.000000 8.000000 re B* +0.800000 0.766667 0.450000 rg +151.000000 221.000000 8.000000 8.000000 re B* +0.850000 0.766667 0.425000 rg +161.000000 221.000000 8.000000 8.000000 re B* +0.900000 0.766667 0.400000 rg +171.000000 221.000000 8.000000 8.000000 re B* +0.950000 0.766667 0.375000 rg +181.000000 221.000000 8.000000 8.000000 re B* +1.000000 0.766667 0.350000 rg +191.000000 221.000000 8.000000 8.000000 re B* +0.050000 0.800000 0.850000 rg +1.000000 231.000000 8.000000 8.000000 re B* +0.100000 0.800000 0.825000 rg +11.000000 231.000000 8.000000 8.000000 re B* +0.150000 0.800000 0.800000 rg +21.000000 231.000000 8.000000 8.000000 re B* +0.200000 0.800000 0.775000 rg +31.000000 231.000000 8.000000 8.000000 re B* +0.250000 0.800000 0.750000 rg +41.000000 231.000000 8.000000 8.000000 re B* +0.300000 0.800000 0.725000 rg +51.000000 231.000000 8.000000 8.000000 re B* +0.350000 0.800000 0.700000 rg +61.000000 231.000000 8.000000 8.000000 re B* +0.400000 0.800000 0.675000 rg +71.000000 231.000000 8.000000 8.000000 re B* +0.450000 0.800000 0.650000 rg +81.000000 231.000000 8.000000 8.000000 re B* +0.500000 0.800000 0.625000 rg +91.000000 231.000000 8.000000 8.000000 re B* +0.550000 0.800000 0.600000 rg +101.000000 231.000000 8.000000 8.000000 re B* +0.600000 0.800000 0.575000 rg +111.000000 231.000000 8.000000 8.000000 re B* +0.650000 0.800000 0.550000 rg +121.000000 231.000000 8.000000 8.000000 re B* +0.700000 0.800000 0.525000 rg +131.000000 231.000000 8.000000 8.000000 re B* +0.750000 0.800000 0.500000 rg +141.000000 231.000000 8.000000 8.000000 re B* +0.800000 0.800000 0.475000 rg +151.000000 231.000000 8.000000 8.000000 re B* +0.850000 0.800000 0.450000 rg +161.000000 231.000000 8.000000 8.000000 re B* +0.900000 0.800000 0.425000 rg +171.000000 231.000000 8.000000 8.000000 re B* +0.950000 0.800000 0.400000 rg +181.000000 231.000000 8.000000 8.000000 re B* +1.000000 0.800000 0.375000 rg +191.000000 231.000000 8.000000 8.000000 re B* +0.050000 0.833333 0.875000 rg +1.000000 241.000000 8.000000 8.000000 re B* +0.100000 0.833333 0.850000 rg +11.000000 241.000000 8.000000 8.000000 re B* +0.150000 0.833333 0.825000 rg +21.000000 241.000000 8.000000 8.000000 re B* +0.200000 0.833333 0.800000 rg +31.000000 241.000000 8.000000 8.000000 re B* +0.250000 0.833333 0.775000 rg +41.000000 241.000000 8.000000 8.000000 re B* +0.300000 0.833333 0.750000 rg +51.000000 241.000000 8.000000 8.000000 re B* +0.350000 0.833333 0.725000 rg +61.000000 241.000000 8.000000 8.000000 re B* +0.400000 0.833333 0.700000 rg +71.000000 241.000000 8.000000 8.000000 re B* +0.450000 0.833333 0.675000 rg +81.000000 241.000000 8.000000 8.000000 re B* +0.500000 0.833333 0.650000 rg +91.000000 241.000000 8.000000 8.000000 re B* +0.550000 0.833333 0.625000 rg +101.000000 241.000000 8.000000 8.000000 re B* +0.600000 0.833333 0.600000 rg +111.000000 241.000000 8.000000 8.000000 re B* +0.650000 0.833333 0.575000 rg +121.000000 241.000000 8.000000 8.000000 re B* +0.700000 0.833333 0.550000 rg +131.000000 241.000000 8.000000 8.000000 re B* +0.750000 0.833333 0.525000 rg +141.000000 241.000000 8.000000 8.000000 re B* +0.800000 0.833333 0.500000 rg +151.000000 241.000000 8.000000 8.000000 re B* +0.850000 0.833333 0.475000 rg +161.000000 241.000000 8.000000 8.000000 re B* +0.900000 0.833333 0.450000 rg +171.000000 241.000000 8.000000 8.000000 re B* +0.950000 0.833333 0.425000 rg +181.000000 241.000000 8.000000 8.000000 re B* +1.000000 0.833333 0.400000 rg +191.000000 241.000000 8.000000 8.000000 re B* +0.050000 0.866667 0.900000 rg +1.000000 251.000000 8.000000 8.000000 re B* +0.100000 0.866667 0.875000 rg +11.000000 251.000000 8.000000 8.000000 re B* +0.150000 0.866667 0.850000 rg +21.000000 251.000000 8.000000 8.000000 re B* +0.200000 0.866667 0.825000 rg +31.000000 251.000000 8.000000 8.000000 re B* +0.250000 0.866667 0.800000 rg +41.000000 251.000000 8.000000 8.000000 re B* +0.300000 0.866667 0.775000 rg +51.000000 251.000000 8.000000 8.000000 re B* +0.350000 0.866667 0.750000 rg +61.000000 251.000000 8.000000 8.000000 re B* +0.400000 0.866667 0.725000 rg +71.000000 251.000000 8.000000 8.000000 re B* +0.450000 0.866667 0.700000 rg +81.000000 251.000000 8.000000 8.000000 re B* +0.500000 0.866667 0.675000 rg +91.000000 251.000000 8.000000 8.000000 re B* +0.550000 0.866667 0.650000 rg +101.000000 251.000000 8.000000 8.000000 re B* +0.600000 0.866667 0.625000 rg +111.000000 251.000000 8.000000 8.000000 re B* +0.650000 0.866667 0.600000 rg +121.000000 251.000000 8.000000 8.000000 re B* +0.700000 0.866667 0.575000 rg +131.000000 251.000000 8.000000 8.000000 re B* +0.750000 0.866667 0.550000 rg +141.000000 251.000000 8.000000 8.000000 re B* +0.800000 0.866667 0.525000 rg +151.000000 251.000000 8.000000 8.000000 re B* +0.850000 0.866667 0.500000 rg +161.000000 251.000000 8.000000 8.000000 re B* +0.900000 0.866667 0.475000 rg +171.000000 251.000000 8.000000 8.000000 re B* +0.950000 0.866667 0.450000 rg +181.000000 251.000000 8.000000 8.000000 re B* +1.000000 0.866667 0.425000 rg +191.000000 251.000000 8.000000 8.000000 re B* +0.050000 0.900000 0.925000 rg +1.000000 261.000000 8.000000 8.000000 re B* +0.100000 0.900000 0.900000 rg +11.000000 261.000000 8.000000 8.000000 re B* +0.150000 0.900000 0.875000 rg +21.000000 261.000000 8.000000 8.000000 re B* +0.200000 0.900000 0.850000 rg +31.000000 261.000000 8.000000 8.000000 re B* +0.250000 0.900000 0.825000 rg +41.000000 261.000000 8.000000 8.000000 re B* +0.300000 0.900000 0.800000 rg +51.000000 261.000000 8.000000 8.000000 re B* +0.350000 0.900000 0.775000 rg +61.000000 261.000000 8.000000 8.000000 re B* +0.400000 0.900000 0.750000 rg +71.000000 261.000000 8.000000 8.000000 re B* +0.450000 0.900000 0.725000 rg +81.000000 261.000000 8.000000 8.000000 re B* +0.500000 0.900000 0.700000 rg +91.000000 261.000000 8.000000 8.000000 re B* +0.550000 0.900000 0.675000 rg +101.000000 261.000000 8.000000 8.000000 re B* +0.600000 0.900000 0.650000 rg +111.000000 261.000000 8.000000 8.000000 re B* +0.650000 0.900000 0.625000 rg +121.000000 261.000000 8.000000 8.000000 re B* +0.700000 0.900000 0.600000 rg +131.000000 261.000000 8.000000 8.000000 re B* +0.750000 0.900000 0.575000 rg +141.000000 261.000000 8.000000 8.000000 re B* +0.800000 0.900000 0.550000 rg +151.000000 261.000000 8.000000 8.000000 re B* +0.850000 0.900000 0.525000 rg +161.000000 261.000000 8.000000 8.000000 re B* +0.900000 0.900000 0.500000 rg +171.000000 261.000000 8.000000 8.000000 re B* +0.950000 0.900000 0.475000 rg +181.000000 261.000000 8.000000 8.000000 re B* +1.000000 0.900000 0.450000 rg +191.000000 261.000000 8.000000 8.000000 re B* +0.050000 0.933333 0.950000 rg +1.000000 271.000000 8.000000 8.000000 re B* +0.100000 0.933333 0.925000 rg +11.000000 271.000000 8.000000 8.000000 re B* +0.150000 0.933333 0.900000 rg +21.000000 271.000000 8.000000 8.000000 re B* +0.200000 0.933333 0.875000 rg +31.000000 271.000000 8.000000 8.000000 re B* +0.250000 0.933333 0.850000 rg +41.000000 271.000000 8.000000 8.000000 re B* +0.300000 0.933333 0.825000 rg +51.000000 271.000000 8.000000 8.000000 re B* +0.350000 0.933333 0.800000 rg +61.000000 271.000000 8.000000 8.000000 re B* +0.400000 0.933333 0.775000 rg +71.000000 271.000000 8.000000 8.000000 re B* +0.450000 0.933333 0.750000 rg +81.000000 271.000000 8.000000 8.000000 re B* +0.500000 0.933333 0.725000 rg +91.000000 271.000000 8.000000 8.000000 re B* +0.550000 0.933333 0.700000 rg +101.000000 271.000000 8.000000 8.000000 re B* +0.600000 0.933333 0.675000 rg +111.000000 271.000000 8.000000 8.000000 re B* +0.650000 0.933333 0.650000 rg +121.000000 271.000000 8.000000 8.000000 re B* +0.700000 0.933333 0.625000 rg +131.000000 271.000000 8.000000 8.000000 re B* +0.750000 0.933333 0.600000 rg +141.000000 271.000000 8.000000 8.000000 re B* +0.800000 0.933333 0.575000 rg +151.000000 271.000000 8.000000 8.000000 re B* +0.850000 0.933333 0.550000 rg +161.000000 271.000000 8.000000 8.000000 re B* +0.900000 0.933333 0.525000 rg +171.000000 271.000000 8.000000 8.000000 re B* +0.950000 0.933333 0.500000 rg +181.000000 271.000000 8.000000 8.000000 re B* +1.000000 0.933333 0.475000 rg +191.000000 271.000000 8.000000 8.000000 re B* +0.050000 0.966667 0.975000 rg +1.000000 281.000000 8.000000 8.000000 re B* +0.100000 0.966667 0.950000 rg +11.000000 281.000000 8.000000 8.000000 re B* +0.150000 0.966667 0.925000 rg +21.000000 281.000000 8.000000 8.000000 re B* +0.200000 0.966667 0.900000 rg +31.000000 281.000000 8.000000 8.000000 re B* +0.250000 0.966667 0.875000 rg +41.000000 281.000000 8.000000 8.000000 re B* +0.300000 0.966667 0.850000 rg +51.000000 281.000000 8.000000 8.000000 re B* +0.350000 0.966667 0.825000 rg +61.000000 281.000000 8.000000 8.000000 re B* +0.400000 0.966667 0.800000 rg +71.000000 281.000000 8.000000 8.000000 re B* +0.450000 0.966667 0.775000 rg +81.000000 281.000000 8.000000 8.000000 re B* +0.500000 0.966667 0.750000 rg +91.000000 281.000000 8.000000 8.000000 re B* +0.550000 0.966667 0.725000 rg +101.000000 281.000000 8.000000 8.000000 re B* +0.600000 0.966667 0.700000 rg +111.000000 281.000000 8.000000 8.000000 re B* +0.650000 0.966667 0.675000 rg +121.000000 281.000000 8.000000 8.000000 re B* +0.700000 0.966667 0.650000 rg +131.000000 281.000000 8.000000 8.000000 re B* +0.750000 0.966667 0.625000 rg +141.000000 281.000000 8.000000 8.000000 re B* +0.800000 0.966667 0.600000 rg +151.000000 281.000000 8.000000 8.000000 re B* +0.850000 0.966667 0.575000 rg +161.000000 281.000000 8.000000 8.000000 re B* +0.900000 0.966667 0.550000 rg +171.000000 281.000000 8.000000 8.000000 re B* +0.950000 0.966667 0.525000 rg +181.000000 281.000000 8.000000 8.000000 re B* +1.000000 0.966667 0.500000 rg +191.000000 281.000000 8.000000 8.000000 re B* +0.050000 1.000000 1.000000 rg +1.000000 291.000000 8.000000 8.000000 re B* +0.100000 1.000000 0.975000 rg +11.000000 291.000000 8.000000 8.000000 re B* +0.150000 1.000000 0.950000 rg +21.000000 291.000000 8.000000 8.000000 re B* +0.200000 1.000000 0.925000 rg +31.000000 291.000000 8.000000 8.000000 re B* +0.250000 1.000000 0.900000 rg +41.000000 291.000000 8.000000 8.000000 re B* +0.300000 1.000000 0.875000 rg +51.000000 291.000000 8.000000 8.000000 re B* +0.350000 1.000000 0.850000 rg +61.000000 291.000000 8.000000 8.000000 re B* +0.400000 1.000000 0.825000 rg +71.000000 291.000000 8.000000 8.000000 re B* +0.450000 1.000000 0.800000 rg +81.000000 291.000000 8.000000 8.000000 re B* +0.500000 1.000000 0.775000 rg +91.000000 291.000000 8.000000 8.000000 re B* +0.550000 1.000000 0.750000 rg +101.000000 291.000000 8.000000 8.000000 re B* +0.600000 1.000000 0.725000 rg +111.000000 291.000000 8.000000 8.000000 re B* +0.650000 1.000000 0.700000 rg +121.000000 291.000000 8.000000 8.000000 re B* +0.700000 1.000000 0.675000 rg +131.000000 291.000000 8.000000 8.000000 re B* +0.750000 1.000000 0.650000 rg +141.000000 291.000000 8.000000 8.000000 re B* +0.800000 1.000000 0.625000 rg +151.000000 291.000000 8.000000 8.000000 re B* +0.850000 1.000000 0.600000 rg +161.000000 291.000000 8.000000 8.000000 re B* +0.900000 1.000000 0.575000 rg +171.000000 291.000000 8.000000 8.000000 re B* +0.950000 1.000000 0.550000 rg +181.000000 291.000000 8.000000 8.000000 re B* +1.000000 1.000000 0.525000 rg +191.000000 291.000000 8.000000 8.000000 re B* + +Q +endstream +endobj +xref +0 5 +0000000000 65535 f +0000000015 00000 n +0000000068 00000 n +0000000161 00000 n +0000000230 00000 n +trailer << + /Root 1 0 R + /Size 5 +>> +startxref +45360 +%%EOF diff --git a/corpus/basic/rectangles.pdf b/corpus/basic/rectangles.pdf new file mode 100644 index 0000000..6911d22 --- /dev/null +++ b/corpus/basic/rectangles.pdf @@ -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 diff --git a/corpus/basic/rectangles_multi_pages.pdf b/corpus/basic/rectangles_multi_pages.pdf new file mode 100644 index 0000000..22d44a4 --- /dev/null +++ b/corpus/basic/rectangles_multi_pages.pdf @@ -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 diff --git a/corpus/basic/whitespace.pdf b/corpus/basic/whitespace.pdf new file mode 100644 index 0000000..c4fb0d8 --- /dev/null +++ b/corpus/basic/whitespace.pdf @@ -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 diff --git a/corpus/edge-cases/annots.pdf b/corpus/edge-cases/annots.pdf new file mode 100644 index 0000000..745346c --- /dev/null +++ b/corpus/edge-cases/annots.pdf @@ -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 diff --git a/corpus/edge-cases/bookmarks.pdf b/corpus/edge-cases/bookmarks.pdf new file mode 100644 index 0000000..757f859 --- /dev/null +++ b/corpus/edge-cases/bookmarks.pdf @@ -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 diff --git a/corpus/edge-cases/combobox_form.pdf b/corpus/edge-cases/combobox_form.pdf new file mode 100644 index 0000000..5b61db5 --- /dev/null +++ b/corpus/edge-cases/combobox_form.pdf @@ -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 diff --git a/corpus/edge-cases/embedded_attachments.pdf b/corpus/edge-cases/embedded_attachments.pdf new file mode 100644 index 0000000..1e8fecd Binary files /dev/null and b/corpus/edge-cases/embedded_attachments.pdf differ diff --git a/corpus/edge-cases/empty_xref.pdf b/corpus/edge-cases/empty_xref.pdf new file mode 100644 index 0000000..ed02848 --- /dev/null +++ b/corpus/edge-cases/empty_xref.pdf @@ -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 diff --git a/corpus/edge-cases/encrypted.pdf b/corpus/edge-cases/encrypted.pdf new file mode 100644 index 0000000..b489d56 Binary files /dev/null and b/corpus/edge-cases/encrypted.pdf differ diff --git a/corpus/edge-cases/linearized.pdf b/corpus/edge-cases/linearized.pdf new file mode 100644 index 0000000..3c2d3a2 Binary files /dev/null and b/corpus/edge-cases/linearized.pdf differ diff --git a/corpus/edge-cases/listbox_form.pdf b/corpus/edge-cases/listbox_form.pdf new file mode 100644 index 0000000..10c5702 --- /dev/null +++ b/corpus/edge-cases/listbox_form.pdf @@ -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 diff --git a/corpus/edge-cases/no_page_count.pdf b/corpus/edge-cases/no_page_count.pdf new file mode 100644 index 0000000..885df8c --- /dev/null +++ b/corpus/edge-cases/no_page_count.pdf @@ -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 diff --git a/corpus/edge-cases/page_labels.pdf b/corpus/edge-cases/page_labels.pdf new file mode 100644 index 0000000..fd1a1e5 Binary files /dev/null and b/corpus/edge-cases/page_labels.pdf differ diff --git a/corpus/edge-cases/text_form.pdf b/corpus/edge-cases/text_form.pdf new file mode 100644 index 0000000..1f3ab85 --- /dev/null +++ b/corpus/edge-cases/text_form.pdf @@ -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 diff --git a/corpus/edge-cases/unsupported_feature.pdf b/corpus/edge-cases/unsupported_feature.pdf new file mode 100644 index 0000000..eb7ee08 --- /dev/null +++ b/corpus/edge-cases/unsupported_feature.pdf @@ -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 <> + >> + /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 diff --git a/corpus/edge-cases/zero_length_stream.pdf b/corpus/edge-cases/zero_length_stream.pdf new file mode 100644 index 0000000..1317c3f --- /dev/null +++ b/corpus/edge-cases/zero_length_stream.pdf @@ -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 diff --git a/corpus/fonts/hebrew_mirrored.pdf b/corpus/fonts/hebrew_mirrored.pdf new file mode 100644 index 0000000..1134350 --- /dev/null +++ b/corpus/fonts/hebrew_mirrored.pdf @@ -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#(%pV +nuG^jZEkc[.-qdl^dk1F&,fKT9F2)eaH/1QX +=p%W;pT6OOkC.gL0;aQZ%EQ]$ +CMpO/ouiOe*=oqMFW5p^geAElUWZdj+!8FWu:gnKPdkmGSM;%q39@mC[7^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\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'>bb#jgf**OlY[IHaK5,@C./bHbDalE":hWu.1N,@;S&gl,n3: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 +4SBp^*fhF-*fhL/*r?NkO.Sgf4:WJt*n-E0W]KmAKl)1[L/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 +LqA*jpO7b7bY=!,<-,5gQ6!\>V]AQ)^-GB`&C"Q_Tt5F^2b04 +H^dXF*&#KkrUdWPo4O.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-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';pGko/fq8/"=:R5@Xn#Q]c2U1bel!_n`0,C>VAYsbZH&PH@FJ$f#/ +0td=Kg'>AR25F7&YOo!dFFCb.uJQlb(Q4<5Au742_H^C[$nr;0XJ4b3Tg +[;48b%t<.Gdkk?8SYSUrma +g=r&81jkFAnU9>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"qaZhoo5D,:=$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_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>1Wud3mOBZ)WgX'F4+E];'H25pPfMT*QlPBHkoI@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> +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 diff --git a/corpus/fonts/latin_extended.pdf b/corpus/fonts/latin_extended.pdf new file mode 100644 index 0000000..6c34ab0 Binary files /dev/null and b/corpus/fonts/latin_extended.pdf differ diff --git a/corpus/fonts/rotated_text.pdf b/corpus/fonts/rotated_text.pdf new file mode 100644 index 0000000..d6d8db8 --- /dev/null +++ b/corpus/fonts/rotated_text.pdf @@ -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 diff --git a/corpus/fonts/rotated_text_90.pdf b/corpus/fonts/rotated_text_90.pdf new file mode 100644 index 0000000..a3a316d --- /dev/null +++ b/corpus/fonts/rotated_text_90.pdf @@ -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 diff --git a/corpus/fonts/text_font.pdf b/corpus/fonts/text_font.pdf new file mode 100644 index 0000000..78f3bcc Binary files /dev/null and b/corpus/fonts/text_font.pdf differ diff --git a/corpus/fonts/utf-8.pdf b/corpus/fonts/utf-8.pdf new file mode 100644 index 0000000..6bfb3ef --- /dev/null +++ b/corpus/fonts/utf-8.pdf @@ -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 + /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 diff --git a/corpus/fonts/vertical_text.pdf b/corpus/fonts/vertical_text.pdf new file mode 100644 index 0000000..8c0a2a5 --- /dev/null +++ b/corpus/fonts/vertical_text.pdf @@ -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![MV1As+]B +'495o@Y1$[N=Y"V^.$-"n:U;RjO^N4a)+9@>=#jGhF!S/7idXo\$E.-Hf!Q!CSMQ;G;Ho;[nuK#ZMO7Yf&Pd!%@tr;*$^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!@`!?&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*p0+UgafRn=tkd-ms-eej'u7=0WP:EV;`hloWXt +=>fSH20/b6eu!4$i^emK@2+:0LIOA@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 diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index bff58b2..5beea22 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -7,11 +7,14 @@ 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/core/graphics_state.cpp src/core/display_list.cpp src/parser/pdfium_loader.cpp + src/parser/pdfium_document.cpp src/fonts/font_face.cpp src/fonts/hb_shaper.cpp ) @@ -31,6 +34,7 @@ target_link_libraries(pdfengine PRIVATE freetype harfbuzz::harfbuzz + PNG::PNG ) if(PDFENGINE_WITH_PDFIUM) diff --git a/engine/include/pdfengine/pdf_document.hpp b/engine/include/pdfengine/pdf_document.hpp index 16d1cfb..24f9b68 100644 --- a/engine/include/pdfengine/pdf_document.hpp +++ b/engine/include/pdfengine/pdf_document.hpp @@ -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 +#include +#include +#include +#include + 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 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 render(int dpi = 96) const = 0; + + [[nodiscard]] virtual std::expected 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, EngineError> + loadFromFile(const std::string& path, const std::string& password = ""); + + [[nodiscard]] static std::expected, EngineError> + loadFromMemory(const std::vector& data, const std::string& password = ""); + + [[nodiscard]] virtual int pageCount() const noexcept = 0; + [[nodiscard]] virtual DocumentMetadata metadata() const noexcept = 0; + + [[nodiscard]] virtual std::expected, EngineError> + getPage(int pageIndex) = 0; + + virtual std::expected applyEdits(const std::string& editsJson) = 0; + + [[nodiscard]] virtual std::expected, EngineError> + saveIncremental() const = 0; +}; + +} \ No newline at end of file diff --git a/engine/src/parser/pdfium_document.cpp b/engine/src/parser/pdfium_document.cpp new file mode 100644 index 0000000..e8b469a --- /dev/null +++ b/engine/src/parser/pdfium_document.cpp @@ -0,0 +1,433 @@ +#include "parser/pdfium_document.hpp" + +#ifdef PDFENGINE_WITH_PDFIUM +#include +#include +#include +#include +#include +#include "parser/pdfium_loader.hpp" +#endif + +#include +#include + +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(c); + } else if (c < 0x800) { + utf8 += static_cast(0xC0 | (c >> 6)); + utf8 += static_cast(0x80 | (c & 0x3F)); + } else { + utf8 += static_cast(0xE0 | (c >> 12)); + utf8 += static_cast(0x80 | ((c >> 6) & 0x3F)); + utf8 += static_cast(0x80 | (c & 0x3F)); + } + } + return utf8; +} + +#ifdef PDFENGINE_WITH_PDFIUM +struct VectorWriter : public FPDF_FILEWRITE { + std::vector buffer; + + static int WriteBlockCallback(FPDF_FILEWRITE* pThis, const void* pData, unsigned long size) { + auto* self = static_cast(pThis); + const auto* bytes = static_cast(pData); + self->buffer.insert(self->buffer.end(), bytes, bytes + size); + return 1; + } + + VectorWriter() { + this->version = 1; + this->WriteBlock = &VectorWriter::WriteBlockCallback; + } +}; + +struct PngWriteState { + std::vector* buffer; +}; + +void pngWriteCallback(png_structp png_ptr, png_bytep data, png_size_t length) { + auto* state = reinterpret_cast(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 encodeBgraToPng(const uint8_t* bgra, int width, int height, int stride) { + std::vector 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 rowPointers(height); + for (int y = 0; y < height; ++y) { + rowPointers[y] = const_cast(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, 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(doc); +#else + (void)path; + (void)password; + spdlog::error("loadFromFile failed: PDFEngine compiled without PDFium support."); + return std::unexpected(EngineError::Unknown); +#endif +} + +std::expected, EngineError> +PdfDocument::loadFromMemory(const std::vector& 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(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(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 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 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 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(width() * scale); + int h = static_cast(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(FPDFBitmap_GetBuffer(bitmap)); + int stride = FPDFBitmap_GetStride(bitmap); + + std::vector 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 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 buffer(charCount + 1, 0); + int written = FPDFText_GetText(textPage_, 0, charCount, buffer.data()); + if (written <= 0) { + return ""; + } + + return utf16le_to_utf8(reinterpret_cast(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 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 buf(len / 2); + FPDF_GetMetaText(doc_, key, buf.data(), len); + return utf16le_to_utf8(reinterpret_cast(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, 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(page, pageIndex); +#else + (void)pageIndex; + return std::unexpected(EngineError::Unknown); +#endif +} + +std::expected PdfiumDocument::applyEdits(const std::string& editsJson) { + (void)editsJson; + return {}; +} + +std::expected, 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 +} + +} \ No newline at end of file diff --git a/engine/src/parser/pdfium_document.hpp b/engine/src/parser/pdfium_document.hpp new file mode 100644 index 0000000..a3fc3e1 --- /dev/null +++ b/engine/src/parser/pdfium_document.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include + +#ifdef PDFENGINE_WITH_PDFIUM +#include +#include +#endif + +#include +#include +#include +#include + +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 render(int dpi = 96) const override; + std::expected 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, EngineError> getPage(int pageIndex) override; + + std::expected applyEdits(const std::string& editsJson) override; + std::expected, EngineError> saveIncremental() const override; + +private: + NativeDocHandle doc_ = nullptr; +}; + +} \ No newline at end of file diff --git a/engine/tests/CMakeLists.txt b/engine/tests/CMakeLists.txt index 7e8f1c5..3a0558e 100644 --- a/engine/tests/CMakeLists.txt +++ b/engine/tests/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(pdfengine_smoke fonts_test.cpp graphics_state_test.cpp display_list_test.cpp + document_test.cpp ) target_link_libraries(pdfengine_smoke @@ -19,6 +20,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) diff --git a/engine/tests/document_test.cpp b/engine/tests/document_test.cpp new file mode 100644 index 0000000..1f7caf1 --- /dev/null +++ b/engine/tests/document_test.cpp @@ -0,0 +1,346 @@ +#include +#include +#include +#include +#include +#include +#include + +#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 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 buffer(size); + if (file.read(reinterpret_cast(buffer.data()), size)) { + return buffer; + } + return {}; +} + +const std::vector 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 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 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 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 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 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& 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 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(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 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(ptWidth * scale); + int expectedH = static_cast(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 rotations = {0, 90, 180, 270}; + + std::vector 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()); +} + +} diff --git a/gateway/app/main.py b/gateway/app/main.py index 8cb2927..e47d5e2 100644 --- a/gateway/app/main.py +++ b/gateway/app/main.py @@ -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 diff --git a/gateway/app/routers/documents.py b/gateway/app/routers/documents.py index c899523..5e32454 100644 --- a/gateway/app/routers/documents.py +++ b/gateway/app/routers/documents.py @@ -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} \ No newline at end of file diff --git a/gateway/app/routers/edits.py b/gateway/app/routers/edits.py index 3092472..780a91a 100644 --- a/gateway/app/routers/edits.py +++ b/gateway/app/routers/edits.py @@ -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) \ No newline at end of file diff --git a/gateway/app/routers/render.py b/gateway/app/routers/render.py index 58a431b..e856ab1 100644 --- a/gateway/app/routers/render.py +++ b/gateway/app/routers/render.py @@ -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) diff --git a/gateway/app/services/store.py b/gateway/app/services/store.py new file mode 100644 index 0000000..5ead98f --- /dev/null +++ b/gateway/app/services/store.py @@ -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() diff --git a/gateway/pdfengine.cp312-win_amd64.pyd b/gateway/pdfengine.cp312-win_amd64.pyd new file mode 100644 index 0000000..843409f Binary files /dev/null and b/gateway/pdfengine.cp312-win_amd64.pyd differ diff --git a/gateway/tests/test_health.py b/gateway/tests/test_health.py index 1e8ebdf..43cc0ae 100644 --- a/gateway/tests/test_health.py +++ b/gateway/tests/test_health.py @@ -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 \ No newline at end of file diff --git a/gateway/tests/test_placeholders.py b/gateway/tests/test_placeholders.py deleted file mode 100644 index bbcb1e9..0000000 --- a/gateway/tests/test_placeholders.py +++ /dev/null @@ -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() diff --git a/gateway/tests/test_routes.py b/gateway/tests/test_routes.py new file mode 100644 index 0000000..aaf14eb --- /dev/null +++ b/gateway/tests/test_routes.py @@ -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" \ No newline at end of file diff --git a/scripts/build_cpp.ps1 b/scripts/build_cpp.ps1 new file mode 100644 index 0000000..374205b --- /dev/null +++ b/scripts/build_cpp.ps1 @@ -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 diff --git a/scripts/copy_test_corpus.ps1 b/scripts/copy_test_corpus.ps1 new file mode 100644 index 0000000..8d50a73 --- /dev/null +++ b/scripts/copy_test_corpus.ps1 @@ -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" diff --git a/scripts/start_gateway.ps1 b/scripts/start_gateway.ps1 new file mode 100644 index 0000000..3f73276 --- /dev/null +++ b/scripts/start_gateway.ps1 @@ -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 +} diff --git a/scripts/test_cpp.ps1 b/scripts/test_cpp.ps1 new file mode 100644 index 0000000..7187912 --- /dev/null +++ b/scripts/test_cpp.ps1 @@ -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 diff --git a/scripts/test_gateway.ps1 b/scripts/test_gateway.ps1 new file mode 100644 index 0000000..2bd5926 --- /dev/null +++ b/scripts/test_gateway.ps1 @@ -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 +} diff --git a/vcpkg.json b/vcpkg.json index 7f5988a..10fbd31 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -13,7 +13,8 @@ "freetype", "harfbuzz", "spdlog", - "gtest" + "gtest", + "pybind11" ], "builtin-baseline": "495848814af4cc2760e70f7440c2dbe66d3ff196" }