360 lines
13 KiB
C++
360 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<double, pdfengine::EngineError> WasmMockPage::getGlyphWidth(const std::string& fontName, uint32_t charcode, double fontSize) const {
|
|
(void)fontName; (void)charcode;
|
|
return 6.0 * (fontSize / 12.0);
|
|
}
|
|
|
|
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);
|
|
}
|