yams/plugins/zyp/zpdf_wrapper.cpp
Trevon 6d8877a6d4
feat(search): add Simeon bandit tuning (#33)
* feat(search): per-profile simeon bandit with training-free self-tuning

* fix(zyp): free parallel buffers correctly

* fix: code cleanup and simeon version bump

* fix(search): guard Simeon bandit state
2026-05-17 04:08:50 -04:00

280 lines
7.6 KiB
C++

/**
* @file zpdf_wrapper.cpp
* @brief C++ RAII wrapper implementation for zpdf library
*/
#include "zpdf_wrapper.h"
#include "zpdf.h"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <future>
#include <sstream>
#include <thread>
#include <utility>
namespace yams::zyp {
// ============================================================================
// TextBuffer
// ============================================================================
TextBuffer::TextBuffer(uint8_t* data, size_t len)
: TextBuffer(data, len, TextBuffer::Deallocator::Zpdf) {}
TextBuffer::TextBuffer(uint8_t* data, size_t len, TextBuffer::Deallocator deallocator)
: data_(data), len_(len), deallocator_(deallocator) {}
TextBuffer TextBuffer::fromMalloc(uint8_t* data, size_t len) {
return TextBuffer(data, len, TextBuffer::Deallocator::Malloc);
}
TextBuffer::~TextBuffer() {
release();
}
void TextBuffer::release() noexcept {
if (data_) {
if (deallocator_ == TextBuffer::Deallocator::Malloc) {
std::free(data_);
} else {
zpdf_free_buffer(data_, len_);
}
data_ = nullptr;
len_ = 0;
}
}
TextBuffer::TextBuffer(TextBuffer&& other) noexcept
: data_(std::exchange(other.data_, nullptr)), len_(std::exchange(other.len_, 0)),
deallocator_(std::exchange(other.deallocator_, TextBuffer::Deallocator::Zpdf)) {}
TextBuffer& TextBuffer::operator=(TextBuffer&& other) noexcept {
if (this != &other) {
release();
data_ = std::exchange(other.data_, nullptr);
len_ = std::exchange(other.len_, 0);
deallocator_ = std::exchange(other.deallocator_, TextBuffer::Deallocator::Zpdf);
}
return *this;
}
std::string_view TextBuffer::text() const {
if (!data_ || len_ == 0) {
return {};
}
return {reinterpret_cast<const char*>(data_), len_};
}
bool TextBuffer::empty() const {
return !data_ || len_ == 0;
}
TextBuffer::operator bool() const {
return data_ != nullptr;
}
// ============================================================================
// Document
// ============================================================================
Document::Document(void* handle) : handle_(handle) {}
Document::~Document() {
if (handle_) {
zpdf_close(static_cast<ZpdfDocument*>(handle_));
}
}
Document::Document(Document&& other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {}
Document& Document::operator=(Document&& other) noexcept {
if (this != &other) {
if (handle_) {
zpdf_close(static_cast<ZpdfDocument*>(handle_));
}
handle_ = std::exchange(other.handle_, nullptr);
}
return *this;
}
std::optional<Document> Document::open(std::string_view path) {
// Ensure null-terminated string
std::string pathStr(path);
auto* handle = zpdf_open(pathStr.c_str());
if (!handle) {
return std::nullopt;
}
return Document(handle);
}
std::optional<Document> Document::openMemory(std::span<const uint8_t> data) {
if (data.empty()) {
return std::nullopt;
}
auto* handle = zpdf_open_memory(data.data(), data.size());
if (!handle) {
return std::nullopt;
}
return Document(handle);
}
int Document::pageCount() const {
if (!handle_) {
return 0;
}
int count = zpdf_page_count(static_cast<ZpdfDocument*>(handle_));
return count >= 0 ? count : 0;
}
std::optional<PageInfo> Document::pageInfo(int pageNum) const {
if (!handle_) {
return std::nullopt;
}
PageInfo info{};
int result = zpdf_get_page_info(static_cast<ZpdfDocument*>(handle_), pageNum, &info.width,
&info.height, &info.rotation);
if (result != 0) {
return std::nullopt;
}
return info;
}
TextBuffer Document::extractPage(int pageNum) const {
if (!handle_) {
return {};
}
size_t len = 0;
uint8_t* data = zpdf_extract_page(static_cast<ZpdfDocument*>(handle_), pageNum, &len);
return TextBuffer(data, len);
}
TextBuffer Document::extractAll() const {
if (!handle_) {
return {};
}
size_t len = 0;
uint8_t* data = zpdf_extract_all(static_cast<ZpdfDocument*>(handle_), &len);
return TextBuffer(data, len);
}
TextBuffer Document::extractAllParallel() const {
if (!handle_) {
return {};
}
size_t len = 0;
uint8_t* data = zpdf_extract_all_parallel(static_cast<ZpdfDocument*>(handle_), &len);
return TextBuffer(data, len);
}
TextBuffer Document::extractAllReadingOrder() const {
if (!handle_) {
return {};
}
size_t len = 0;
uint8_t* data =
zpdf_extract_all_reading_order_parallel(static_cast<ZpdfDocument*>(handle_), &len);
return TextBuffer(data, len);
}
bool Document::valid() const {
return handle_ != nullptr;
}
TextBuffer Document::extractAllParallelized(std::span<const uint8_t> data, int numThreads) {
if (data.empty())
return {};
if (numThreads <= 0)
numThreads = static_cast<int>(std::thread::hardware_concurrency());
if (numThreads < 1)
numThreads = 1;
auto probeDoc = Document::openMemory(data);
if (!probeDoc)
return {};
const int totalPages = probeDoc->pageCount();
probeDoc.reset();
if (totalPages <= 0)
return {};
if (numThreads > totalPages)
numThreads = totalPages;
if (numThreads <= 1) {
auto doc = Document::openMemory(data);
if (!doc)
return {};
return doc->extractAll();
}
// Open per-thread documents and distribute pages
const int pagesPerThread = (totalPages + numThreads - 1) / numThreads;
std::vector<std::future<std::pair<int, std::string>>> futures;
futures.reserve(static_cast<size_t>(numThreads));
for (int t = 0; t < numThreads; ++t) {
const int startPage = t * pagesPerThread;
const int endPage = std::min(startPage + pagesPerThread, totalPages);
if (startPage >= totalPages)
break;
futures.push_back(std::async(
std::launch::async, [data, startPage, endPage]() -> std::pair<int, std::string> {
auto doc = Document::openMemory(data);
if (!doc)
return {startPage, {}};
std::ostringstream oss;
for (int p = startPage; p < endPage; ++p) {
auto pageText = doc->extractPage(p);
auto view = pageText.text();
if (!view.empty())
oss.write(view.data(), static_cast<std::streamsize>(view.size()));
oss << '\n';
}
return {startPage, oss.str()};
}));
}
// Collect results in page order
std::vector<std::pair<int, std::string>> results;
results.reserve(futures.size());
for (auto& f : futures) {
try {
results.push_back(f.get());
} catch (const std::exception& e) {
(void)e; // thread failed; skip its pages
}
}
std::sort(results.begin(), results.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
std::ostringstream combined;
for (auto& [_, text] : results) {
if (!text.empty())
combined << text;
}
const std::string finalText = combined.str();
if (finalText.empty())
return {};
auto* buf = static_cast<uint8_t*>(std::malloc(finalText.size() + 1));
if (!buf)
return {};
std::memcpy(buf, finalText.data(), finalText.size());
buf[finalText.size()] = '\0';
return TextBuffer::fromMalloc(buf, finalText.size());
}
} // namespace yams::zyp