diff --git a/angular.json b/angular.json index 51f355f..71b10c4 100644 --- a/angular.json +++ b/angular.json @@ -104,5 +104,8 @@ } } } + }, + "cli": { + "analytics": false } } diff --git a/src/app/deck-list.component.html b/src/app/deck-list.component.html index 28dd7f0..294a86d 100644 --- a/src/app/deck-list.component.html +++ b/src/app/deck-list.component.html @@ -46,7 +46,7 @@
-
diff --git a/src/app/deck-list.component.ts b/src/app/deck-list.component.ts index 60caf55..60761d2 100644 --- a/src/app/deck-list.component.ts +++ b/src/app/deck-list.component.ts @@ -5,6 +5,8 @@ import { CommonModule } from '@angular/common'; import { CreateDeckModalComponent } from './create-deck-modal/create-deck-modal.component'; import { TrainingComponent } from './training/training.component'; import { UploadImageModalComponent } from './upload-image-modal/upload-image-modal.component'; +import { EditImageModalComponent } from './edit-image-modal/edit-image-modal.component'; +import { firstValueFrom } from 'rxjs'; @Component({ selector: 'app-deck-list', @@ -14,7 +16,9 @@ import { UploadImageModalComponent } from './upload-image-modal/upload-image-mod CommonModule, CreateDeckModalComponent, UploadImageModalComponent, - TrainingComponent + TrainingComponent, + EditImageModalComponent, + UploadImageModalComponent ] }) export class DeckListComponent implements OnInit { @@ -23,6 +27,10 @@ export class DeckListComponent implements OnInit { @ViewChild(CreateDeckModalComponent) createDeckModal!: CreateDeckModalComponent; @ViewChild(UploadImageModalComponent) uploadImageModal!: UploadImageModalComponent; + @ViewChild(EditImageModalComponent) editModal!: EditImageModalComponent; + @ViewChild(UploadImageModalComponent) uploadModal!: UploadImageModalComponent; + + imageData: { imageSrc: string | ArrayBuffer | null, deckImage:DeckImage} | null = null; currentUploadDeckName: string = ''; @@ -65,7 +73,27 @@ export class DeckListComponent implements OnInit { error: (err) => console.error('Fehler beim Löschen des Bildes', err) }); } - + editImage(deck: Deck, image: DeckImage): void { + let imageSrc = null + fetch(`/api/debug_image/${image.id}/original_compressed.jpg`) + .then(response => { + if (!response.ok) { + throw new Error('Netzwerkantwort war nicht ok'); + } + return response.blob(); + }) + .then(blob => { + const reader = new FileReader(); + reader.onloadend = () => { + imageSrc = reader.result; // Base64-String + this.imageData = {imageSrc,deckImage:image} + }; + reader.readAsDataURL(blob); + }) + .catch(error => { + console.error('Fehler beim Laden des Bildes:', error); + }); + } openTraining(deck: Deck): void { this.selectedDeck = deck; } @@ -131,4 +159,22 @@ export class DeckListComponent implements OnInit { const expandedArray = Array.from(this.expandedDecks); sessionStorage.setItem('expandedDecks', JSON.stringify(expandedArray)); } + + // Funktion zum Öffnen des Upload Modals (kann durch einen Button ausgelöst werden) + openUploadModal(): void { + this.uploadImageModal.open(); + } + + // Handler für das imageUploaded Event + onImageUploaded(imageData: any): void { + this.imageData = imageData; + } + onClosed(){ + this.imageData = null; + } + async onImageSaved() { + // Handle das Speichern der Bilddaten, z.B. aktualisiere die Liste der Bilder + this.imageData = null; + this.decks = await firstValueFrom(this.deckService.getDecks()) + } } diff --git a/src/app/deck.service.ts b/src/app/deck.service.ts index 26796ae..2d7911c 100644 --- a/src/app/deck.service.ts +++ b/src/app/deck.service.ts @@ -14,6 +14,7 @@ export interface DeckImage { name: string; id:string; } + export interface Box { x1:number; x2:number; @@ -31,6 +32,21 @@ export interface BackendBox { y1: number; y2: number; } + +// Definiert ein einzelnes Punktpaar [x, y] +type OcrPoint = [number, number]; + +// Definiert die Box als Array von vier Punkten +type OcrBox = [OcrPoint, OcrPoint, OcrPoint, OcrPoint]; + +// Interface für jedes JSON-Objekt +export interface OcrResult { + box: OcrBox; + confidence: number; + name: string; + text: string; +} + @Injectable({ providedIn: 'root' }) diff --git a/src/app/edit-image-modal/edit-image-modal.component.html b/src/app/edit-image-modal/edit-image-modal.component.html new file mode 100644 index 0000000..b8e7901 --- /dev/null +++ b/src/app/edit-image-modal/edit-image-modal.component.html @@ -0,0 +1,30 @@ + + diff --git a/src/app/edit-image-modal/edit-image-modal.component.ts b/src/app/edit-image-modal/edit-image-modal.component.ts new file mode 100644 index 0000000..fa0dabe --- /dev/null +++ b/src/app/edit-image-modal/edit-image-modal.component.ts @@ -0,0 +1,227 @@ +// src/app/edit-image-modal.component.ts +import { Component, Input, Output, EventEmitter, AfterViewInit, ViewChild, ElementRef, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { fabric } from 'fabric'; +import { Modal } from 'flowbite'; +import { DeckImage, DeckService, OcrResult } from '../deck.service'; + +@Component({ + selector: 'app-edit-image-modal', + templateUrl: './edit-image-modal.component.html', + standalone: true, + imports: [CommonModule] +}) +export class EditImageModalComponent implements AfterViewInit, OnDestroy { + @Input() deckName: string = ''; + @Input() imageData : {imageSrc:string|ArrayBuffer|null, deckImage:DeckImage|null} = {imageSrc:null,deckImage:null}; + @Output() imageSaved = new EventEmitter(); + @Output() closed = new EventEmitter(); + @ViewChild('editImageModal') modalElement!: ElementRef; + @ViewChild('canvas') canvasElement!: ElementRef; + + detectedText: string = ''; + boxes: { x1: number; x2: number; y1: number; y2: number }[] = []; + canvas!: fabric.Canvas; + + maxCanvasWidth: number = 0; + maxCanvasHeight: number = 0; + + private keyDownHandler!: (e: KeyboardEvent) => void; + modal: any; + + constructor(private deckService: DeckService) { } + + async ngAfterViewInit() { + this.modal = new Modal(this.modalElement.nativeElement,{ + onHide: () => { + this.closed.emit(); + }}, + ); + + this.maxCanvasWidth = window.innerWidth * 0.6; + this.maxCanvasHeight = window.innerHeight * 0.6; + + this.keyDownHandler = this.onKeyDown.bind(this); + document.addEventListener('keydown', this.keyDownHandler); + + await this.initializeCanvas(); + + this.modal.show(); + } + + ngOnDestroy(): void { + document.removeEventListener('keydown', this.keyDownHandler); + if (this.canvas) { + this.canvas.dispose(); + } + } + + open(): void { + this.modal.show(); + } + + closeModal(): void { + this.modal.hide(); + } + + async initializeCanvas() { + await this.processImage(); + } + + 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(): Promise { + try { + if (!this.imageData){ + return + } + + this.canvas = new fabric.Canvas(this.canvasElement.nativeElement); + + // Hintergrundbild setzen + const backgroundImage = await this.loadFabricImage(this.imageData.imageSrc as string); + + const originalWidth = backgroundImage.width!; + const originalHeight = backgroundImage.height!; + + const scaleX = this.maxCanvasWidth / originalWidth; + const scaleY = this.maxCanvasHeight / originalHeight; + const scaleFactor = Math.min(scaleX, scaleY, 1); + + const canvasWidth = originalWidth * scaleFactor; + const canvasHeight = originalHeight * scaleFactor; + + this.canvas.setWidth(canvasWidth); + this.canvas.setHeight(canvasHeight); + + backgroundImage.set({ + scaleX: scaleFactor, + scaleY: scaleFactor, + }); + + this.canvas.setBackgroundImage(backgroundImage, this.canvas.renderAll.bind(this.canvas)); + + this.boxes = []; + + // Boxen hinzufügen + this.imageData.deckImage?.boxes.forEach(box => { + + const rect = new fabric.Rect({ + left: box.x1 * scaleFactor, + top: box.y1 * scaleFactor, + width: (box.x2-box.x1) * scaleFactor, + height: (box.y2-box.y1) * scaleFactor, + fill: 'rgba(255, 0, 0, 0.3)', + selectable: true, + hasControls: true, + hasBorders: true, + objectCaching: false, + }); + + 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); + }); + + this.updateBoxCoordinates(); + + // this.detectedText = ocrResults.map(result => result.text).join('\n'); + + } catch (error) { + console.error('Fehler bei der Bildverarbeitung:', error); + } + } + + 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(); + } + } + } + + updateBoxCoordinates(): void { + this.boxes = []; + + let scaleFactor = 1; + const bgImage = this.canvas.backgroundImage; + if (bgImage && bgImage instanceof fabric.Image) { + scaleFactor = bgImage.get('scaleX') || 1; + } + + this.canvas.getObjects('rect').forEach((rect: fabric.Rect) => { + const left = rect.left!; + const top = rect.top!; + const width = rect.width! * rect.scaleX!; + const height = rect.height! * rect.scaleY!; + + const x1 = left / scaleFactor; + const y1 = top / scaleFactor; + const x2 = (left + width) / scaleFactor; + const y2 = (top + height) / scaleFactor; + + this.boxes.push({ + x1: Math.round(x1), + x2: Math.round(x2), + y1: Math.round(y1), + y2: Math.round(y2) + }); + }); + + this.canvas.requestRenderAll(); + } + + save(): void { + // Hier implementierst du die Logik zum Speichern der Bilddaten + // Zum Beispiel über einen Service oder direkt hier + const data = { + deckname: this.deckName, + bildname: this.imageData.deckImage?.name,//this.imageFile?.name, + bildid: this.imageData.deckImage?.id, + boxes: this.boxes, + }; + this.deckService.saveImageData(data).subscribe({ + next: () => { + this.imageSaved.emit(); + this.closeModal(); + }, + error: (err) => { + console.error('Fehler beim Speichern des Bildes:', err); + alert('Fehler beim Speichern des Bildes.'); + this.closeModal(); + } + }); + + } +} diff --git a/src/app/upload-image-modal/upload-image-modal.component.html b/src/app/upload-image-modal/upload-image-modal.component.html index 6484778..713cd2e 100644 --- a/src/app/upload-image-modal/upload-image-modal.component.html +++ b/src/app/upload-image-modal/upload-image-modal.component.html @@ -11,24 +11,14 @@

Bild zu Deck hinzufügen

- -
-
- - -
-
- - -
- - + +
+ +
-
+

{{ processingStatus }}

@@ -42,4 +32,3 @@
- diff --git a/src/app/upload-image-modal/upload-image-modal.component.ts b/src/app/upload-image-modal/upload-image-modal.component.ts index 0acbc22..fa0e094 100644 --- a/src/app/upload-image-modal/upload-image-modal.component.ts +++ b/src/app/upload-image-modal/upload-image-modal.component.ts @@ -1,9 +1,8 @@ // 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 { Box, DeckImage, DeckService, OcrResult } from '../deck.service'; import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { fabric } from 'fabric'; import { Modal } from 'flowbite'; @Component({ @@ -14,57 +13,23 @@ import { Modal } from 'flowbite'; }) export class UploadImageModalComponent implements AfterViewInit, OnDestroy { @Input() deckName: string = ''; - @Output() imageUploaded = new EventEmitter(); + @Output() imageUploaded = new EventEmitter<{ imageSrc: string | ArrayBuffer | null | undefined, deckImage:DeckImage }>(); @ViewChild('uploadImageModal') modalElement!: ElementRef; - @ViewChild('canvas') canvasElement!: ElementRef; imageFile: File | null = null; processingStatus: string = ''; loading: boolean = false; + modal: any; - // Fabric.js Related Variables - originalImageSrc: string | ArrayBuffer | undefined | null = null; - detectedText: string = ''; - boxes: { x1: number; x2: number; y1: number; y2: number }[] = []; - canvas!: fabric.Canvas; - - // Maximal erlaubte Größe - maxCanvasWidth: number = 0; - maxCanvasHeight: number = 0; - - // Referenz zum Keydown-Eventhandler - private keyDownHandler!: (e: KeyboardEvent) => void; - - // State Management - formVisible: boolean = true; - canvasVisible: boolean = false; - modal:any; - originalImageWidth:number|undefined; - originalImageHeight:number|undefined; - imagename:string|undefined|null; constructor(private deckService: DeckService, private http: HttpClient) { } ngAfterViewInit(): void { - // Initialisiere die Flowbite Modal this.modal = new Modal(this.modalElement.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(); - } + // Modal wird automatisch von Flowbite verwaltet } open(): void { @@ -79,53 +44,24 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy { resetState(): void { this.imageFile = null; this.processingStatus = ''; - this.detectedText = ''; - this.boxes = []; - this.originalImageSrc = null; - this.canvasVisible = false; - this.formVisible = true; - - // Clear Fabric canvas if it exists - if (this.canvas) { - this.canvas.clear(); - this.canvas.dispose(); - this.canvas = undefined as any; - } + this.loading = false; } - 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 { + onFileChange(event: any): void { const file: File = event.target.files[0]; if (!file) return; this.imageFile = file; 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; + const imageSrc = e.target?.result; // Bild als Base64-String ohne Präfix (data:image/...) - const imageBase64 = (this.originalImageSrc as string).split(',')[1]; + const imageBase64 = (imageSrc as string).split(',')[1]; try { - // Eingabefelder ausblenden und Canvas anzeigen - this.formVisible = false; - this.canvasVisible = true; - - // Anfrage an den Backend-Service senden const response = await this.http.post('/api/ocr', { image: imageBase64 }).toPromise(); if (!response || !response.results) { @@ -134,204 +70,35 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy { return; } - // Bildverarbeitung im Frontend durchführen - this.imagename = (response.results && response.results.length>0)?response.results[0].name:null; - await this.processImage(response.results); + this.processingStatus = 'Verarbeitung abgeschlossen'; + this.loading = false; + + // Emit Event mit Bilddaten und OCR-Ergebnissen + const bildname=this.imageFile?.name??''; + const bildid=response.results.length>0?response.results[0].name:null + const boxes:Box[] = []; + response.results.forEach((result: OcrResult) => { + const box = result.box; + + 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); + boxes.push({x1:xMin,x2:xMax,y1:yMin,y2:yMax}) + }); + const deckImage:DeckImage={name:bildname,id:bildid,boxes} + this.imageUploaded.emit({ imageSrc, deckImage }); + + // Schließe das Upload-Modal + this.closeModal(); } catch (error) { console.error('Fehler beim OCR-Service:', error); this.processingStatus = 'Fehler beim OCR-Service'; this.loading = false; - // Eingabefelder ausblenden und Canvas anzeigen - this.formVisible = true; - this.canvasVisible = 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 - if (this.canvas) { - this.canvas.clear(); - this.canvas.dispose(); - } - - this.canvas = new fabric.Canvas(this.canvasElement.nativeElement); - - 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(255, 0, 0, 0.3)', - 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; - - } 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(); - } - - save(): void { - // Hier kannst du die Logik zum Speichern der Bilddaten implementieren - // Zum Beispiel: - const data = { - deckname: this.deckName, - bildname: this.imageFile?.name, - bildid: this.imagename, - boxes: this.boxes, - }; - this.deckService.saveImageData(data).subscribe({ - next: () => { - this.imageUploaded.emit(); - this.closeModal(); - }, - error: (err) => { - console.error('Fehler beim Speichern des Bildes:', err); - alert('Fehler beim Speichern des Bildes.'); - } - }); - // Temporäres Beispiel: - this.imageUploaded.emit(); - this.closeModal(); - } }