'category', 'text' => strtoupper((string) $category['name'])]; foreach ($category['items'] as $item) { $lines[] = [ 'type' => 'item', 'text' => sprintf('%s $%.2f', $item['name'], $item['price_cents'] / 100), ]; $lines[] = ['type' => 'description', 'text' => (string) $item['description']]; } } foreach (array_chunk($lines, 24) as $chunk) { $pages[] = $this->pageStream($businessName, $chunk); } if ($pages === []) { $pages[] = $this->pageStream($businessName, []); } $pdf = $this->buildPdf($pages); header('Content-Type: application/pdf'); header('Content-Disposition: attachment; filename="lakeside-orders-menu.pdf"'); header('Content-Length: ' . strlen($pdf)); echo $pdf; exit; } private function pageStream(string $businessName, array $lines): string { $escape = static function (string $value): string { $ascii = function_exists('iconv') ? (iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value) ?: $value) : preg_replace('/[^\x20-\x7E]/', '', $value); return str_replace( ['\\', '(', ')', "\r", "\n"], ['\\\\', '\\(', '\\)', ' ', ' '], (string) $ascii ); }; $stream = "0.035 0.071 0.102 rg\n0 0 612 792 re f\n"; $stream .= "0.78 0.58 0.20 rg\nBT /F1 25 Tf 46 738 Td (" . $escape($businessName) . ") Tj ET\n"; $stream .= "0.91 0.92 0.90 rg\nBT /F1 10 Tf 46 718 Td (LAKESIDE PICKUP | ONLINE MENU) Tj ET\n"; $stream .= "0.78 0.58 0.20 RG\n46 704 m 566 704 l S\n"; $y = 678; foreach ($lines as $line) { $text = $escape(substr((string) $line['text'], 0, 92)); if ($line['type'] === 'category') { $stream .= "0.78 0.58 0.20 rg\nBT /F1 15 Tf 46 {$y} Td ({$text}) Tj ET\n"; $y -= 24; } elseif ($line['type'] === 'item') { $stream .= "0.96 0.95 0.89 rg\nBT /F1 12 Tf 54 {$y} Td ({$text}) Tj ET\n"; $y -= 17; } else { $stream .= "0.68 0.72 0.72 rg\nBT /F1 8 Tf 54 {$y} Td ({$text}) Tj ET\n"; $y -= 23; } } $stream .= "0.55 0.60 0.62 rg\nBT /F1 8 Tf 46 35 Td (Prices and availability are subject to change.) Tj ET\n"; return $stream; } private function buildPdf(array $streams): string { $objects = []; $objects[1] = '<< /Type /Catalog /Pages 2 0 R >>'; $pageIds = []; $nextId = 4; foreach ($streams as $index => $stream) { $pageId = $nextId++; $contentId = $nextId++; $pageIds[] = $pageId . ' 0 R'; $objects[$pageId] = sprintf( '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 3 0 R >> >> /Contents %d 0 R >>', $contentId ); $objects[$contentId] = "<< /Length " . strlen($stream) . " >>\nstream\n{$stream}endstream"; } $objects[2] = '<< /Type /Pages /Kids [' . implode(' ', $pageIds) . '] /Count ' . count($pageIds) . ' >>'; $objects[3] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>'; ksort($objects); $pdf = "%PDF-1.4\n"; $offsets = [0]; foreach ($objects as $id => $object) { $offsets[$id] = strlen($pdf); $pdf .= "{$id} 0 obj\n{$object}\nendobj\n"; } $xref = strlen($pdf); $count = max(array_keys($objects)) + 1; $pdf .= "xref\n0 {$count}\n0000000000 65535 f \n"; for ($id = 1; $id < $count; $id++) { $pdf .= sprintf("%010d 00000 n \n", $offsets[$id] ?? 0); } $pdf .= "trailer\n<< /Size {$count} /Root 1 0 R >>\nstartxref\n{$xref}\n%%EOF"; return $pdf; } }