search fuunction done
This commit is contained in:
+30
-8
@@ -5,7 +5,7 @@ import { PDFViewer } from './viewer/PDFViewer';
|
||||
import type { PDFViewerRef } from './viewer/PDFViewer';
|
||||
import type { Annotation } from './viewer/AnnotationLayer';
|
||||
import { gatewayService } from './lib/gatewayService';
|
||||
import type { DocumentInfo } from './lib/gatewayService';
|
||||
import type { DocumentInfo, SearchResult } from './lib/gatewayService';
|
||||
import { wasmLoader } from './lib/wasmLoader';
|
||||
import './App.css';
|
||||
|
||||
@@ -29,16 +29,30 @@ function App() {
|
||||
|
||||
// Search State
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||
const [searchResultCount, setSearchResultCount] = useState(0);
|
||||
const [searchCurrentMatch, setSearchCurrentMatch] = useState(0);
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
const handleSearch = async (query: string) => {
|
||||
setSearchQuery(query);
|
||||
// Mock search results for Phase 0
|
||||
if (query) {
|
||||
setSearchResultCount(5);
|
||||
if (!query || !selectedDocId) {
|
||||
setSearchResults([]);
|
||||
setSearchResultCount(0);
|
||||
setSearchCurrentMatch(0);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await gatewayService.searchDocument(selectedDocId, query);
|
||||
setSearchResults(results);
|
||||
setSearchResultCount(results.length);
|
||||
setSearchCurrentMatch(0);
|
||||
if (results.length > 0) {
|
||||
viewerRef.current?.scrollToPage(results[0].pageIndex);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Search failed", err);
|
||||
setSearchResults([]);
|
||||
setSearchResultCount(0);
|
||||
setSearchCurrentMatch(0);
|
||||
}
|
||||
@@ -46,13 +60,17 @@ function App() {
|
||||
|
||||
const handleSearchNext = () => {
|
||||
if (searchResultCount > 0) {
|
||||
setSearchCurrentMatch((prev) => (prev + 1) % searchResultCount);
|
||||
const nextMatch = (searchCurrentMatch + 1) % searchResultCount;
|
||||
setSearchCurrentMatch(nextMatch);
|
||||
viewerRef.current?.scrollToPage(searchResults[nextMatch].pageIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchPrev = () => {
|
||||
if (searchResultCount > 0) {
|
||||
setSearchCurrentMatch((prev) => (prev - 1 + searchResultCount) % searchResultCount);
|
||||
const prevMatch = (searchCurrentMatch - 1 + searchResultCount) % searchResultCount;
|
||||
setSearchCurrentMatch(prevMatch);
|
||||
viewerRef.current?.scrollToPage(searchResults[prevMatch].pageIndex);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -198,11 +216,15 @@ function App() {
|
||||
ref={viewerRef}
|
||||
documentId={activeDoc.id}
|
||||
totalPages={activeDoc.totalPages}
|
||||
pageWidth={activeDoc.pageWidth}
|
||||
pageHeight={activeDoc.pageHeight}
|
||||
zoom={zoom}
|
||||
rotation={rotation}
|
||||
activeTool={activeTool}
|
||||
annotations={annotations}
|
||||
searchQuery={searchQuery}
|
||||
searchResults={searchResults}
|
||||
searchCurrentMatch={searchCurrentMatch}
|
||||
onAnnotationAdded={handleAnnotationAdded}
|
||||
onPageVisible={setCurrentPage}
|
||||
/>
|
||||
|
||||
@@ -160,6 +160,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
Highlight
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onActiveToolChange('draw')}
|
||||
className={`tool-btn draw ${activeTool === 'draw' ? 'active' : ''}`}
|
||||
title="Freehand Ink Pen"
|
||||
>
|
||||
Draw
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onActiveToolChange('signature')}
|
||||
className={`tool-btn signature ${activeTool === 'signature' ? 'active' : ''}`}
|
||||
|
||||
@@ -313,6 +313,12 @@ body {
|
||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
.tool-btn.draw.active {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #60a5fa;
|
||||
border: 1px solid rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.tool-btn.signature.active {
|
||||
background: rgba(79, 70, 229, 0.2);
|
||||
color: #c7d2fe;
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface DocumentInfo {
|
||||
filename: string;
|
||||
sizeBytes: number;
|
||||
totalPages: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
uploadedAt: string;
|
||||
status: 'processing' | 'ready' | 'error';
|
||||
}
|
||||
@@ -21,7 +23,20 @@ export interface RenderParams {
|
||||
rotation: number;
|
||||
}
|
||||
|
||||
import type { Point, Rect } from './coordinateMapping';
|
||||
import type { Point } from './coordinateMapping';
|
||||
|
||||
export interface SearchRect {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
pageIndex: number;
|
||||
rects: SearchRect[];
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface TextOverlayData {
|
||||
text: string;
|
||||
@@ -171,6 +186,8 @@ class GatewayService {
|
||||
filename: file.name,
|
||||
sizeBytes: file.size,
|
||||
totalPages: 5, // Mocked total pages
|
||||
pageWidth: 612,
|
||||
pageHeight: 792,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
status: 'ready',
|
||||
});
|
||||
@@ -248,6 +265,21 @@ class GatewayService {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async searchDocument(documentId: string, query: string): Promise<SearchResult[]> {
|
||||
if (!query) return [];
|
||||
|
||||
const urlParams = new URLSearchParams({ q: query });
|
||||
const response = await fetch(`${this.baseUrl}/documents/${documentId}/search?${urlParams.toString()}`);
|
||||
|
||||
if (response.status === 501) {
|
||||
// Return mock empty results if backend isn't available
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!response.ok) throw new Error(`Failed to search document: ${response.statusText}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private getMockDocuments(): DocumentInfo[] {
|
||||
return [
|
||||
{
|
||||
@@ -255,6 +287,8 @@ class GatewayService {
|
||||
filename: 'Quarterly_Financial_Report.pdf',
|
||||
sizeBytes: 1024 * 1024 * 3.4, // 3.4MB
|
||||
totalPages: 12,
|
||||
pageWidth: 612,
|
||||
pageHeight: 792,
|
||||
uploadedAt: new Date(Date.now() - 1000 * 60 * 60 * 3).toISOString(),
|
||||
status: 'ready',
|
||||
},
|
||||
@@ -263,6 +297,8 @@ class GatewayService {
|
||||
filename: 'Engineering_Specification_v4.pdf',
|
||||
sizeBytes: 1024 * 1024 * 18.2, // 18.2MB
|
||||
totalPages: 54,
|
||||
pageWidth: 612,
|
||||
pageHeight: 792,
|
||||
uploadedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(),
|
||||
status: 'ready',
|
||||
},
|
||||
@@ -271,6 +307,8 @@ class GatewayService {
|
||||
filename: 'Tenant_Lease_Agreement_Final.pdf',
|
||||
sizeBytes: 1024 * 245, // 245KB
|
||||
totalPages: 4,
|
||||
pageWidth: 612,
|
||||
pageHeight: 792,
|
||||
uploadedAt: new Date(Date.now() - 1000 * 60 * 45).toISOString(),
|
||||
status: 'ready',
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ class WasmLoader {
|
||||
try {
|
||||
// Dynamic import from the public folder / static route
|
||||
// @ts-ignore
|
||||
const createModule = (await import(/* @vite-ignore */ '/pdfengine.mjs')).default;
|
||||
const moduleUrl = (await import('/pdfengine.mjs?url')).default;
|
||||
const createModule = (await import(/* @vite-ignore */ moduleUrl)).default;
|
||||
const Module = await createModule({
|
||||
locateFile: (path: string) => {
|
||||
if (path.endsWith('.wasm')) {
|
||||
|
||||
@@ -3,11 +3,12 @@ import type { Rect } from '../lib/coordinateMapping';
|
||||
|
||||
export interface Annotation {
|
||||
id: string;
|
||||
type: 'highlight' | 'signature' | 'strikeout' | 'comment';
|
||||
type: 'highlight' | 'signature' | 'strikeout' | 'comment' | 'ink';
|
||||
bbox: Rect;
|
||||
color?: string;
|
||||
author: string;
|
||||
content?: string;
|
||||
paths?: { x: number; y: number }[][];
|
||||
}
|
||||
|
||||
interface AnnotationLayerProps {
|
||||
@@ -59,6 +60,23 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Ink Annotations */}
|
||||
<svg
|
||||
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}
|
||||
>
|
||||
{annotations
|
||||
.filter((anno) => anno.type === 'ink' && anno.paths)
|
||||
.map((anno) => (
|
||||
<g key={anno.id} stroke={anno.color || '#3b82f6'} strokeWidth={2 * zoom} fill="none" strokeLinecap="round" strokeLinejoin="round">
|
||||
{anno.paths!.map((path, i) => {
|
||||
if (path.length === 0) return null;
|
||||
const d = path.map((pt, j) => `${j === 0 ? 'M' : 'L'} ${pt.x * zoom} ${pt.y * zoom}`).join(' ');
|
||||
return <path key={i} d={d} />;
|
||||
})}
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import type { Annotation } from './AnnotationLayer';
|
||||
|
||||
interface OverlayLayerProps {
|
||||
pageIndex: number;
|
||||
width: number;
|
||||
height: number;
|
||||
activeTool: string;
|
||||
onDrawStroke?: (path: string) => void;
|
||||
zoom: number;
|
||||
onAnnotationAdded?: (anno: Annotation) => void;
|
||||
}
|
||||
|
||||
export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
||||
@@ -13,11 +15,70 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
||||
width,
|
||||
height,
|
||||
activeTool,
|
||||
zoom,
|
||||
onAnnotationAdded,
|
||||
}) => {
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [currentPath, setCurrentPath] = useState<{ x: number; y: number }[]>([]);
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
|
||||
const getCoordinates = (e: React.MouseEvent | MouseEvent) => {
|
||||
if (!svgRef.current) return { x: 0, y: 0 };
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
return {
|
||||
x: (e.clientX - rect.left) / zoom,
|
||||
y: (e.clientY - rect.top) / zoom,
|
||||
};
|
||||
};
|
||||
|
||||
const handlePointerDown = (e: React.PointerEvent) => {
|
||||
if (activeTool !== 'draw') return;
|
||||
setIsDrawing(true);
|
||||
e.target.setPointerCapture?.(e.pointerId);
|
||||
setCurrentPath([getCoordinates(e)]);
|
||||
};
|
||||
|
||||
const handlePointerMove = (e: React.PointerEvent) => {
|
||||
if (!isDrawing || activeTool !== 'draw') return;
|
||||
setCurrentPath((prev) => [...prev, getCoordinates(e)]);
|
||||
};
|
||||
|
||||
const handlePointerUp = (e: React.PointerEvent) => {
|
||||
if (!isDrawing || activeTool !== 'draw') return;
|
||||
setIsDrawing(false);
|
||||
e.target.releasePointerCapture?.(e.pointerId);
|
||||
|
||||
if (currentPath.length > 1) {
|
||||
// Calculate bounding box
|
||||
const xs = currentPath.map(p => p.x);
|
||||
const ys = currentPath.map(p => p.y);
|
||||
const minX = Math.min(...xs);
|
||||
const maxX = Math.max(...xs);
|
||||
const minY = Math.min(...ys);
|
||||
const maxY = Math.max(...ys);
|
||||
|
||||
const newAnno: Annotation = {
|
||||
id: `anno_${Math.random().toString(36).substring(2, 11)}`,
|
||||
type: 'ink',
|
||||
bbox: {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
},
|
||||
author: 'Current User',
|
||||
paths: [currentPath],
|
||||
color: '#3b82f6', // Default blue color for now
|
||||
};
|
||||
onAnnotationAdded?.(newAnno);
|
||||
}
|
||||
setCurrentPath([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overlay-layer"
|
||||
style={{ width: `${width}px`, height: `${height}px` }}
|
||||
style={{ width: `${width}px`, height: `${height}px`, pointerEvents: activeTool === 'draw' ? 'auto' : 'none' }}
|
||||
>
|
||||
{/* Signature overlay state indicator */}
|
||||
{activeTool === 'signature' && (
|
||||
@@ -29,9 +90,30 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
||||
)}
|
||||
|
||||
{activeTool === 'draw' && (
|
||||
<div className="overlay-toast animate-pulse">
|
||||
Freehand Ink Pen Enabled
|
||||
</div>
|
||||
<>
|
||||
<div className="overlay-toast animate-pulse" style={{ pointerEvents: 'none' }}>
|
||||
Freehand Ink Pen Enabled
|
||||
</div>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
style={{ width: '100%', height: '100%', position: 'absolute', top: 0, left: 0, cursor: 'crosshair', touchAction: 'none' }}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
>
|
||||
{currentPath.length > 0 && (
|
||||
<path
|
||||
d={currentPath.map((pt, i) => `${i === 0 ? 'M' : 'L'} ${pt.x * zoom} ${pt.y * zoom}`).join(' ')}
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2 * zoom}
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,15 +7,20 @@ import { OverlayLayer } from './OverlayLayer';
|
||||
import { SearchOverlayLayer } from './SearchOverlayLayer';
|
||||
import type { Rect } from '../lib/coordinateMapping';
|
||||
import { gatewayService } from '../lib/gatewayService';
|
||||
import type { SearchResult } from '../lib/gatewayService';
|
||||
|
||||
interface PDFViewerProps {
|
||||
documentId: string;
|
||||
totalPages: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
zoom: number;
|
||||
rotation: number;
|
||||
activeTool: string;
|
||||
annotations: Annotation[];
|
||||
searchQuery?: string;
|
||||
searchResults?: SearchResult[];
|
||||
searchCurrentMatch?: number;
|
||||
onAnnotationAdded?: (anno: Annotation) => void;
|
||||
onPageVisible?: (pageIndex: number) => void;
|
||||
}
|
||||
@@ -36,11 +41,15 @@ export interface PDFViewerRef {
|
||||
export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
documentId,
|
||||
totalPages,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
zoom,
|
||||
rotation,
|
||||
activeTool,
|
||||
annotations,
|
||||
searchQuery,
|
||||
searchResults,
|
||||
searchCurrentMatch,
|
||||
onAnnotationAdded,
|
||||
onPageVisible,
|
||||
}, ref) => {
|
||||
@@ -49,9 +58,9 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
const [renderedPages, setRenderedPages] = useState<string[]>([]);
|
||||
const [containerHeight, setContainerHeight] = useState(800);
|
||||
|
||||
// Standard Page Dimensions: Letter size is 612x792 pt
|
||||
const basePageWidth = 612;
|
||||
const basePageHeight = 792;
|
||||
// Use provided dimensions or fallback to Letter size
|
||||
const basePageWidth = pageWidth || 612;
|
||||
const basePageHeight = pageHeight || 792;
|
||||
const pageGap = 24; // space between pages
|
||||
|
||||
// Calculate layout coordinates for all pages sequentially
|
||||
@@ -298,6 +307,8 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
width={page.width}
|
||||
height={page.height}
|
||||
activeTool={activeTool}
|
||||
zoom={zoom}
|
||||
onAnnotationAdded={onAnnotationAdded}
|
||||
/>
|
||||
|
||||
{/* Search highlights overlay */}
|
||||
@@ -307,6 +318,8 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
height={page.height}
|
||||
zoom={zoom}
|
||||
searchQuery={searchQuery || ''}
|
||||
searchResults={searchResults}
|
||||
searchCurrentMatch={searchCurrentMatch}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import type { SearchResult } from '../lib/gatewayService';
|
||||
|
||||
interface SearchOverlayLayerProps {
|
||||
pageIndex: number;
|
||||
@@ -6,6 +7,8 @@ interface SearchOverlayLayerProps {
|
||||
height: number;
|
||||
zoom: number;
|
||||
searchQuery: string;
|
||||
searchResults?: SearchResult[];
|
||||
searchCurrentMatch?: number;
|
||||
}
|
||||
|
||||
export const SearchOverlayLayer: React.FC<SearchOverlayLayerProps> = ({
|
||||
@@ -14,47 +17,45 @@ export const SearchOverlayLayer: React.FC<SearchOverlayLayerProps> = ({
|
||||
height,
|
||||
zoom,
|
||||
searchQuery,
|
||||
searchResults = [],
|
||||
searchCurrentMatch = 0,
|
||||
}) => {
|
||||
if (!searchQuery) return null;
|
||||
if (!searchQuery || searchResults.length === 0) return null;
|
||||
|
||||
// Mock some search results based on the query for Phase 0/1
|
||||
// We'll just generate deterministic-looking boxes so it looks like it found something
|
||||
const mockResults = [];
|
||||
const hash = searchQuery.length + pageIndex;
|
||||
|
||||
if (hash % 3 !== 0) {
|
||||
mockResults.push({
|
||||
x: 100 * zoom,
|
||||
y: (150 + hash * 10) * zoom,
|
||||
width: 120 * zoom,
|
||||
height: 18 * zoom,
|
||||
});
|
||||
}
|
||||
if (hash % 2 === 0) {
|
||||
mockResults.push({
|
||||
x: 300 * zoom,
|
||||
y: (250 + hash * 5) * zoom,
|
||||
width: 80 * zoom,
|
||||
height: 18 * zoom,
|
||||
});
|
||||
}
|
||||
// Filter results for this specific page and keep track of global index
|
||||
const pageMatches = searchResults
|
||||
.map((result, globalIndex) => ({ ...result, globalIndex }))
|
||||
.filter((result) => result.pageIndex === pageIndex);
|
||||
|
||||
if (pageMatches.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute top-0 left-0 pointer-events-none z-20"
|
||||
style={{ width: `${width}px`, height: `${height}px` }}
|
||||
>
|
||||
{mockResults.map((rect, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="absolute bg-yellow-400/40 border border-yellow-500/60 rounded-sm"
|
||||
style={{
|
||||
left: `${rect.x}px`,
|
||||
top: `${rect.y}px`,
|
||||
width: `${rect.width}px`,
|
||||
height: `${rect.height}px`,
|
||||
}}
|
||||
/>
|
||||
{pageMatches.map((match) => (
|
||||
<React.Fragment key={match.globalIndex}>
|
||||
{match.rects.map((rect, rectIdx) => {
|
||||
const isActive = match.globalIndex === searchCurrentMatch;
|
||||
return (
|
||||
<div
|
||||
key={rectIdx}
|
||||
className={`absolute rounded-sm border ${
|
||||
isActive
|
||||
? 'bg-orange-500/50 border-orange-600/80 shadow-[0_0_8px_rgba(249,115,22,0.6)] z-30'
|
||||
: 'bg-yellow-400/40 border-yellow-500/60'
|
||||
}`}
|
||||
style={{
|
||||
left: `${rect.x * zoom}px`,
|
||||
top: `${rect.y * zoom}px`,
|
||||
width: `${rect.w * zoom}px`,
|
||||
height: `${rect.h * zoom}px`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,8 @@ class DocumentInfoResponse(BaseModel):
|
||||
filename: str
|
||||
sizeBytes: int
|
||||
totalPages: int
|
||||
pageWidth: float
|
||||
pageHeight: float
|
||||
uploadedAt: str
|
||||
status: str
|
||||
|
||||
@@ -33,6 +35,8 @@ async def upload_document(file: UploadFile = File(...), password: str = "") -> D
|
||||
filename=info["filename"],
|
||||
sizeBytes=info["sizeBytes"],
|
||||
totalPages=info["totalPages"],
|
||||
pageWidth=info["pageWidth"],
|
||||
pageHeight=info["pageHeight"],
|
||||
uploadedAt=info["uploadedAt"],
|
||||
status=info["status"]
|
||||
)
|
||||
@@ -62,6 +66,8 @@ def list_documents() -> List[DocumentInfoResponse]:
|
||||
filename=d["filename"],
|
||||
sizeBytes=d["sizeBytes"],
|
||||
totalPages=d["totalPages"],
|
||||
pageWidth=d.get("pageWidth", 612.0),
|
||||
pageHeight=d.get("pageHeight", 792.0),
|
||||
uploadedAt=d["uploadedAt"],
|
||||
status=d["status"]
|
||||
)
|
||||
@@ -85,6 +91,8 @@ def get_document(document_id: str) -> DocumentInfoResponse:
|
||||
filename=d["filename"],
|
||||
sizeBytes=d["sizeBytes"],
|
||||
totalPages=d["totalPages"],
|
||||
pageWidth=d.get("pageWidth", 612.0),
|
||||
pageHeight=d.get("pageHeight", 792.0),
|
||||
uploadedAt=d["uploadedAt"],
|
||||
status=d["status"]
|
||||
)
|
||||
@@ -202,4 +210,96 @@ def get_document_fonts(document_id: str, start_page: int = 0, end_page: int = -1
|
||||
except IndexError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
class SearchRect(BaseModel):
|
||||
x: float
|
||||
y: float
|
||||
w: float
|
||||
h: float
|
||||
|
||||
class SearchMatch(BaseModel):
|
||||
pageIndex: int
|
||||
rects: List[SearchRect]
|
||||
text: str
|
||||
|
||||
@router.get("/{document_id}/search", response_model=List[SearchMatch])
|
||||
def search_document(document_id: str, q: str) -> List[SearchMatch]:
|
||||
if not engine.is_available():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Engine bridge (bindings/python) not yet available."
|
||||
)
|
||||
|
||||
if not q:
|
||||
return []
|
||||
|
||||
doc_info = document_store.get_document(document_id)
|
||||
if not doc_info:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||
|
||||
try:
|
||||
doc = doc_info["doc_instance"]
|
||||
matches = []
|
||||
lower_query = q.lower()
|
||||
query_len = len(lower_query)
|
||||
|
||||
for page_idx in range(doc.page_count):
|
||||
page = doc.get_page(page_idx)
|
||||
glyphs = page.extract_text_with_bounds()
|
||||
if not glyphs:
|
||||
continue
|
||||
|
||||
text_str = ""
|
||||
char_to_glyph = []
|
||||
for i, g in enumerate(glyphs):
|
||||
s = g.get("text", "")
|
||||
start_len = len(text_str)
|
||||
text_str += s
|
||||
for _ in range(len(text_str) - start_len):
|
||||
char_to_glyph.append(i)
|
||||
|
||||
lower_text = text_str.lower()
|
||||
idx = 0
|
||||
while True:
|
||||
idx = lower_text.find(lower_query, idx)
|
||||
if idx == -1:
|
||||
break
|
||||
|
||||
start_glyph_idx = char_to_glyph[idx]
|
||||
end_glyph_idx = char_to_glyph[idx + query_len - 1]
|
||||
|
||||
rects = []
|
||||
current_rect = None
|
||||
|
||||
for g_idx in range(start_glyph_idx, end_glyph_idx + 1):
|
||||
g = glyphs[g_idx]
|
||||
dom_y = page.height - (g["y"] + g["h"])
|
||||
|
||||
if current_rect is None:
|
||||
current_rect = {"x": g["x"], "y": dom_y, "w": g["w"], "h": g["h"]}
|
||||
else:
|
||||
if abs(dom_y - current_rect["y"]) < g.get("fontSize", 12) * 0.5:
|
||||
max_x = max(current_rect["x"] + current_rect["w"], g["x"] + g["w"])
|
||||
current_rect["w"] = max_x - current_rect["x"]
|
||||
current_rect["y"] = min(current_rect["y"], dom_y)
|
||||
current_rect["h"] = max(current_rect["h"], g["h"])
|
||||
else:
|
||||
rects.append(SearchRect(**current_rect))
|
||||
current_rect = {"x": g["x"], "y": dom_y, "w": g["w"], "h": g["h"]}
|
||||
|
||||
if current_rect:
|
||||
rects.append(SearchRect(**current_rect))
|
||||
|
||||
matches.append(SearchMatch(
|
||||
pageIndex=page_idx,
|
||||
rects=rects,
|
||||
text=text_str[idx:idx + query_len]
|
||||
))
|
||||
|
||||
idx += 1
|
||||
|
||||
return matches
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
@@ -11,12 +11,23 @@ class DocumentStore:
|
||||
def add_document(self, filename: str, bytes_data: bytes, doc_instance: Any) -> Dict[str, Any]:
|
||||
doc_id = str(uuid.uuid4())
|
||||
uploaded_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
page_width = 612.0
|
||||
page_height = 792.0
|
||||
if doc_instance.page_count > 0:
|
||||
try:
|
||||
page_0 = doc_instance.get_page(0)
|
||||
page_width = page_0.width
|
||||
page_height = page_0.height
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
info = {
|
||||
"id": doc_id,
|
||||
"filename": filename,
|
||||
"sizeBytes": len(bytes_data),
|
||||
"totalPages": doc_instance.page_count,
|
||||
"pageWidth": page_width,
|
||||
"pageHeight": page_height,
|
||||
"uploadedAt": uploaded_at,
|
||||
"status": "ready",
|
||||
"doc_instance": doc_instance,
|
||||
|
||||
+10
-9
@@ -43,19 +43,20 @@ if (-not $vcpkgRoot) {
|
||||
Write-Host "VCPKG_ROOT defaulted to: $vcpkgRoot" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$vcvars = "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
|
||||
if (-not (Test-Path $vcvars)) {
|
||||
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||
if (Test-Path $vswhere) {
|
||||
$vsPath = (& $vswhere -latest -prerelease -products * -property installationPath | Select-Object -First 1)
|
||||
if ($vsPath) {
|
||||
$vcvars = Join-Path $vsPath "VC\Auxiliary\Build\vcvars64.bat"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $vcvars -or -not (Test-Path $vcvars)) {
|
||||
$vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $vcvars)) {
|
||||
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||
if (Test-Path $vswhere) {
|
||||
$vsPath = (& $vswhere -latest -prerelease -products * -property installationPath | Select-Object -First 1)
|
||||
if ($vsPath) {
|
||||
$vcvars = Join-Path $vsPath "VC\Auxiliary\Build\vcvars64.bat"
|
||||
}
|
||||
}
|
||||
$vcvars = "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $vcvars)) {
|
||||
|
||||
Reference in New Issue
Block a user