| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- 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<HTMLDivElement>;
- canvasRef: React.RefObject<HTMLCanvasElement>;
- onSelectionComplete: (screenshot: Blob, text: string) => void;
- onCancel: () => void;
- pdfBlob: Blob | null;
- pageNumber: number;
- authToken: string;
- zoomLevel?: number; // オプションのズームレベルパラメータ
- }
- export const PDFSelectionTool: React.FC<PDFSelectionToolProps> = ({
- 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<HTMLCanvasElement>(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<Blob>((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<string> => {
- 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 (
- <div
- className="absolute inset-0 z-50 cursor-crosshair bg-white/20"
- onMouseDown={handleMouseDown}
- onMouseMove={handleMouseMove}
- onMouseUp={handleMouseUp}
- >
- <canvas
- ref={overlayCanvasRef}
- className="absolute inset-0 pointer-events-none"
- />
- <div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-75 text-white px-4 py-2 rounded-lg text-sm z-[60]">
- {t('dragToSelect')}
- </div>
- </div>
- );
- };
|