feat: all AI formats printable, crop-vs-border rule, mobile layout, Passbilder rename
Print now offers every format the AI pipeline knows (9x13 to 60x90, DIN A6-A2, squares, poster/frame sizes to 70x100, and the screen ratios as physical sizes). When a picture does not match the target ratio the user picks per image between cropping to fill and keeping the whole picture on a border colour - never a stretch. The module is called Passbilder now; /druck redirects. Fixes two real defects: sharp runs extend after resize, so padded cells came out oversized (a 35x45 mm cell became 35x171 mm with a border), and the grid packer rotated a single portrait photo just because more would fit sideways. Mobile: cards become rows, touch targets ~40px, crop editor as a bottom sheet. Verified at 390/820/1440px - all three produce the same PDF.
This commit is contained in:
@@ -59,14 +59,22 @@ import Base from '../layouts/Base.astro';
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5 · Druck — exakte Maße und Schnittmarken (ohne KI)</h2>
|
||||
<p class="lead">Der Reiter <b>Druck</b> ist der Fotolabor-Teil von Klarbild: kein Modell, keine Kosten,
|
||||
keine Wartezeit. Nur Zuschnitt, Skalierung und Geometrie.</p>
|
||||
<h2>5 · Passbilder & Druckbogen — exakte Maße und Schnittmarken (ohne KI)</h2>
|
||||
<p class="lead">Der Reiter <b>Passbilder</b> ist der Fotolabor-Teil von Klarbild: kein Modell, keine Kosten,
|
||||
keine Wartezeit. Nur Zuschnitt, Skalierung und Geometrie — vom biometrischen Passbild bis zum
|
||||
30 × 40-Poster.</p>
|
||||
<ol>
|
||||
<li>Bilder hochladen (ziehen, ⌘/Strg + V, oder <b>Aus Bibliothek</b> ein fertiges Ergebnis holen).</li>
|
||||
<li>Je Bild das <b>Endmaß</b> wählen — biometrisches Passbild 35 × 45 mm, 3 × 4 cm, 9 × 13, 10 × 15,
|
||||
12 × 15, 13 × 18 … oder <b>Eigenes Maß</b>: <code>12x15</code> (cm), <code>35x45mm</code>,
|
||||
<code>5</code> (= 5 × 5 cm) oder <code>4:3/15</code> (Verhältnis 4:3, längere Kante 15 cm).</li>
|
||||
<li>Je Bild das <b>Endmaß</b> wählen. Es sind <b>alle Formate der KI-Generierung</b> da —
|
||||
biometrisches Passbild 35 × 45 mm, 2 × 3 bis 6 × 9 cm, 9 × 13 bis 24 × 30,
|
||||
Poster- und Rahmenmaße 30 × 40 bis 70 × 100, Quadrate, DIN A6–A2 und die
|
||||
Seitenverhältnisse (16:9 „The Frame", 9:16, 3:2, 4:3, 5:4) — oder <b>Eigenes Maß</b>:
|
||||
<code>12x15</code> (cm), <code>35x45mm</code>, <code>5</code> (= 5 × 5 cm) oder
|
||||
<code>4:3/15</code> (Verhältnis 4:3, längere Kante 15 cm).</li>
|
||||
<li><b>Wenn das Bild nicht zum Format passt</b>, entscheidest du je Bild:
|
||||
<b>Zuschneiden</b> (das Format wird ausgefüllt, außen fehlt etwas — Standard) oder
|
||||
<b>Rand lassen</b> (das ganze Bild bleibt sichtbar, außen steht die <b>Randfarbe</b> —
|
||||
weiß, schwarz, papierfarben oder frei gewählt). <b>Verzerrt wird nie.</b></li>
|
||||
<li>Auf das Vorschaubild tippen → <b>Zuschnitt</b>: schieben und zoomen. Das Seitenverhältnis bleibt
|
||||
fest am Zielformat, verzerrt wird nie.</li>
|
||||
<li><b>Anzahl</b> je Bild setzen — so entsteht der Passbild- oder Kita-Satz.</li>
|
||||
@@ -98,7 +106,8 @@ import Base from '../layouts/Base.astro';
|
||||
<li><b>Automatische Anordnung</b>: gleich große Bilder ergeben ein sauberes Raster; bei gemischten
|
||||
Größen werden die kleinen automatisch in die Restflächen neben den großen gesetzt.</li>
|
||||
</ul>
|
||||
<p class="mini">Auch per Telegram: Bild schicken → „📐 Druckbogen" → Maß und Anzahl antippen → PDF zurück.</p>
|
||||
<p class="mini">Auch per Telegram: Bild schicken → „📐 Passbilder" → Maß und Anzahl antippen → PDF zurück.
|
||||
Und am Handy funktioniert die Seite genauso wie am Rechner.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
@@ -22,6 +22,8 @@ interface CellIn {
|
||||
count?: number;
|
||||
allowRotate?: boolean;
|
||||
landscape?: boolean; // Bildformat quer statt hoch
|
||||
fit?: 'cover' | 'contain';// abweichendes Seitenverhältnis: zuschneiden oder einpassen
|
||||
bg?: string; // Randfarbe beim Einpassen
|
||||
}
|
||||
|
||||
const json = (b: unknown, s = 200) =>
|
||||
@@ -72,12 +74,12 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const buf = await loadSource(r.cell.src, locals.user as any);
|
||||
if (needPlain.has(r.cell.id)) {
|
||||
const out = await renderCell(buf, r.cell.crop ?? null, r.wMm, r.hMm, dpi,
|
||||
{ ext: wantExt, bleedMm: sheet.bleedMm, rotate: false });
|
||||
{ ext: wantExt, bleedMm: sheet.bleedMm, rotate: false, fit: fitOf(r.cell), background: bgOf(r.cell) });
|
||||
images[r.cell.id] = { bytes: out.buffer, ext: out.ext };
|
||||
}
|
||||
if (needRotated.has(r.cell.id)) {
|
||||
const out = await renderCell(buf, r.cell.crop ?? null, r.wMm, r.hMm, dpi,
|
||||
{ ext: wantExt, bleedMm: sheet.bleedMm, rotate: true });
|
||||
{ ext: wantExt, bleedMm: sheet.bleedMm, rotate: true, fit: fitOf(r.cell), background: bgOf(r.cell) });
|
||||
images[`${r.cell.id}::rot`] = { bytes: out.buffer, ext: out.ext };
|
||||
}
|
||||
}
|
||||
@@ -174,6 +176,10 @@ function cellSize(c: CellIn): { w: number; h: number } | null {
|
||||
return c.landscape ? { w: s.h, h: s.w } : s;
|
||||
}
|
||||
|
||||
const fitOf = (c: CellIn): 'cover' | 'contain' => (c.fit === 'contain' ? 'contain' : 'cover');
|
||||
/** Randfarbe nur als sicheres Hex zulassen. */
|
||||
const bgOf = (c: CellIn): string => (/^#[0-9a-f]{6}$/i.test(String(c.bg || '')) ? String(c.bg) : '#ffffff');
|
||||
|
||||
const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
|
||||
const numOr = (v: any, def: number, lo: number, hi: number) => {
|
||||
const n = Number(v);
|
||||
|
||||
@@ -31,7 +31,10 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const dpi = Math.min(1200, Math.max(72, Number(body?.dpi) || 300));
|
||||
const ext: 'jpg' | 'png' = body?.ext === 'png' ? 'png' : 'jpg';
|
||||
const buf = await loadSource(src, locals.user as any);
|
||||
const out = await renderCell(buf, crop, size.w, size.h, dpi, { ext, bleedMm: Number(body?.bleedMm) || 0 });
|
||||
const fit = body?.fit === 'contain' ? 'contain' : 'cover';
|
||||
const background = /^#[0-9a-f]{6}$/i.test(String(body?.bg || '')) ? String(body.bg) : '#ffffff';
|
||||
const out = await renderCell(buf, crop, size.w, size.h, dpi,
|
||||
{ ext, bleedMm: Number(body?.bleedMm) || 0, fit, background });
|
||||
|
||||
// Hinweis, falls die Quelle für echte 300 dpi zu klein ist.
|
||||
const cropW = (crop?.w ?? 1) * out.srcPx[0];
|
||||
|
||||
+2
-14
@@ -1,16 +1,4 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import PrintApp from '../components/PrintApp.tsx';
|
||||
// Alte Adresse — die Funktion heißt jetzt „Passbilder".
|
||||
return Astro.redirect('/passbilder', 301);
|
||||
---
|
||||
<Base title="Druck · Klarbild">
|
||||
<h1 class="seiten-titel">Druck</h1>
|
||||
<p class="seiten-lead">
|
||||
Bilder ohne KI auf exakte Maße bringen und mehrere davon in 100 %-Größe mit
|
||||
Schnittmarken auf einen Bogen setzen — Passbildsatz, Kita-Bilder, Sticker.
|
||||
</p>
|
||||
<PrintApp client:load />
|
||||
<style>
|
||||
.seiten-titel{font-family:var(--font-display);font-size:1.6rem;margin:6px 0 4px;letter-spacing:-.02em;}
|
||||
.seiten-lead{color:var(--soft);font-size:13.5px;margin:0 0 16px;max-width:64ch;line-height:1.55;}
|
||||
</style>
|
||||
</Base>
|
||||
|
||||
+11
-5
@@ -13,7 +13,7 @@ const BODY = `# Klarbild
|
||||
> saubere, druckfertige Bilder (bereinigen, freistellen, exakte Fotoformate,
|
||||
> The-Frame-Querformat, Sticker mit Kontur), liefert an Picdrop (SFTP/FTPS)
|
||||
> aus und sichert auf beliebige Backup-Ziele (z. B. NAS). Zusätzlich ein
|
||||
> KI-freies Druckmodul: Bilder exakt auf physische Maße bringen und mehrere
|
||||
> KI-freie Passbildfunktion: Bilder exakt auf physische Maße bringen und mehrere
|
||||
> davon in 100-%-Größe mit Schnittmarken auf einen Druckbogen setzen (PDF).
|
||||
> Bedienung über Web, Telegram-Bot und HTTP-API/MCP.
|
||||
|
||||
@@ -59,13 +59,19 @@ Sprache der Oberfläche: Deutsch. Stack: Astro 5 (SSR) · Postgres · sharp · p
|
||||
- GET /api/items/:id/file — Ergebnisbild. Query: ?thumb=1 (Vorschau), ?preview=1 (kleines JPG fürs Vollbild),
|
||||
?download=1 (als Datei), ?src=1 (Quelle). Content-Type wird aus den Magic Bytes bestimmt (PNG/JPG/WEBP).
|
||||
|
||||
## Druckmodul (ohne KI, /druck)
|
||||
## Passbilder & Druckbogen (ohne KI, /passbilder)
|
||||
Rein lokale Geometrie: Zuschnitt (sharp) + PDF (pdf-lib). Kein Modell, keine Kosten.
|
||||
- Maßangaben: \`12x15\` = 12×15 cm · \`35x45mm\` · \`5\` = 5×5 cm · \`4:3/15\` = Verhältnis 4:3, längere Kante 15 cm.
|
||||
- Papierformate (id): A6,A5,A4,A3,A3plus(329×483 mm),A2, F9x13,F10x15,F13x18,F15x20,F20x30, Letter, Legal — oder freies Maß.
|
||||
- Bildformate (id): P35x45 (biometrisch), P50x50, K20x30,K30x40,K40x50,K45x60,K60x90,
|
||||
S9x13,S10x15,S12x15,S13x18,S15x20,S18x24,S20x30,S30x40,S30x45,S40x50,S40x60,S50x70,
|
||||
Q10x10,Q13x13,Q20x20,Q30x30, DA6,DA5,DA4,DA3.
|
||||
- Bildformate (id): P35x45 (biometrisch), P50x50 · K20x30,K30x40,K40x50,K45x60,K60x80,K60x90 ·
|
||||
S9x13,S10x15,S11x15,S12x15,S13x18,S15x20,S18x24,S20x25,S20x30,S24x30 ·
|
||||
R30x40,R30x45,R40x50,R40x60,R50x70,R60x80,R60x90,R70x100 (Poster & Rahmen) ·
|
||||
Q10x10,Q13x13,Q20x20,Q30x30 · DA6,DA5,DA4,DA3,DA2 ·
|
||||
W16x9,W9x16,W3x2,W4x3,W5x4 (Seitenverhältnisse als Druckmaß, u. a. The Frame).
|
||||
Damit sind alle Formate der KI-Generierung (src/lib/format.ts) auch hier druckbar.
|
||||
- Passt das Bild nicht zum Format (fit je Zelle): \`cover\` = zuschneiden bis ausgefüllt (Standard,
|
||||
nichts bleibt leer, außen fehlt etwas) oder \`contain\` = ganzes Bild einpassen, außen bleibt die
|
||||
Randfarbe \`bg\` (#rrggbb, Standard #ffffff). Verzerrt wird nie.
|
||||
- Schnitthilfen (marks.mode): \`none\` · \`corner\` (Eckmarken außerhalb des Endformats, Standard 4 mm lang,
|
||||
3 mm Versatz, 0,25 pt Haarlinie) · \`grid\` (durchgehende Linien über den Bogen, laufen nie durch ein Motiv).
|
||||
- Beschnittzugabe (bleedMm): das Bild ragt je Seite darüber hinaus; die Trimmbox bleibt exakt der gewählte Ausschnitt.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import PrintApp from '../components/PrintApp.tsx';
|
||||
---
|
||||
<Base title="Passbilder · Klarbild">
|
||||
<h1 class="seiten-titel">Passbilder & Druckbogen</h1>
|
||||
<p class="seiten-lead">
|
||||
Ohne KI: Bilder auf exakte Maße bringen und mehrere davon in 100-%-Größe mit
|
||||
Schnittmarken auf einen Bogen setzen — vom biometrischen Passbild über den
|
||||
Kita-Satz bis zum 30 × 40-Poster.
|
||||
</p>
|
||||
<PrintApp client:load />
|
||||
<style>
|
||||
.seiten-titel{font-family:var(--font-display);font-size:1.6rem;margin:6px 0 4px;letter-spacing:-.02em;}
|
||||
.seiten-lead{color:var(--soft);font-size:13.5px;margin:0 0 16px;max-width:64ch;line-height:1.55;}
|
||||
@media(max-width:560px){.seiten-titel{font-size:1.35rem;}.seiten-lead{font-size:12.5px;}}
|
||||
</style>
|
||||
</Base>
|
||||
Reference in New Issue
Block a user