// src/app/upload-image-modal.component.ts import { Component, Input, Output, EventEmitter, AfterViewInit, ViewChild, ElementRef, OnDestroy } from '@angular/core'; import { DeckService } from '../deck.service'; import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { fabric } from 'fabric'; import { FormsModule } from '@angular/forms'; import { Modal } from 'flowbite'; @Component({ selector: 'app-upload-image-modal', templateUrl: './upload-image-modal.component.html', standalone: true, imports: [CommonModule, FormsModule] }) export class UploadImageModalComponent implements AfterViewInit, OnDestroy { @Input() deckName: string = ''; @Output() imageUploaded = new EventEmitter(); @ViewChild('uploadImageModal') modalElement!: ElementRef; @ViewChild('canvas') canvasElement!: ElementRef; imageFile: File | null = null; imageText: string = ''; modal!: any; // Typ kann je nach Flowbite-Version angepasst werden canvas!: fabric.Canvas; // Fabric.js Related Variables originalImageSrc: string | ArrayBuffer | undefined | null = null; detectedText: string = ''; processingStatus: string = ''; loading: boolean = false; // Maximal erlaubte Größe maxCanvasWidth: number = 0; maxCanvasHeight: number = 0; boxes: { x1: number; x2: number; y1: number; y2: number }[] = []; originalImageWidth: number = 0; originalImageHeight: number = 0; // Referenz zum Keydown-Eventhandler private keyDownHandler!: (e: KeyboardEvent) => void; constructor(private deckService: DeckService, private http: HttpClient) { } ngAfterViewInit(): void { // Initialisiere die Flowbite Modal this.modal = new Modal(this.modalElement.nativeElement); // Initialisiere Fabric.js Canvas this.canvas = new fabric.Canvas(this.canvasElement.nativeElement); // Berechne die maximal erlaubten Abmessungen basierend auf dem Viewport this.maxCanvasWidth = window.innerWidth * 0.6; // Passe nach Bedarf an this.maxCanvasHeight = window.innerHeight * 0.6; // Passe nach Bedarf an // Keydown-Eventlistener hinzufügen this.keyDownHandler = this.onKeyDown.bind(this); document.addEventListener('keydown', this.keyDownHandler); } ngOnDestroy(): void { // Keydown-Eventlistener entfernen document.removeEventListener('keydown', this.keyDownHandler); // Fabric.js Canvas zerstören if (this.canvas) { this.canvas.dispose(); } } open(): void { this.resetState(); this.modal.show(); } closeModal(): void { this.modal.hide(); } resetState(): void { this.imageFile = null; this.imageText = ''; this.originalImageSrc = null; this.detectedText = ''; this.processingStatus = ''; this.loading = false; this.boxes = []; this.originalImageWidth = 0; this.originalImageHeight = 0; this.canvas.clear(); } onKeyDown(e: KeyboardEvent): void { if (e.key === 'Delete' || e.key === 'Del') { const activeObject = this.canvas.getActiveObject(); if (activeObject) { this.canvas.remove(activeObject); this.canvas.requestRenderAll(); this.updateBoxCoordinates(); } } } async onFileChange(event: any): Promise { const file: File = event.target.files[0]; if (!file) return; // Status zurücksetzen this.processingStatus = 'Verarbeitung läuft...'; this.detectedText = ''; this.loading = true; // Bild als Base64 laden const reader = new FileReader(); reader.onload = async (e) => { this.originalImageSrc = e.target?.result; // Bild als Base64-String ohne Präfix (data:image/...) const imageBase64 = (this.originalImageSrc as string).split(',')[1]; try { // Anfrage an den Backend-Service senden const response = await this.http.post('/api/ocr', { image: imageBase64 }).toPromise(); if (!response || !response.results) { this.processingStatus = 'Ungültige Antwort vom OCR-Service'; this.loading = false; return; } // Bildverarbeitung im Frontend durchführen await this.processImage(response.results); } catch (error) { console.error('Fehler beim OCR-Service:', error); this.processingStatus = 'Fehler beim OCR-Service'; this.loading = false; } }; reader.readAsDataURL(file); } private loadFabricImage(url: string): Promise { return new Promise((resolve, reject) => { fabric.Image.fromURL( url, (img) => { resolve(img); }, { crossOrigin: 'anonymous', originX: 'left', originY: 'top', } ); }); } async processImage(ocrResults: any[]): Promise { // Canvas zurücksetzen this.canvas.clear(); this.boxes = []; // Hintergrundbild setzen try { const backgroundImage = await this.loadFabricImage(this.originalImageSrc as string); // Speichere die Originalbildgröße this.originalImageWidth = backgroundImage.width!; this.originalImageHeight = backgroundImage.height!; // Berechne Skalierungsfaktor basierend auf maximal erlaubter Größe const scaleX = this.maxCanvasWidth / backgroundImage.width!; const scaleY = this.maxCanvasHeight / backgroundImage.height!; const scaleFactor = Math.min(scaleX, scaleY, 1); // Vermeide Vergrößerung // Neue Größe des Canvas const canvasWidth = backgroundImage.width! * scaleFactor; const canvasHeight = backgroundImage.height! * scaleFactor; // Canvas-Größe anpassen this.canvas.setWidth(canvasWidth); this.canvas.setHeight(canvasHeight); // Hintergrundbild skalieren backgroundImage.set({ scaleX: scaleFactor, scaleY: scaleFactor, }); // Hintergrundbild setzen this.canvas.setBackgroundImage(backgroundImage, this.canvas.renderAll.bind(this.canvas)); // Boxen hinzufügen ocrResults.forEach(result => { const box = result.box; // Grenzen berechnen const xs = box.map((point: number[]) => point[0]); const ys = box.map((point: number[]) => point[1]); const xMin = Math.min(...xs); const xMax = Math.max(...xs); const yMin = Math.min(...ys); const yMax = Math.max(...ys); // Skalierung anwenden const left = xMin * scaleFactor; const top = yMin * scaleFactor; const width = (xMax - xMin) * scaleFactor; const height = (yMax - yMin) * scaleFactor; // Rechteck erstellen const rect = new fabric.Rect({ left: left, top: top, width: width, height: height, fill: 'rgba(0, 0, 0, 0.5)', selectable: true, hasControls: true, hasBorders: true, objectCaching: false, }); // Event-Listener hinzufügen rect.on('modified', () => { this.updateBoxCoordinates(); }); rect.on('moved', () => { this.updateBoxCoordinates(); }); rect.on('scaled', () => { this.updateBoxCoordinates(); }); rect.on('rotated', () => { this.updateBoxCoordinates(); }); rect.on('removed', () => { this.updateBoxCoordinates(); }); this.canvas.add(rect); }); // Initiale Box-Koordinaten aktualisieren this.updateBoxCoordinates(); // Erkannten Text anzeigen this.detectedText = ocrResults.map(result => result.text).join('\n'); this.processingStatus = 'Verarbeitung abgeschlossen'; this.loading = false; // Eingabefelder und Button ausblenden this.hideFormElements(); } catch (error) { console.error('Fehler beim Setzen des Hintergrundbildes:', error); this.processingStatus = 'Fehler bei der Bildverarbeitung'; this.loading = false; } } updateBoxCoordinates(): void { // Leere die aktuelle Box-Liste this.boxes = []; // Skalierungsfaktor ermitteln (sollte der gleiche sein wie zuvor) let scaleFactor = 1; const bgImage = this.canvas.backgroundImage; if (bgImage && bgImage instanceof fabric.Image) { scaleFactor = bgImage.get('scaleX') || 1; } // Alle Rechtecke durchgehen this.canvas.getObjects('rect').forEach((rect: fabric.Rect) => { // Aktuelle Position und Größe des Rechtecks const left = rect.left!; const top = rect.top!; const width = rect.width! * rect.scaleX!; const height = rect.height! * rect.scaleY!; // Umrechnung auf Originalbildgröße const x1 = left / scaleFactor; const y1 = top / scaleFactor; const x2 = (left + width) / scaleFactor; const y2 = (top + height) / scaleFactor; // Werte runden this.boxes.push({ x1: Math.round(x1), x2: Math.round(x2), y1: Math.round(y1), y2: Math.round(y2) }); }); // Trigger Angular Change Detection this.canvas.requestRenderAll(); } hideFormElements(): void { // Implementiere die Logik, um die Eingabefelder und den Button auszublenden // Dies kann durch eine zusätzliche Variable gesteuert werden this.formVisible = false; this.canvasVisible = true; } showFormElements(): void { this.formVisible = true; this.canvasVisible = false; } // Neue Variable zur Steuerung der Sichtbarkeit formVisible: boolean = true; canvasVisible: boolean = true; }