Verbessere Image Generator: Fixierter Crop-Rahmen, optimierte Vorschau-Skalierung

- Crop-Rahmen ist fixiert (nicht verschiebbar/skalierbar), nur Bild bewegbar
- Zuschnitt auf exakt 369×492 Pixel
- Finales Badge im A4-Format (794×1123px / 21×29.7cm)
- Vorschau auf 35% skaliert mit korrektem Platzverbrauch
- Große Bilder passen initial in Container, unbegrenztes Zoomen möglich
- Badge-Template behält alle mm/cm-Maße aus print.html
This commit is contained in:
Matthias Lotz 2026-01-11 18:22:10 +01:00
parent 67184e7e01
commit d6c2986ebf
3 changed files with 75 additions and 42 deletions

View File

@ -70,6 +70,7 @@
margin: 0 auto;
padding: 5mm 0 0 0;
object-fit: cover;
object-position: center;
}
.badge-template .your-name {
@ -87,8 +88,9 @@
}
.badge-template .warnung {
font-size: 10mm;
font-size: 9mm;
color: red;
flex: 1;
text-align: center;
font-weight: bold;
}

View File

@ -41,11 +41,14 @@ class EmployeeImageGenerator extends Component {
this.badgeTemplate = useRef("badgeTemplate");
this.cropper = null;
// A4-Format wie Original: 794×1123px (wie thekenheld)
this.targetWidth = 794;
this.targetHeight = 1123;
// Umrechnungsfaktor: 793px / 210mm = 3.776 px/mm
this.pxPerMm = this.targetWidth / 210;
// Zielgröße für das zugeschnittene Foto
this.cropWidth = 369;
this.cropHeight = 492;
// Zielgröße für das finale Badge (A4-Format)
this.badgeWidth = 794;
this.badgeHeight = 1123;
// Umrechnungsfaktor für mm (falls benötigt)
this.pxPerMm = this.badgeWidth / 210;
// Lade Company-Logo beim Start
this.loadCompanyLogo();
@ -107,20 +110,29 @@ class EmployeeImageGenerator extends Component {
return;
}
const aspectRatio = this.targetWidth / this.targetHeight;
const aspectRatio = this.cropWidth / this.cropHeight;
this.cropper = new window.Cropper(imageElement, {
aspectRatio: aspectRatio,
viewMode: 3,
dragMode: 'move',
autoCropArea: 1,
viewMode: 1, // Erlaubt unbegrenztes Zoomen
dragMode: 'move', // Bild verschieben
autoCropArea: 0.7, // Crop-Rahmen ist 70% des Containers - besser sichtbar
restore: false,
guides: true,
center: true,
highlight: false,
cropBoxMovable: true,
cropBoxResizable: true,
cropBoxMovable: false, // Crop-Rahmen NICHT verschiebbar
cropBoxResizable: false, // Crop-Rahmen NICHT in Größe veränderbar
toggleDragModeOnDblclick: false,
zoomable: true,
zoomOnWheel: true, // Zoom mit Mausrad
zoomOnTouch: true, // Zoom mit Touch-Gesten
minContainerWidth: 200,
minContainerHeight: 200,
ready: function() {
// Setze initiales Zoom-Level auf "fit in container"
this.cropper.zoomTo(0);
},
});
}
@ -131,8 +143,8 @@ class EmployeeImageGenerator extends Component {
}
const canvas = this.cropper.getCroppedCanvas({
width: this.targetWidth,
height: this.targetHeight,
width: this.cropWidth,
height: this.cropHeight,
});
if (canvas) {
@ -163,16 +175,30 @@ class EmployeeImageGenerator extends Component {
return;
}
// Konvertiere HTML-Element zu Canvas mit html2canvas (exakt wie thekenheld)
// Entferne temporär die Skalierung vom Wrapper (nicht vom Badge selbst)
const wrapper = badgeElement.parentElement;
const originalTransform = wrapper ? wrapper.style.transform : '';
if (wrapper) {
wrapper.style.transform = 'none';
}
// Konvertiere HTML-Element zu Canvas mit html2canvas in voller A4-Größe
const canvas = await window.html2canvas(badgeElement, {
width: this.targetWidth,
height: this.targetHeight,
width: this.badgeWidth,
height: this.badgeHeight,
scale: 1, // Wichtig: keine zusätzliche Skalierung
backgroundColor: '#ffffff',
});
// Konvertiere Canvas zu Base64 PNG (wie Original thekenheld)
// Stelle die Skalierung für die Vorschau wieder her
if (wrapper) {
wrapper.style.transform = originalTransform;
}
// Konvertiere Canvas zu Base64 PNG
const finalImageBase64 = canvas.toDataURL('image/png');
// Speichere in Odoo (entferne data:image/jpeg;base64, prefix)
// Speichere in Odoo (entferne data:image/png;base64, prefix)
const imageData = finalImageBase64.split(',')[1];
await this.orm.write('hr.employee', [this.state.employeeId], {

View File

@ -54,10 +54,10 @@
<i class="fa fa-info-circle me-2"/>
Bewege und zoome das Bild, um den gewünschten Ausschnitt zu wählen
</div>
<div class="img-container" style="max-height: 500px; overflow: hidden;">
<div class="img-container" style="max-height: 500px; overflow: hidden; display: flex; align-items: center; justify-content: center;">
<img t-ref="cropperImage"
t-att-src="state.imageDataUrl"
style="max-width: 100%; display: block;"/>
style="max-width: 100%; max-height: 100%; display: block;"/>
</div>
</div>
@ -103,32 +103,37 @@
<!-- Rechte Spalte: Vorschau -->
<div class="col-md-6">
<h5 class="mb-3">Vorschau</h5>
<div class="border rounded p-2 bg-light" style="overflow-y: auto; max-height: 600px;">
<!-- HTML Badge Template (wird mit html2canvas konvertiert) -->
<div t-ref="badgeTemplate" class="badge-template">
<header>
<div class="logo">
<img t-if="state.companyLogo" t-att-src="state.companyLogo" alt="Logo" class="logo"/>
<div class="border rounded p-3 d-flex justify-content-center align-items-center"
style="background: rgb(204, 204, 204); min-height: 400px;">
<!-- Wrapper nimmt nur den skalierten Platz ein (21cm × 29.7cm × 0.35 = 7.35cm × 10.4cm) -->
<div style="width: 7.35cm; height: 10.4cm; position: relative;">
<div style="width: 21cm; height: 29.7cm; transform: scale(0.35); transform-origin: top left; position: absolute; top: 0; left: 0;">
<div t-ref="badgeTemplate" class="badge-template">
<header>
<div class="logo">
<img t-if="state.companyLogo" t-att-src="state.companyLogo" alt="Logo" class="logo"/>
</div>
</header>
<div class="content">
<div class="headline1">An der Theke für Dich</div>
<div class="headline2">Werkstattaufsicht und Werkzeugausgabe</div>
<img t-if="state.croppedImageDataUrl"
t-att-src="state.croppedImageDataUrl"
alt="avatar"
class="centered-image"/>
<div class="your-name" t-esc="state.employeeName || 'Dein Name'"/>
<hr/>
<div class="headline3">Themenschwerpunkte:</div>
<div class="topics" t-esc="state.jobFocus || 'Deine Schwerpunkte'"/>
</div>
<footer><div class="warnung">! Zugang zur Theke nur für Berechtigte !</div></footer>
</div>
</header>
<div class="content">
<div class="headline1">An der Theke für Dich</div>
<div class="headline2">Werkstattaufsicht und Werkzeugausgabe</div>
<img t-if="state.croppedImageDataUrl"
t-att-src="state.croppedImageDataUrl"
alt="avatar"
class="centered-image"/>
<div class="your-name" t-esc="state.employeeName || 'Dein Name'"/>
<hr/>
<div class="headline3">Themenschwerpunkte:</div>
<div class="topics" t-esc="state.jobFocus || 'Deine Schwerpunkte'"/>
<div class="warnung">! Zugang zur Theke nur für Berechtigte !</div>
</div>
<footer></footer>
</div>
</div>
<small class="text-muted d-block mt-2 text-center">
A4-Format (793 × 1122 Pixel) - 50% Vorschau
A4-Format (21×29.7cm) - 35% Vorschau
</small>
</div>
</div>