This commit is contained in:
@@ -11,7 +11,7 @@ Aktuelle Version: **v1.1**
|
|||||||
- Vorteig
|
- Vorteig
|
||||||
- Rückwärts-Zeitplan
|
- Rückwärts-Zeitplan
|
||||||
- Rezept-Link über URL-Hash
|
- Rezept-Link über URL-Hash
|
||||||
- Rezept direkt als PDF herunterladen
|
- Rezept direkt als einseitige DIN-A4-PDF im Cyberpunk-Stil herunterladen
|
||||||
|
|
||||||
Live: https://teig.deploybar.de/
|
Live: https://teig.deploybar.de/
|
||||||
|
|
||||||
|
|||||||
+47
-18
@@ -1220,27 +1220,56 @@ function wrapPdfLines(lines, width=92){
|
|||||||
function buildPdfBytes(lines){
|
function buildPdfBytes(lines){
|
||||||
const encoder=new TextEncoder();
|
const encoder=new TextEncoder();
|
||||||
const title=String(lines[0]||"PIZZATEIG");
|
const title=String(lines[0]||"PIZZATEIG");
|
||||||
const body=wrapPdfLines(lines.slice(1));
|
let body=wrapPdfLines(lines.slice(1),88);
|
||||||
const pageSize=46;
|
const maxRows=62;
|
||||||
const pages=[];
|
if(body.length>maxRows) body=[...body.slice(0,maxRows-1),"[... weitere Angaben gekürzt]"];
|
||||||
for(let i=0;i<Math.max(1,Math.ceil(body.length/pageSize));i++) pages.push(body.slice(i*pageSize,(i+1)*pageSize));
|
|
||||||
|
const units=body.reduce((sum,line)=>sum+(line ? 1 : .55),0)||1;
|
||||||
|
const lineHeight=Math.max(8.5,Math.min(13,610/units));
|
||||||
|
const bodySize=Math.max(6.8,Math.min(9.5,lineHeight-1.8));
|
||||||
|
|
||||||
|
let stream="q\n";
|
||||||
|
// Dunkle Grundfläche und dezentes Terminal-Raster.
|
||||||
|
stream+="0.027 0.031 0.043 rg 0 0 595 842 re f\n";
|
||||||
|
stream+="0.055 0.082 0.114 RG 0.3 w\n";
|
||||||
|
for(let x=19;x<595;x+=32) stream+=`${x} 0 m ${x} 842 l S\n`;
|
||||||
|
for(let y=10;y<842;y+=32) stream+=`0 ${y} m 595 ${y} l S\n`;
|
||||||
|
// Panel, Neon-Kanten und asymmetrische Akzentleisten.
|
||||||
|
stream+="0.063 0.082 0.122 rg 28 58 539 704 re f\n";
|
||||||
|
stream+="0 0.941 1 RG 0.9 w 28 58 539 704 re S\n";
|
||||||
|
stream+="0 0.941 1 rg 28 786 539 3 re f\n";
|
||||||
|
stream+="1 0.176 0.471 rg 28 776 128 5 re f\n";
|
||||||
|
stream+="1 0.176 0.471 rg 552 58 15 15 re f\nQ\n";
|
||||||
|
|
||||||
|
// Kopfzeile im Stil der Anwendung.
|
||||||
|
stream+=`BT\n/F2 22 Tf\n0 0.941 1 rg\n36 805 Td\n<${winAnsiHex("TEIG//TERMINAL")}> Tj\nET\n`;
|
||||||
|
stream+=`BT\n/F1 7.5 Tf\n1 0.176 0.471 rg\n37 793 Td\n<${winAnsiHex("RECIPE EXPORT // OFFLINE GENERATED // SYSTEM READY")}> Tj\nET\n`;
|
||||||
|
stream+=`BT\n/F2 14 Tf\n0.922 0.965 1 rg\n42 741 Td\n<${winAnsiHex(title)}> Tj\nET\n`;
|
||||||
|
stream+="0 0.941 1 RG 0.7 w 42 729 m 553 729 l S\n";
|
||||||
|
|
||||||
|
let y=711;
|
||||||
|
body.forEach(line=>{
|
||||||
|
if(!line){ y-=lineHeight*.55; return; }
|
||||||
|
const section=/^[A-ZÄÖÜ][A-ZÄÖÜ0-9 /()%-]{2,}$/.test(line.trim()) && line.trim().length<42;
|
||||||
|
const isLink=/^https?:\/\//.test(line.trim());
|
||||||
|
const font=section?"F2":"F1";
|
||||||
|
const size=section?Math.min(10.5,bodySize+1):isLink?Math.max(6.5,bodySize-1):bodySize;
|
||||||
|
const color=section?"1 0.176 0.471":isLink?"0 0.941 1":"0.886 0.925 0.961";
|
||||||
|
if(section) stream+=`0 0.941 1 rg 42 ${y-1.5} 5 5 re f\n`;
|
||||||
|
stream+=`BT\n/${font} ${size.toFixed(1)} Tf\n${color} rg\n${section?53:42} ${y.toFixed(1)} Td\n<${winAnsiHex(line.trimEnd())}> Tj\nET\n`;
|
||||||
|
y-=lineHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
stream+="0 0.941 1 RG 0.5 w 42 47 m 553 47 l S\n";
|
||||||
|
stream+=`BT\n/F1 7 Tf\n0 0.941 1 rg\n42 35 Td\n<${winAnsiHex("TEIG//TERMINAL // LOCAL PDF // A4 // 1 OF 1")}> Tj\nET\n`;
|
||||||
|
|
||||||
const objects=[];
|
const objects=[];
|
||||||
objects[1]="<< /Type /Catalog /Pages 2 0 R >>";
|
objects[1]="<< /Type /Catalog /Pages 2 0 R >>";
|
||||||
objects[3]="<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>";
|
objects[2]="<< /Type /Pages /Kids [6 0 R] /Count 1 >>";
|
||||||
objects[4]="<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>";
|
objects[3]="<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>";
|
||||||
const pageRefs=[];
|
objects[4]="<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold /Encoding /WinAnsiEncoding >>";
|
||||||
|
objects[5]=`<< /Length ${encoder.encode(stream).length} >>\nstream\n${stream}endstream`;
|
||||||
pages.forEach((pageLines,index)=>{
|
objects[6]="<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents 5 0 R >>";
|
||||||
const contentId=5+index*2, pageId=contentId+1;
|
|
||||||
pageRefs.push(`${pageId} 0 R`);
|
|
||||||
let stream=`BT\n/F2 16 Tf\n50 800 Td\n<${winAnsiHex(title)}> Tj\n/F1 10 Tf\n0 -26 Td\n14 TL\n`;
|
|
||||||
pageLines.forEach(line=>{ stream+=`<${winAnsiHex(line)}> Tj\nT*\n`; });
|
|
||||||
stream+=`ET\nBT\n/F1 8 Tf\n50 32 Td\n<${winAnsiHex(`TEIG//TERMINAL · Seite ${index+1} / ${pages.length}`)}> Tj\nET\n`;
|
|
||||||
objects[contentId]=`<< /Length ${encoder.encode(stream).length} >>\nstream\n${stream}endstream`;
|
|
||||||
objects[pageId]=`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents ${contentId} 0 R >>`;
|
|
||||||
});
|
|
||||||
objects[2]=`<< /Type /Pages /Kids [${pageRefs.join(" ")}] /Count ${pages.length} >>`;
|
|
||||||
|
|
||||||
const chunks=[encoder.encode("%PDF-1.4\n")], offsets=[0];
|
const chunks=[encoder.encode("%PDF-1.4\n")], offsets=[0];
|
||||||
let length=chunks[0].length;
|
let length=chunks[0].length;
|
||||||
|
|||||||
@@ -32,7 +32,22 @@ assert.equal(
|
|||||||
const bytes = buildPdfBytes([
|
const bytes = buildPdfBytes([
|
||||||
'PIZZATEIG — Napoletana',
|
'PIZZATEIG — Napoletana',
|
||||||
'4 Teiglinge à 250 g · 1.000 g Teig',
|
'4 Teiglinge à 250 g · 1.000 g Teig',
|
||||||
'Wasser 620 g · Salz 28 g · 20 °C',
|
'Mehl: Caputo Pizzeria · Hydration 62 %',
|
||||||
|
'',
|
||||||
|
'ZUTATEN',
|
||||||
|
' Mehl 605 g',
|
||||||
|
' Wasser 375 g',
|
||||||
|
' Frischhefe 2,3 g',
|
||||||
|
' Salz 17 g',
|
||||||
|
'',
|
||||||
|
'ZEITPLAN',
|
||||||
|
' 19:00 Mo — Teig kneten und ruhen lassen (30 Min.)',
|
||||||
|
' 19:30 Mo — Ballen formen und bei Raumtemperatur reifen lassen',
|
||||||
|
' 23:00 Mo — Ballen in den Kühlschrank stellen',
|
||||||
|
' 15:00 Di — Ballen aus dem Kühlschrank nehmen',
|
||||||
|
' 19:00 Di — Pizza backen',
|
||||||
|
'',
|
||||||
|
'https://teig.deploybar.de/#style=napo&balls=4&ball=250&plan=d24',
|
||||||
]);
|
]);
|
||||||
assert.ok(bytes instanceof Uint8Array);
|
assert.ok(bytes instanceof Uint8Array);
|
||||||
const ascii = Buffer.from(bytes).toString('latin1');
|
const ascii = Buffer.from(bytes).toString('latin1');
|
||||||
@@ -40,8 +55,19 @@ assert.ok(ascii.startsWith('%PDF-1.4'));
|
|||||||
assert.match(ascii, /\/Type \/Catalog/);
|
assert.match(ascii, /\/Type \/Catalog/);
|
||||||
assert.match(ascii, /\/Type \/Page\b/);
|
assert.match(ascii, /\/Type \/Page\b/);
|
||||||
assert.match(ascii, /\/Encoding \/WinAnsiEncoding/);
|
assert.match(ascii, /\/Encoding \/WinAnsiEncoding/);
|
||||||
|
assert.match(ascii, /0\.027 0\.031 0\.043 rg/); // dunkler Cyberpunk-Hintergrund
|
||||||
|
assert.match(ascii, /0 0\.941 1 rg/); // Cyan-Akzent
|
||||||
|
assert.match(ascii, /1 0\.176 0\.471 rg/); // Magenta-Akzent
|
||||||
assert.match(ascii, /xref\n0 \d+/);
|
assert.match(ascii, /xref\n0 \d+/);
|
||||||
assert.match(ascii, /%%EOF\n$/);
|
assert.match(ascii, /%%EOF\n$/);
|
||||||
|
assert.match(ascii, /\/Count 1\b/);
|
||||||
|
|
||||||
|
const longPdf = Buffer.from(buildPdfBytes([
|
||||||
|
'PIZZATEIG — Belastungstest',
|
||||||
|
...Array.from({ length: 120 }, (_, i) => `ZEILE ${i + 1} — Rezeptinformation`),
|
||||||
|
])).toString('latin1');
|
||||||
|
assert.match(longPdf, /\/Count 1\b/, 'PDF muss auf genau eine Seite begrenzt bleiben');
|
||||||
|
assert.equal((longPdf.match(/\/Type \/Page\b/g) || []).length, 1, 'PDF enthält mehr als eine A4-Seite');
|
||||||
|
|
||||||
fs.writeFileSync('/tmp/teig-terminal-download-test.pdf', Buffer.from(bytes));
|
fs.writeFileSync('/tmp/teig-terminal-download-test.pdf', Buffer.from(bytes));
|
||||||
console.log('pdf download tests: OK');
|
console.log('pdf download tests: OK');
|
||||||
|
|||||||
Reference in New Issue
Block a user