edit functionality + separation betw upload & edit
This commit is contained in:
parent
40abef241e
commit
92ec07fe75
|
|
@ -104,5 +104,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<!-- Edit Icon -->
|
<!-- Edit Icon -->
|
||||||
<button class="text-blue-500 hover:text-blue-700" title="Bild bearbeiten">
|
<button (click)="editImage(deck, image)" class="text-blue-500 hover:text-blue-700" title="Bild bearbeiten">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4H4v7m0 0l9-9 9 9M20 13v7h-7m0 0l-9-9-9 9" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4H4v7m0 0l9-9 9 9M20 13v7h-7m0 0l-9-9-9 9" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -78,8 +78,9 @@
|
||||||
<app-create-deck-modal (deckCreated)="loadDecks()"></app-create-deck-modal>
|
<app-create-deck-modal (deckCreated)="loadDecks()"></app-create-deck-modal>
|
||||||
|
|
||||||
<!-- UploadImageModalComponent -->
|
<!-- UploadImageModalComponent -->
|
||||||
<app-upload-image-modal [deckName]="currentUploadDeckName" (imageUploaded)="loadDecks()"></app-upload-image-modal>
|
<!-- <app-upload-image-modal [deckName]="currentUploadDeckName" (imageUploaded)="loadDecks()"></app-upload-image-modal> -->
|
||||||
|
<app-upload-image-modal (imageUploaded)="onImageUploaded($event)"> </app-upload-image-modal>
|
||||||
|
<app-edit-image-modal *ngIf="imageData" [deckName]="currentUploadDeckName" [imageData]="imageData" (imageSaved)="onImageSaved()" (closed)="onClosed()"></app-edit-image-modal>
|
||||||
<!-- TrainingComponent -->
|
<!-- TrainingComponent -->
|
||||||
<app-training *ngIf="selectedDeck" [deck]="selectedDeck" (close)="closeTraining()"></app-training>
|
<app-training *ngIf="selectedDeck" [deck]="selectedDeck" (close)="closeTraining()"></app-training>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { CommonModule } from '@angular/common';
|
||||||
import { CreateDeckModalComponent } from './create-deck-modal/create-deck-modal.component';
|
import { CreateDeckModalComponent } from './create-deck-modal/create-deck-modal.component';
|
||||||
import { TrainingComponent } from './training/training.component';
|
import { TrainingComponent } from './training/training.component';
|
||||||
import { UploadImageModalComponent } from './upload-image-modal/upload-image-modal.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({
|
@Component({
|
||||||
selector: 'app-deck-list',
|
selector: 'app-deck-list',
|
||||||
|
|
@ -14,7 +16,9 @@ import { UploadImageModalComponent } from './upload-image-modal/upload-image-mod
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CreateDeckModalComponent,
|
CreateDeckModalComponent,
|
||||||
UploadImageModalComponent,
|
UploadImageModalComponent,
|
||||||
TrainingComponent
|
TrainingComponent,
|
||||||
|
EditImageModalComponent,
|
||||||
|
UploadImageModalComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class DeckListComponent implements OnInit {
|
export class DeckListComponent implements OnInit {
|
||||||
|
|
@ -23,6 +27,10 @@ export class DeckListComponent implements OnInit {
|
||||||
|
|
||||||
@ViewChild(CreateDeckModalComponent) createDeckModal!: CreateDeckModalComponent;
|
@ViewChild(CreateDeckModalComponent) createDeckModal!: CreateDeckModalComponent;
|
||||||
@ViewChild(UploadImageModalComponent) uploadImageModal!: UploadImageModalComponent;
|
@ViewChild(UploadImageModalComponent) uploadImageModal!: UploadImageModalComponent;
|
||||||
|
@ViewChild(EditImageModalComponent) editModal!: EditImageModalComponent;
|
||||||
|
@ViewChild(UploadImageModalComponent) uploadModal!: UploadImageModalComponent;
|
||||||
|
|
||||||
|
imageData: { imageSrc: string | ArrayBuffer | null, deckImage:DeckImage} | null = null;
|
||||||
|
|
||||||
currentUploadDeckName: string = '';
|
currentUploadDeckName: string = '';
|
||||||
|
|
||||||
|
|
@ -65,7 +73,27 @@ export class DeckListComponent implements OnInit {
|
||||||
error: (err) => console.error('Fehler beim Löschen des Bildes', err)
|
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 {
|
openTraining(deck: Deck): void {
|
||||||
this.selectedDeck = deck;
|
this.selectedDeck = deck;
|
||||||
}
|
}
|
||||||
|
|
@ -131,4 +159,22 @@ export class DeckListComponent implements OnInit {
|
||||||
const expandedArray = Array.from(this.expandedDecks);
|
const expandedArray = Array.from(this.expandedDecks);
|
||||||
sessionStorage.setItem('expandedDecks', JSON.stringify(expandedArray));
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export interface DeckImage {
|
||||||
name: string;
|
name: string;
|
||||||
id:string;
|
id:string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Box {
|
export interface Box {
|
||||||
x1:number;
|
x1:number;
|
||||||
x2:number;
|
x2:number;
|
||||||
|
|
@ -31,6 +32,21 @@ export interface BackendBox {
|
||||||
y1: number;
|
y1: number;
|
||||||
y2: 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({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!-- src/app/edit-image-modal.component.html -->
|
||||||
|
<div #editImageModal id="editImageModal" tabindex="-1" aria-hidden="true" class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full">
|
||||||
|
<div class="relative h-full contents">
|
||||||
|
<div class="relative bg-white rounded-lg shadow">
|
||||||
|
<button type="button" class="absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" (click)="closeModal()">
|
||||||
|
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Schließen</span>
|
||||||
|
</button>
|
||||||
|
<div class="p-6 relative">
|
||||||
|
<h3 class="mb-4 text-xl font-medium text-gray-900">Bild bearbeiten</h3>
|
||||||
|
|
||||||
|
<!-- Canvas und Save Button -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<canvas #canvas class="border border-gray-300 rounded w-full h-auto"></canvas>
|
||||||
|
<button (click)="save()" class="mt-4 bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Erkannten Text anzeigen -->
|
||||||
|
<!-- <div *ngIf="detectedText" class="mt-4">
|
||||||
|
<h4 class="text-lg font-medium text-gray-700">Erkannter Text:</h4>
|
||||||
|
<pre class="whitespace-pre-wrap text-sm text-gray-600">{{ detectedText }}</pre>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -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<void>();
|
||||||
|
@Output() closed = new EventEmitter<void>();
|
||||||
|
@ViewChild('editImageModal') modalElement!: ElementRef;
|
||||||
|
@ViewChild('canvas') canvasElement!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
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<fabric.Image> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fabric.Image.fromURL(
|
||||||
|
url,
|
||||||
|
(img) => {
|
||||||
|
resolve(img);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
originX: 'left',
|
||||||
|
originY: 'top',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async processImage(): Promise<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,24 +11,14 @@
|
||||||
<div class="p-6 relative">
|
<div class="p-6 relative">
|
||||||
<h3 class="mb-4 text-xl font-medium text-gray-900">Bild zu Deck hinzufügen</h3>
|
<h3 class="mb-4 text-xl font-medium text-gray-900">Bild zu Deck hinzufügen</h3>
|
||||||
|
|
||||||
<!-- Formular sichtbar, solange formVisible true ist -->
|
<!-- Formular zum Hochladen -->
|
||||||
<div *ngIf="formVisible">
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="imageFile" class="block text-sm font-medium text-gray-700">Bild hochladen</label>
|
<label for="imageFile" class="block text-sm font-medium text-gray-700">Bild hochladen</label>
|
||||||
<input type="file" id="imageFile" (change)="onFileChange($event)" accept="image/*" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
<input type="file" id="imageFile" (change)="onFileChange($event)" accept="image/*" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Canvas und Save Button sichtbar, sobald canvasVisible true ist -->
|
|
||||||
<div *ngIf="canvasVisible" class="mt-4">
|
|
||||||
<canvas #canvas class="border border-gray-300 rounded w-full h-auto"></canvas>
|
|
||||||
<button (click)="save()" class="mt-4 bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Statusanzeige -->
|
<!-- Statusanzeige -->
|
||||||
<div *ngIf="processingStatus && !canvasVisible" class="mt-4">
|
<div *ngIf="processingStatus" class="mt-4">
|
||||||
<p class="text-sm text-gray-700">{{ processingStatus }}</p>
|
<p class="text-sm text-gray-700">{{ processingStatus }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -42,4 +32,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
// src/app/upload-image-modal.component.ts
|
// src/app/upload-image-modal.component.ts
|
||||||
import { Component, Input, Output, EventEmitter, AfterViewInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
|
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 { CommonModule } from '@angular/common';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { fabric } from 'fabric';
|
|
||||||
import { Modal } from 'flowbite';
|
import { Modal } from 'flowbite';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
@ -14,57 +13,23 @@ import { Modal } from 'flowbite';
|
||||||
})
|
})
|
||||||
export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
|
export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
|
||||||
@Input() deckName: string = '';
|
@Input() deckName: string = '';
|
||||||
@Output() imageUploaded = new EventEmitter<void>();
|
@Output() imageUploaded = new EventEmitter<{ imageSrc: string | ArrayBuffer | null | undefined, deckImage:DeckImage }>();
|
||||||
|
|
||||||
@ViewChild('uploadImageModal') modalElement!: ElementRef;
|
@ViewChild('uploadImageModal') modalElement!: ElementRef;
|
||||||
@ViewChild('canvas') canvasElement!: ElementRef<HTMLCanvasElement>;
|
|
||||||
|
|
||||||
imageFile: File | null = null;
|
imageFile: File | null = null;
|
||||||
processingStatus: string = '';
|
processingStatus: string = '';
|
||||||
loading: boolean = false;
|
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) { }
|
constructor(private deckService: DeckService, private http: HttpClient) { }
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
// Initialisiere die Flowbite Modal
|
|
||||||
this.modal = new Modal(this.modalElement.nativeElement);
|
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 {
|
ngOnDestroy(): void {
|
||||||
// Keydown-Eventlistener entfernen
|
// Modal wird automatisch von Flowbite verwaltet
|
||||||
document.removeEventListener('keydown', this.keyDownHandler);
|
|
||||||
// Fabric.js Canvas zerstören
|
|
||||||
if (this.canvas) {
|
|
||||||
this.canvas.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open(): void {
|
open(): void {
|
||||||
|
|
@ -79,53 +44,24 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
|
||||||
resetState(): void {
|
resetState(): void {
|
||||||
this.imageFile = null;
|
this.imageFile = null;
|
||||||
this.processingStatus = '';
|
this.processingStatus = '';
|
||||||
this.detectedText = '';
|
this.loading = false;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown(e: KeyboardEvent): void {
|
onFileChange(event: any): 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<void> {
|
|
||||||
const file: File = event.target.files[0];
|
const file: File = event.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
this.imageFile = file;
|
this.imageFile = file;
|
||||||
this.processingStatus = 'Verarbeitung läuft...';
|
this.processingStatus = 'Verarbeitung läuft...';
|
||||||
this.detectedText = '';
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
// Bild als Base64 laden
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async (e) => {
|
reader.onload = async (e) => {
|
||||||
this.originalImageSrc = e.target?.result;
|
const imageSrc = e.target?.result;
|
||||||
|
|
||||||
// Bild als Base64-String ohne Präfix (data:image/...)
|
// 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 {
|
try {
|
||||||
// Eingabefelder ausblenden und Canvas anzeigen
|
|
||||||
this.formVisible = false;
|
|
||||||
this.canvasVisible = true;
|
|
||||||
|
|
||||||
// Anfrage an den Backend-Service senden
|
|
||||||
const response = await this.http.post<any>('/api/ocr', { image: imageBase64 }).toPromise();
|
const response = await this.http.post<any>('/api/ocr', { image: imageBase64 }).toPromise();
|
||||||
|
|
||||||
if (!response || !response.results) {
|
if (!response || !response.results) {
|
||||||
|
|
@ -134,204 +70,35 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bildverarbeitung im Frontend durchführen
|
this.processingStatus = 'Verarbeitung abgeschlossen';
|
||||||
this.imagename = (response.results && response.results.length>0)?response.results[0].name:null;
|
|
||||||
await this.processImage(response.results);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim OCR-Service:', error);
|
|
||||||
this.processingStatus = 'Fehler beim OCR-Service';
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
// Eingabefelder ausblenden und Canvas anzeigen
|
|
||||||
this.formVisible = true;
|
|
||||||
this.canvasVisible = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadFabricImage(url: string): Promise<fabric.Image> {
|
// Emit Event mit Bilddaten und OCR-Ergebnissen
|
||||||
return new Promise((resolve, reject) => {
|
const bildname=this.imageFile?.name??'';
|
||||||
fabric.Image.fromURL(
|
const bildid=response.results.length>0?response.results[0].name:null
|
||||||
url,
|
const boxes:Box[] = [];
|
||||||
(img) => {
|
response.results.forEach((result: OcrResult) => {
|
||||||
resolve(img);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
crossOrigin: 'anonymous',
|
|
||||||
originX: 'left',
|
|
||||||
originY: 'top',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async processImage(ocrResults: any[]): Promise<void> {
|
|
||||||
// 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;
|
const box = result.box;
|
||||||
|
|
||||||
// Grenzen berechnen
|
|
||||||
const xs = box.map((point: number[]) => point[0]);
|
const xs = box.map((point: number[]) => point[0]);
|
||||||
const ys = box.map((point: number[]) => point[1]);
|
const ys = box.map((point: number[]) => point[1]);
|
||||||
const xMin = Math.min(...xs);
|
const xMin = Math.min(...xs);
|
||||||
const xMax = Math.max(...xs);
|
const xMax = Math.max(...xs);
|
||||||
const yMin = Math.min(...ys);
|
const yMin = Math.min(...ys);
|
||||||
const yMax = Math.max(...ys);
|
const yMax = Math.max(...ys);
|
||||||
|
boxes.push({x1:xMin,x2:xMax,y1:yMin,y2:yMax})
|
||||||
// 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,
|
|
||||||
});
|
});
|
||||||
|
const deckImage:DeckImage={name:bildname,id:bildid,boxes}
|
||||||
|
this.imageUploaded.emit({ imageSrc, deckImage });
|
||||||
|
|
||||||
// Event-Listener hinzufügen
|
// Schließe das Upload-Modal
|
||||||
rect.on('modified', () => {
|
this.closeModal();
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Setzen des Hintergrundbildes:', error);
|
console.error('Fehler beim OCR-Service:', error);
|
||||||
this.processingStatus = 'Fehler bei der Bildverarbeitung';
|
this.processingStatus = 'Fehler beim OCR-Service';
|
||||||
this.loading = false;
|
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({
|
reader.readAsDataURL(file);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue