This commit is contained in:
saqib mir
2026-06-05 10:27:39 +05:30
parent 0862afa1ce
commit ce5ad5bb16
5 changed files with 58 additions and 43 deletions
+31 -19
View File
@@ -1,5 +1,4 @@
#include "parser/pdfium_document.hpp"
#ifdef PDFENGINE_WITH_PDFIUM
#include <fpdfview.h>
#include <fpdf_text.h>
@@ -996,11 +995,26 @@ std::expected<std::shared_ptr<PdfPage>, EngineError> PdfiumDocument::getPage(int
if (pageIndex < 0 || pageIndex >= pageCount()) {
return std::unexpected(EngineError::PageOutOfBounds);
}
{
std::lock_guard<std::mutex> 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<PdfiumPage>(page, pageIndex);
auto pageObj = std::make_shared<PdfiumPage>(page, pageIndex);
{
std::lock_guard<std::mutex> lock(pageCacheMutex_);
pageCache_[pageIndex] = pageObj;
}
return pageObj;
#else
(void)pageIndex;
return std::unexpected(EngineError::Unknown);
@@ -1167,7 +1181,7 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
return std::unexpected(EngineError::Unknown);
}
invalidateFontCache();
invalidateCaches();
return {};
#else
(void)editsJson;
@@ -1440,12 +1454,6 @@ std::expected<std::vector<FontInfo>, EngineError> PdfiumDocument::getFonts(int s
return std::vector<FontInfo>();
}
// 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<std::vector<FontInfo>, EngineError> PdfiumDocument::getFonts(int s
std::vector<FontInfo> aggregated;
for (int i = startPage; i <= endPage; ++i) {
FPDF_PAGE page = FPDF_LoadPage(doc_, i);
if (!page) {
auto pageRes = const_cast<PdfiumDocument*>(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<std::vector<FontInfo>, EngineError> PdfiumDocument::getFonts(int s
#endif
}
void PdfiumDocument::invalidateFontCache() {
std::lock_guard<std::mutex> lock(fontsMutex_);
cachedFonts_.clear();
hasCachedFonts_ = false;
spdlog::info("Document font cache has been invalidated.");
void PdfiumDocument::invalidateCaches() {
{
std::lock_guard<std::mutex> lock(fontsMutex_);
cachedFonts_.clear();
hasCachedFonts_ = false;
}
{
std::lock_guard<std::mutex> lock(pageCacheMutex_);
pageCache_.clear();
}
spdlog::info("Document caches have been invalidated.");
}
}
+4 -1
View File
@@ -77,7 +77,7 @@ public:
std::expected<std::shared_ptr<PdfPage>, EngineError> getPage(int pageIndex) override;
std::expected<std::vector<FontInfo>, EngineError> getFonts(int startPage = 0, int endPage = -1) const override;
void invalidateFontCache();
void invalidateCaches();
std::expected<void, EngineError> applyEdits(const std::string& editsJson) override;
std::expected<std::vector<uint8_t>, EngineError> saveIncremental() const override;
@@ -89,6 +89,9 @@ private:
mutable std::vector<FontInfo> cachedFonts_;
mutable bool hasCachedFonts_ = false;
mutable std::mutex fontsMutex_;
mutable std::unordered_map<int, std::shared_ptr<PdfPage>> pageCache_;
mutable std::mutex pageCacheMutex_;
};
// Exposed for testing
+3 -3
View File
@@ -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,
+10 -10
View File
@@ -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[
+10 -10
View File
@@ -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,