From ce5ad5bb166b13a76eac9ba94e1eb4b35a5eb496 Mon Sep 17 00:00:00 2001 From: saqib mir Date: Fri, 5 Jun 2026 10:27:39 +0530 Subject: [PATCH] fix --- engine/src/parser/pdfium_document.cpp | 50 +++++++++++++++++---------- engine/src/parser/pdfium_document.hpp | 5 ++- gateway/app/routers/documents.py | 6 ++-- gateway/app/routers/edits.py | 20 +++++------ gateway/app/routers/render.py | 20 +++++------ 5 files changed, 58 insertions(+), 43 deletions(-) diff --git a/engine/src/parser/pdfium_document.cpp b/engine/src/parser/pdfium_document.cpp index e2c9213..350694a 100644 --- a/engine/src/parser/pdfium_document.cpp +++ b/engine/src/parser/pdfium_document.cpp @@ -1,5 +1,4 @@ #include "parser/pdfium_document.hpp" - #ifdef PDFENGINE_WITH_PDFIUM #include #include @@ -996,11 +995,26 @@ std::expected, EngineError> PdfiumDocument::getPage(int if (pageIndex < 0 || pageIndex >= pageCount()) { return std::unexpected(EngineError::PageOutOfBounds); } + + { + std::lock_guard lock(pageCacheMutex_); + auto it = pageCache_.find(pageIndex); + if (it != pageCache_.end()) { + return it->second; + } + } + FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex); if (!page) { return std::unexpected(EngineError::Unknown); } - return std::make_shared(page, pageIndex); + + auto pageObj = std::make_shared(page, pageIndex); + { + std::lock_guard lock(pageCacheMutex_); + pageCache_[pageIndex] = pageObj; + } + return pageObj; #else (void)pageIndex; return std::unexpected(EngineError::Unknown); @@ -1167,7 +1181,7 @@ std::expected PdfiumDocument::applyEdits(const std::string& e return std::unexpected(EngineError::Unknown); } - invalidateFontCache(); + invalidateCaches(); return {}; #else (void)editsJson; @@ -1440,12 +1454,6 @@ std::expected, EngineError> PdfiumDocument::getFonts(int s return std::vector(); } - // Security Safeguard: cap maximum scan range to 1000 pages to prevent memory/CPU exhaustion - int scanCount = endPage - startPage + 1; - if (scanCount > 1000) { - spdlog::warn("Requested scan range ({} pages) exceeds limit. Capping scan to 1000 pages.", scanCount); - endPage = startPage + 999; - } // Return full document-level cache if available and full range is requested if (startPage == 0 && endPage == total - 1 && hasCachedFonts_) { @@ -1454,15 +1462,13 @@ std::expected, EngineError> PdfiumDocument::getFonts(int s std::vector aggregated; for (int i = startPage; i <= endPage; ++i) { - FPDF_PAGE page = FPDF_LoadPage(doc_, i); - if (!page) { + auto pageRes = const_cast(this)->getPage(i); + if (!pageRes) { spdlog::error("Failed to load page index {} for font diagnostics", i); continue; } - // Stack-allocated wrapper ensures FPDF handles are closed properly upon destruction - PdfiumPage tempPage(page, i); - auto pageFontsRes = tempPage.getFonts(); + auto pageFontsRes = pageRes.value()->getFonts(); if (pageFontsRes) { for (const auto& f : *pageFontsRes) { auto it = std::find_if(aggregated.begin(), aggregated.end(), [&](const FontInfo& existing) { @@ -1503,11 +1509,17 @@ std::expected, EngineError> PdfiumDocument::getFonts(int s #endif } -void PdfiumDocument::invalidateFontCache() { - std::lock_guard lock(fontsMutex_); - cachedFonts_.clear(); - hasCachedFonts_ = false; - spdlog::info("Document font cache has been invalidated."); +void PdfiumDocument::invalidateCaches() { + { + std::lock_guard lock(fontsMutex_); + cachedFonts_.clear(); + hasCachedFonts_ = false; + } + { + std::lock_guard lock(pageCacheMutex_); + pageCache_.clear(); + } + spdlog::info("Document caches have been invalidated."); } } diff --git a/engine/src/parser/pdfium_document.hpp b/engine/src/parser/pdfium_document.hpp index 741de3a..abe0d53 100644 --- a/engine/src/parser/pdfium_document.hpp +++ b/engine/src/parser/pdfium_document.hpp @@ -77,7 +77,7 @@ public: std::expected, EngineError> getPage(int pageIndex) override; std::expected, EngineError> getFonts(int startPage = 0, int endPage = -1) const override; - void invalidateFontCache(); + void invalidateCaches(); std::expected applyEdits(const std::string& editsJson) override; std::expected, EngineError> saveIncremental() const override; @@ -89,6 +89,9 @@ private: mutable std::vector cachedFonts_; mutable bool hasCachedFonts_ = false; mutable std::mutex fontsMutex_; + + mutable std::unordered_map> pageCache_; + mutable std::mutex pageCacheMutex_; }; // Exposed for testing diff --git a/gateway/app/routers/documents.py b/gateway/app/routers/documents.py index 7019b9e..c319c8e 100644 --- a/gateway/app/routers/documents.py +++ b/gateway/app/routers/documents.py @@ -1,5 +1,5 @@ -from typing import List -from fastapi import APIRouter, HTTPException, status, File, UploadFile +from typing import List, Annotated +from fastapi import APIRouter, HTTPException, status, File, UploadFile, Query from pydantic import BaseModel from app.services import engine @@ -159,7 +159,7 @@ class FontInfoResponse(BaseModel): capHeight: float @router.get("/{document_id}/fonts", response_model=List[FontInfoResponse]) -def get_document_fonts(document_id: str, start_page: int = 0, end_page: int = -1) -> List[FontInfoResponse]: +def get_document_fonts(document_id: str, start_page: Annotated[int, Query(ge=0)] = 0, end_page: Annotated[int, Query(ge=-1)] = -1) -> List[FontInfoResponse]: if not engine.is_available(): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, diff --git a/gateway/app/routers/edits.py b/gateway/app/routers/edits.py index 5bfb916..5bd5322 100644 --- a/gateway/app/routers/edits.py +++ b/gateway/app/routers/edits.py @@ -18,7 +18,7 @@ class TextOverlayData(BaseModel): y: float width: float height: float - fontSize: float + fontSize: float = Field(..., gt=0) fontFamily: str color: str @@ -59,7 +59,7 @@ class FreeTextData(BaseModel): width: float height: float text: str - fontSize: float = 12.0 + fontSize: float = Field(12.0, gt=0) color: str = "#000000" class StickyNoteData(BaseModel): @@ -83,49 +83,49 @@ class PageRotationData(BaseModel): class TextOverlayOperation(BaseModel): id: str type: Literal["text_overlay"] - pageIndex: int + pageIndex: int = Field(..., ge=0) data: TextOverlayData class RedactionOperation(BaseModel): id: str type: Literal["redaction"] - pageIndex: int + pageIndex: int = Field(..., ge=0) data: RedactionData class ImageOverlayOperation(BaseModel): id: str type: Literal["image_overlay"] - pageIndex: int + pageIndex: int = Field(..., ge=0) data: ImageOverlayData class HighlightOperation(BaseModel): id: str type: Literal["highlight"] - pageIndex: int + pageIndex: int = Field(..., ge=0) data: HighlightData class FreeTextOperation(BaseModel): id: str type: Literal["free_text"] - pageIndex: int + pageIndex: int = Field(..., ge=0) data: FreeTextData class CommentOperation(BaseModel): id: str type: Literal["comment"] - pageIndex: int + pageIndex: int = Field(..., ge=0) data: StickyNoteData class FreehandOperation(BaseModel): id: str type: Literal["freehand"] - pageIndex: int + pageIndex: int = Field(..., ge=0) data: FreehandData class PageRotationOperation(BaseModel): id: str type: Literal["page_rotation"] - pageIndex: int + pageIndex: int = Field(..., ge=0) data: PageRotationData EditOperation = Annotated[ diff --git a/gateway/app/routers/render.py b/gateway/app/routers/render.py index 6472800..d6a0495 100644 --- a/gateway/app/routers/render.py +++ b/gateway/app/routers/render.py @@ -1,5 +1,5 @@ -from typing import List -from fastapi import APIRouter, HTTPException, status, Response +from typing import List, Annotated +from fastapi import APIRouter, HTTPException, status, Response, Path, Query from app.services import engine from app.services.store import document_store @@ -9,7 +9,7 @@ router = APIRouter(prefix="/documents/{document_id}/pages", tags=["render"]) compat_router = APIRouter(tags=["render"]) @router.get("/{page_index}/render") -def render_page(document_id: str, page_index: int, dpi: int = 96) -> Response: +def render_page(document_id: str, page_index: Annotated[int, Path(ge=0)], dpi: int = 96) -> Response: if not engine.is_available(): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, @@ -31,7 +31,7 @@ def render_page(document_id: str, page_index: int, dpi: int = 96) -> Response: 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): +def extract_page_text(document_id: str, page_index: Annotated[int, Path(ge=0)]): if not engine.is_available(): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, @@ -57,12 +57,12 @@ def extract_page_text(document_id: str, page_index: int): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @compat_router.get("/render/{document_id}") -def render_page_compat(document_id: str, page: int = 0, zoom: float = 1.0, rotation: int = 0) -> Response: +def render_page_compat(document_id: str, page: Annotated[int, Query(ge=0)] = 0, zoom: float = 1.0, rotation: int = 0) -> Response: dpi = int(96 * zoom) return render_page(document_id, page, dpi) @router.get("/{page_index}") -def get_page_info(document_id: str, page_index: int): +def get_page_info(document_id: str, page_index: Annotated[int, Path(ge=0)]): if not engine.is_available(): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, @@ -83,7 +83,7 @@ def get_page_info(document_id: str, page_index: int): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.get("/{page_index}/transform/page-to-device") -def transform_page_to_device(document_id: str, page_index: int, x: float, y: float, device_width: int, device_height: int, rotate: int = 0): +def transform_page_to_device(document_id: str, page_index: Annotated[int, Path(ge=0)], x: float, y: float, device_width: int, device_height: int, rotate: int = 0): if not engine.is_available(): raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Engine unavailable") @@ -103,7 +103,7 @@ def transform_page_to_device(document_id: str, page_index: int, x: float, y: flo raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.get("/{page_index}/transform/device-to-page") -def transform_device_to_page(document_id: str, page_index: int, x: int, y: int, device_width: int, device_height: int, rotate: int = 0): +def transform_device_to_page(document_id: str, page_index: Annotated[int, Path(ge=0)], x: int, y: int, device_width: int, device_height: int, rotate: int = 0): if not engine.is_available(): raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Engine unavailable") @@ -122,7 +122,7 @@ def transform_device_to_page(document_id: str, page_index: int, x: int, y: int, except Exception as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @router.get("/{page_index}/fonts", response_model=List[FontInfoResponse]) -def get_page_fonts(document_id: str, page_index: int) -> List[FontInfoResponse]: +def get_page_fonts(document_id: str, page_index: Annotated[int, Path(ge=0)]) -> List[FontInfoResponse]: if not engine.is_available(): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, @@ -168,7 +168,7 @@ def get_page_fonts(document_id: str, page_index: int) -> List[FontInfoResponse]: @router.get("/{page_index}/fonts/glyph-width") -def get_page_glyph_width(document_id: str, page_index: int, font_name: str, charcode: int, font_size: float = 12.0): +def get_page_glyph_width(document_id: str, page_index: Annotated[int, Path(ge=0)], font_name: str, charcode: int, font_size: Annotated[float, Query(gt=0)] = 12.0): if not engine.is_available(): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED,