fix
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user