import React, { useState, useRef, useEffect } from 'react'; import * as pdfjs from 'pdfjs-dist'; import { useLanguage } from '../contexts/LanguageContext'; // Set worker path for PDF.js pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.mjs`; // Helper function to convert coordinates between different scales const convertCoordinates = ( x: number, y: number, containerWidth: number, containerHeight: number, pdfWidth: number, pdfHeight: number, zoomLevel: number = 1.0 ) => { // Calculate the scale factors to fit the PDF page in the container while maintaining aspect ratio const scaleX = containerWidth / pdfWidth; const scaleY = containerHeight / pdfHeight; let scale = Math.min(scaleX, scaleY); // Apply zoom level to the scale scale *= zoomLevel; // Calculate padding offsets to center the PDF page in the container const paddedWidth = pdfWidth * scale; const paddedHeight = pdfHeight * scale; const offsetX = (containerWidth - paddedWidth) / 2; const offsetY = (containerHeight - paddedHeight) / 2; // Convert from container coordinates to PDF page coordinates const pdfX = (x - offsetX) / scale; const pdfY = (y - offsetY) / scale; return { x: pdfX, y: pdfY, scale, offsetX, offsetY }; }; // Function to calculate how PDF page is laid out in container space const calculatePDFLayout = ( containerWidth: number, containerHeight: number, pdfWidth: number, pdfHeight: number, zoomLevel: number = 1.0 ) => { // Calculate the scale factors to fit the PDF page in the container while maintaining aspect ratio const scaleX = containerWidth / pdfWidth; const scaleY = containerHeight / pdfHeight; let pageScale = Math.min(scaleX, scaleY); // Apply zoom level to the page scale pageScale *= zoomLevel; // Calculate padding offsets to center the PDF page in the container const paddedWidth = pdfWidth * pageScale; const paddedHeight = pdfHeight * pageScale; const offsetX = (containerWidth - paddedWidth) / 2; const offsetY = (containerHeight - paddedHeight) / 2; return { pageScale, offsetX: Math.round(offsetX), offsetY: Math.round(offsetY), paddedWidth, paddedHeight }; }; // Enhanced function to calculate precise PDF page layout with improved accuracy const calculatePrecisePDFLayout = ( containerWidth: number, containerHeight: number, pdfWidth: number, pdfHeight: number, zoomLevel: number = 1.0 ) => { // Calculate scale to fit the PDF page in the container while maintaining aspect ratio const scaleX = containerWidth / pdfWidth; const scaleY = containerHeight / pdfHeight; let pageScale = Math.min(scaleX, scaleY); // Apply zoom level to the page scale pageScale *= zoomLevel; // Calculate exact page dimensions after scaling const scaledPageWidth = pdfWidth * pageScale; const scaledPageHeight = pdfHeight * pageScale; // Calculate padding to center the page in the container const offsetX = (containerWidth - scaledPageWidth) / 2; const offsetY = (containerHeight - scaledPageHeight) / 2; return { pageScale, offsetX: offsetX, offsetY: offsetY, scaledPageWidth, scaledPageHeight, containerWidth, containerHeight }; }; export interface SelectionCoordinates { x: number; y: number; width: number; height: number; } interface PDFSelectionToolProps { containerRef: React.RefObject; canvasRef: React.RefObject; onSelectionComplete: (screenshot: Blob, text: string) => void; onCancel: () => void; pdfBlob: Blob | null; pageNumber: number; authToken: string; zoomLevel?: number; // オプションのズームレベルパラメータ } export const PDFSelectionTool: React.FC = ({ containerRef, canvasRef, onSelectionComplete, onCancel, pdfBlob, pageNumber, authToken, zoomLevel = 1.0, // デフォルトのズームレベルは1.0 }) => { const { t } = useLanguage(); const [isSelecting, setIsSelecting] = useState(false); const [startPoint, setStartPoint] = useState<{ x: number; y: number } | null>(null); const [currentPoint, setCurrentPoint] = useState<{ x: number; y: number } | null>(null); const overlayCanvasRef = useRef(null); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { onCancel(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onCancel]); const handleMouseDown = (e: React.MouseEvent) => { if (!containerRef.current || !pdfBlob) return; const rect = containerRef.current.getBoundingClientRect(); // コンテナに対する実際の座標を使用 const x = e.clientX - rect.left; const y = e.clientY - rect.top; setStartPoint({ x, y }); setCurrentPoint({ x, y }); setIsSelecting(true); }; const handleMouseMove = (e: React.MouseEvent) => { if (!isSelecting || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; setCurrentPoint({ x, y }); }; const handleMouseUp = async () => { if (!isSelecting || !startPoint || !currentPoint || !containerRef.current || !canvasRef.current) return; setIsSelecting(false); // Calculate selection rectangle based on mouse events (in container coordinates) const startX = Math.min(startPoint.x, currentPoint.x); const startY = Math.min(startPoint.y, currentPoint.y); const endX = Math.max(startPoint.x, currentPoint.x); const endY = Math.max(startPoint.y, currentPoint.y); const width = endX - startX; const height = endY - startY; // Minimum selection size if (width < 10 || height < 10) { onCancel(); return; } try { // Get the actual canvas element from PDFPreview const sourceCanvas = canvasRef.current; const containerRect = containerRef.current.getBoundingClientRect(); console.log('=== Direct Canvas Capture ==='); console.log('Container dimensions:', { width: containerRect.width, height: containerRect.height }); console.log('Canvas dimensions:', { width: sourceCanvas.width, height: sourceCanvas.height }); console.log('Selection in container space:', { startX, startY, endX, endY, width, height }); // Get the canvas bounding rect to find where it's positioned within the container const canvasRect = sourceCanvas.getBoundingClientRect(); const canvasOffsetX = canvasRect.left - containerRect.left; const canvasOffsetY = canvasRect.top - containerRect.top; console.log('Canvas position in container:', { offsetX: canvasOffsetX, offsetY: canvasOffsetY }); console.log('Canvas display size:', { width: canvasRect.width, height: canvasRect.height }); // Calculate selection relative to the canvas element const selectionRelativeToCanvas = { x: startX - canvasOffsetX, y: startY - canvasOffsetY, width: width, height: height }; console.log('Selection relative to canvas:', selectionRelativeToCanvas); // Calculate the scale factor between canvas display size and actual pixel size // The canvas may be rendered at higher resolution (devicePixelRatio) const scaleX = sourceCanvas.width / canvasRect.width; const scaleY = sourceCanvas.height / canvasRect.height; console.log('Canvas scale factors:', { scaleX, scaleY }); // Convert selection coordinates to canvas pixel coordinates const canvasX = Math.round(selectionRelativeToCanvas.x * scaleX); const canvasY = Math.round(selectionRelativeToCanvas.y * scaleY); const canvasWidth = Math.round(selectionRelativeToCanvas.width * scaleX); const canvasHeight = Math.round(selectionRelativeToCanvas.height * scaleY); console.log('Selection in canvas pixel space:', { canvasX, canvasY, canvasWidth, canvasHeight }); // Ensure coordinates are within canvas bounds const safeX = Math.max(0, Math.min(canvasX, sourceCanvas.width)); const safeY = Math.max(0, Math.min(canvasY, sourceCanvas.height)); const safeWidth = Math.max(0, Math.min(canvasWidth, sourceCanvas.width - safeX)); const safeHeight = Math.max(0, Math.min(canvasHeight, sourceCanvas.height - safeY)); console.log('Safe coordinates:', { safeX, safeY, safeWidth, safeHeight }); if (safeWidth === 0 || safeHeight === 0) { console.warn('Selection is outside canvas bounds'); onCancel(); return; } // Extract the selected region from the source canvas const ctx = sourceCanvas.getContext('2d'); if (!ctx) { throw new Error('Could not get canvas context'); } const imageData = ctx.getImageData(safeX, safeY, safeWidth, safeHeight); // Create a new canvas for the selected region const selectedCanvas = document.createElement('canvas'); selectedCanvas.width = safeWidth; selectedCanvas.height = safeHeight; const selectedCtx = selectedCanvas.getContext('2d'); if (!selectedCtx) { throw new Error('Could not create selection canvas context'); } selectedCtx.putImageData(imageData, 0, 0); // Convert selected canvas to blob const screenshot = await new Promise((resolve, reject) => { selectedCanvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject(new Error('Failed to create blob from canvas')); } }, 'image/jpeg', 0.98); }); console.log('Screenshot created successfully'); // Extract text from the selected area using OCR let extractedText = ''; try { extractedText = await performOCR(screenshot, authToken); } catch (ocrError) { console.error('OCR extraction failed:', ocrError); } onSelectionComplete(screenshot, extractedText); } catch (error) { console.error('Failed to process selection:', error); onCancel(); } }; // Render PDF to canvas at specified scale const renderPDFToCanvas = async ( pdfBlob: Blob, pageNumber: number, canvas: HTMLCanvasElement, containerWidth: number, containerHeight: number, renderScale: number, zoomLevel: number = 1.0 ): Promise<{ offsetX: number; offsetY: number; pageScale: number; viewport: any }> => { const pdfData = await pdfBlob.arrayBuffer(); const pdf = await pdfjs.getDocument({ data: pdfData }).promise; if (pageNumber < 1 || pageNumber > pdf.numPages) { throw new Error(`Invalid page number: ${pageNumber}`); } const page = await pdf.getPage(pageNumber); // Calculate the scale needed to render the PDF page to match the layout in container // We want the same aspect ratio and positioning as in the PDF viewer const originalViewport = page.getViewport({ scale: 1 }); // Calculate scale factors to fit page within container while preserving aspect ratio const scaleX = containerWidth / originalViewport.width; const scaleY = containerHeight / originalViewport.height; let pageScale = Math.min(scaleX, scaleY); // Apply zoom level to the page scale pageScale *= zoomLevel; // Apply the render scale factor for high resolution const finalScale = pageScale * renderScale; // Create the viewport at this scale const viewport = page.getViewport({ scale: finalScale }); const context = canvas.getContext('2d'); if (!context) { // Return default values if context not available return { offsetX: 0, offsetY: 0, pageScale: 1, viewport: originalViewport }; } // Calculate offset to center the page in the canvas const offsetX = Math.round((canvas.width - viewport.width) / 2); const offsetY = Math.round((canvas.height - viewport.height) / 2); // Render the page with anti-aliasing and smooth rendering for quality const renderContext = { canvasContext: context, viewport: viewport, transform: [1, 0, 0, 1, offsetX, offsetY], intent: 'display' as const }; // Render the page with improved rendering quality await page.render(renderContext).promise; return { offsetX, offsetY, pageScale, viewport }; }; // Perform OCR on the captured image const performOCR = async (image: Blob, token: string): Promise => { const formData = new FormData(); formData.append('image', image); const response = await fetch('/api/ocr/recognize', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData, }); if (!response.ok) { throw new Error('Failed to recognize text via OCR'); } const data = await response.json(); return data.text; }; useEffect(() => { if (!overlayCanvasRef.current || !startPoint || !currentPoint || !containerRef.current) return; const canvas = overlayCanvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; // Match the canvas dimensions to the container const containerRect = containerRef.current.getBoundingClientRect(); canvas.width = containerRect.width; canvas.height = containerRect.height; // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw selection rectangle const x = Math.min(startPoint.x, currentPoint.x); const y = Math.min(startPoint.y, currentPoint.y); const width = Math.abs(currentPoint.x - startPoint.x); const height = Math.abs(currentPoint.y - startPoint.y); // Draw semi-transparent overlay ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Clear selection area ctx.clearRect(x, y, width, height); // Draw selection border ctx.strokeStyle = '#3b82f6'; ctx.lineWidth = 2; ctx.strokeRect(x, y, width, height); // Draw corner handles const handleSize = 8; ctx.fillStyle = '#3b82f6'; ctx.fillRect(x - handleSize / 2, y - handleSize / 2, handleSize, handleSize); ctx.fillRect(x + width - handleSize / 2, y - handleSize / 2, handleSize, handleSize); ctx.fillRect(x - handleSize / 2, y + height - handleSize / 2, handleSize, handleSize); ctx.fillRect(x + width - handleSize / 2, y + height - handleSize / 2, handleSize, handleSize); }, [startPoint, currentPoint, containerRef]); return (
{t('dragToSelect')}
); };