Files
pdf/wasm/bindings/pdf_engine_facade.cpp
T

355 lines
13 KiB
C++

#include "pdf_engine_facade.hpp"
#include "wasm_rasterizer.hpp"
#include <string_view>
#include <iostream>
#include <cmath>
#include <algorithm>
// --- WasmMockPage Implementation ---
WasmMockPage::WasmMockPage(int pageIndex) : m_pageIndex(pageIndex) {
// 1. Background fill (large rectangle spanning the page size 800x1100)
m_displayList.fillRect(0.0f, 0.0f, 800.0f, 1100.0f);
// 2. Grid lines: spacing of 50 points
float gridSpacing = 50.0f;
for (float x = gridSpacing; x < 800.0f; x += gridSpacing) {
m_displayList.fillRect(x, 0.0f, 1.0f, 1100.0f);
}
for (float y = gridSpacing; y < 1100.0f; y += gridSpacing) {
m_displayList.fillRect(0.0f, y, 800.0f, 1.0f);
}
// 3. Margin/Border
float borderWidth = 4.0f;
m_displayList.fillRect(0.0f, 0.0f, 800.0f, borderWidth); // Top
m_displayList.fillRect(0.0f, 1100.0f - borderWidth, 800.0f, borderWidth); // Bottom
m_displayList.fillRect(0.0f, 0.0f, borderWidth, 1100.0f); // Left
m_displayList.fillRect(800.0f - borderWidth, 0.0f, borderWidth, 1100.0f); // Right
// 4. Center Shape
float centerX = 400.0f;
float centerY = 550.0f;
float size = 120.0f;
if (m_pageIndex == 0) {
// Page 0: Coral pink square in the center
m_displayList.fillRect(centerX - size / 2.0f, centerY - size / 2.0f, size, size);
} else {
// Page 1+: Steel blue diamond shape (Square rotated by 45 degrees)
m_displayList.saveState();
// Translate to the center
m_displayList.setTransform(pdfengine::Matrix(1.0f, 0.0f, 0.0f, 1.0f, centerX, centerY));
// Rotate by 45 degrees
float angle = 45.0f * 3.14159265f / 180.0f;
float cosVal = std::cos(angle);
float sinVal = std::sin(angle);
m_displayList.setTransform(pdfengine::Matrix(cosVal, sinVal, -sinVal, cosVal, 0.0f, 0.0f));
// Draw square centered at translated & rotated origin
m_displayList.fillRect(-size / 2.0f, -size / 2.0f, size, size);
m_displayList.restoreState();
}
// 5. Text Label
m_displayList.drawText("Page " + std::to_string(m_pageIndex + 1), 60.0f, 80.0f);
}
std::expected<pdfengine::PageImage, pdfengine::EngineError> WasmMockPage::render(int dpi) const {
(void)dpi;
return std::unexpected(pdfengine::EngineError::Unknown);
}
std::expected<std::string, pdfengine::EngineError> WasmMockPage::extractText() const {
return "Mock text on page " + std::to_string(m_pageIndex + 1);
}
std::expected<std::vector<pdfengine::GlyphBounds>, pdfengine::EngineError> WasmMockPage::extractTextWithBounds() const {
std::vector<pdfengine::GlyphBounds> glyphs;
std::string text = "Page " + std::to_string(m_pageIndex + 1);
double startX = 60.0;
double startY = 80.0;
for (size_t i = 0; i < text.length(); ++i) {
pdfengine::GlyphBounds gb;
gb.text = std::string(1, text[i]);
gb.x = startX + i * 6.0;
gb.y = startY;
gb.w = 6.0;
gb.h = 8.0;
gb.fontSize = 12.0;
glyphs.push_back(gb);
}
return glyphs;
}
std::expected<std::vector<std::string>, pdfengine::EngineError> WasmMockPage::extractAnnotationsText() const {
return std::vector<std::string>();
}
std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> WasmMockPage::getFonts() const {
// Simulate a non-embedded Helvetica font being substituted with Liberation Sans.
// This is the canonical Phase 1 font substitution scenario.
pdfengine::FontInfo info;
info.fontName = "Helvetica";
info.type = "Type1";
info.isEmbedded = false;
info.isSubset = false;
info.isVertical = false;
info.encoding = "WinAnsiEncoding";
info.hasToUnicode = false;
info.sourceType = "Substituted";
info.substitutedFrom = "Helvetica";
info.substitutedTo = "Liberation Sans Regular";
info.normalizedFamily= "Arial";
info.internalFontId = "mock-page-" + std::to_string(m_pageIndex) + "-helvetica";
info.flags = 32; // PDF font descriptor Nonsymbolic flag
info.ascent = 718.0;
info.descent = -207.0;
info.capHeight = 718.0;
return std::vector<pdfengine::FontInfo>{info};
}
pdfengine::DevicePoint WasmMockPage::pageToDevice(const pdfengine::Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
(void)rotate;
double scaleX = static_cast<double>(deviceWidth) / width();
double scaleY = static_cast<double>(deviceHeight) / height();
int dx = static_cast<int>(pagePoint.x * scaleX);
int dy = static_cast<int>((height() - pagePoint.y) * scaleY);
return {dx, dy};
}
pdfengine::Point2D WasmMockPage::deviceToPage(const pdfengine::DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
(void)rotate;
double scaleX = width() / static_cast<double>(deviceWidth);
double scaleY = height() / static_cast<double>(deviceHeight);
double px = devicePoint.x * scaleX;
double py = height() - (devicePoint.y * scaleY);
return {px, py};
}
// --- WasmMockDocument Implementation ---
WasmMockDocument::WasmMockDocument(int pageCount, std::vector<uint8_t> data)
: m_pageCount(pageCount), m_data(std::move(data)) {}
pdfengine::DocumentMetadata WasmMockDocument::metadata() const noexcept {
pdfengine::DocumentMetadata meta;
meta.title = "WASM Mock Document";
meta.author = "Emscripten Engine";
meta.creator = "PdfEngine SDK";
meta.producer = "WebAssembly Facade";
meta.creationDate = "D:20260601090000";
meta.modificationDate = "D:20260601090000";
return meta;
}
std::expected<std::shared_ptr<pdfengine::PdfPage>, pdfengine::EngineError> WasmMockDocument::getPage(int pageIndex) {
if (pageIndex < 0 || pageIndex >= m_pageCount) {
return std::unexpected(pdfengine::EngineError::PageOutOfBounds);
}
auto it = m_pages.find(pageIndex);
if (it == m_pages.end()) {
auto page = std::make_shared<WasmMockPage>(pageIndex);
m_pages[pageIndex] = page;
return page;
}
return it->second;
}
std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> WasmMockDocument::getFonts(int startPage, int endPage) const {
int last = (endPage < 0) ? m_pageCount - 1 : std::min(endPage, m_pageCount - 1);
std::vector<pdfengine::FontInfo> result;
for (int i = startPage; i <= last; ++i) {
// Each mock page reports the same Helvetica → Liberation Sans substitution.
// Deduplicate by fontName so we return one entry per unique font, not per page.
bool found = false;
for (const auto& existing : result) {
if (existing.fontName == "Helvetica") { found = true; break; }
}
if (!found) {
pdfengine::FontInfo info;
info.fontName = "Helvetica";
info.type = "Type1";
info.isEmbedded = false;
info.isSubset = false;
info.isVertical = false;
info.encoding = "WinAnsiEncoding";
info.hasToUnicode = false;
info.sourceType = "Substituted";
info.substitutedFrom = "Helvetica";
info.substitutedTo = "Liberation Sans Regular";
info.normalizedFamily= "Arial";
info.internalFontId = "mock-doc-helvetica";
info.flags = 32;
info.ascent = 718.0;
info.descent = -207.0;
info.capHeight = 718.0;
result.push_back(info);
}
}
return result;
}
std::expected<void, pdfengine::EngineError> WasmMockDocument::applyEdits(const std::string& editsJson) {
(void)editsJson;
return {};
}
std::expected<std::vector<uint8_t>, pdfengine::EngineError> WasmMockDocument::saveIncremental() const {
return m_data;
}
// --- PdfEngineFacade Implementation ---
PdfEngineFacade::PdfEngineFacade() : m_nextHandle(1) {}
PdfEngineFacade::~PdfEngineFacade() = default;
int PdfEngineFacade::loadDocument(const uint8_t* buffer, int size) {
if (!buffer || size <= 0) {
return 0; // Invalid handle
}
// Realistic page count detector
int pages = 0;
if (size > 4 && buffer[0] == '%' && buffer[1] == 'P' && buffer[2] == 'D' && buffer[3] == 'F') {
std::string_view sv(reinterpret_cast<const char*>(buffer), size);
size_t pos = 0;
while ((pos = sv.find("/Type /Page", pos)) != std::string_view::npos) {
// Avoid matching /Type /Pages
if (pos + 11 < sv.size() && sv[pos + 11] != 's') {
pages++;
}
pos += 11;
}
}
if (pages == 0) {
pages = 3; // Default fallback
}
std::vector<uint8_t> docBytes(buffer, buffer + size);
auto doc = std::make_shared<WasmMockDocument>(pages, std::move(docBytes));
int handle = m_nextHandle++;
m_documents[handle] = std::move(doc);
return handle;
}
bool PdfEngineFacade::renderPage(int docHandle, int pageIndex, float scale, uint8_t* outputBuffer, int width, int height) {
auto it = m_documents.find(docHandle);
if (it == m_documents.end()) {
return false;
}
const auto& doc = it->second;
auto pageRes = doc->getPage(pageIndex);
if (!pageRes.has_value()) {
return false;
}
auto page = *pageRes;
auto mockPage = std::dynamic_pointer_cast<WasmMockPage>(page);
if (!mockPage) {
return false;
}
// Pre-initialize buffer with off-white background color: (250, 250, 245, 255)
// WasmRasterizer will visit and overwrite pixels using CTM mapping, but pre-filling is safe.
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
int idx = (y * width + x) * 4;
outputBuffer[idx + 0] = 250; // R
outputBuffer[idx + 1] = 250; // G
outputBuffer[idx + 2] = 245; // B
outputBuffer[idx + 3] = 255; // A
}
}
WasmRasterizer rasterizer(outputBuffer, width, height, scale);
mockPage->getDisplayList().replay(rasterizer);
return true;
}
void PdfEngineFacade::freeDocument(int docHandle) {
m_documents.erase(docHandle);
}
const char* PdfEngineFacade::buildInfo() {
return "PdfEngine WASM Facade (Phase 1/2 Enabled)";
}
bool PdfEngineFacade::hasSkia() {
return false;
}
// ---------- Font query helpers -----------------------------------------------
namespace {
// Local JSON serialiser (keeps the facade self-contained from wasm_engine.cpp).
std::string fontInfosToJsonLocal(const std::vector<pdfengine::FontInfo>& fonts) {
std::string json = "[";
for (size_t i = 0; i < fonts.size(); ++i) {
const auto& f = fonts[i];
if (i > 0) json += ",";
json += "{";
json += "\"fontName\":\"" + f.fontName + "\",";
json += "\"type\":\"" + f.type + "\",";
json += "\"isEmbedded\":" + std::string(f.isEmbedded ? "true" : "false") + ",";
json += "\"isSubset\":" + std::string(f.isSubset ? "true" : "false") + ",";
json += "\"isVertical\":" + std::string(f.isVertical ? "true" : "false") + ",";
json += "\"encoding\":\"" + f.encoding + "\",";
json += "\"hasToUnicode\":" + std::string(f.hasToUnicode ? "true" : "false") + ",";
json += "\"cmapName\":\"" + f.cmapName + "\",";
json += "\"cidSystemInfo\":\"" + f.cidSystemInfo + "\",";
json += "\"subsetTag\":\"" + f.subsetTag + "\",";
json += "\"sourceType\":\"" + f.sourceType + "\",";
json += "\"substitutedFrom\":\"" + f.substitutedFrom + "\",";
json += "\"substitutedTo\":\"" + f.substitutedTo + "\",";
json += "\"normalizedFamily\":\"" + f.normalizedFamily + "\",";
json += "\"internalFontId\":\"" + f.internalFontId + "\",";
json += "\"ascent\":" + std::to_string(f.ascent) + ",";
json += "\"descent\":" + std::to_string(f.descent) + ",";
json += "\"capHeight\":" + std::to_string(f.capHeight);
json += "}";
}
json += "]";
return json;
}
} // namespace
std::string PdfEngineFacade::getDocumentFonts(int docHandle, int startPage, int endPage) {
auto it = m_documents.find(docHandle);
if (it == m_documents.end()) {
return "[]";
}
auto result = it->second->getFonts(startPage, endPage);
if (!result.has_value()) {
return "[]";
}
return fontInfosToJsonLocal(*result);
}
std::string PdfEngineFacade::getPageFonts(int docHandle, int pageIndex) {
auto it = m_documents.find(docHandle);
if (it == m_documents.end()) {
return "[]";
}
auto pageRes = it->second->getPage(pageIndex);
if (!pageRes.has_value()) {
return "[]";
}
auto result = (*pageRes)->getFonts();
if (!result.has_value()) {
return "[]";
}
return fontInfosToJsonLocal(*result);
}