render($pressRelease); return response()->streamDownload( static function () use ($pdf): void { echo $pdf; }, $this->filename($pressRelease), [ 'Content-Type' => 'application/pdf', 'Cache-Control' => 'private, max-age=0, must-revalidate', ], ); } public function filename(PressRelease $pressRelease): string { return 'Veroeffentlichungsnachweis-PM-'.$pressRelease->id.'.pdf'; } public function render(PressRelease $pressRelease): string { $content = ''; $portalLabel = $pressRelease->portal instanceof Portal ? $pressRelease->portal->label() : (string) $pressRelease->portal; $publishedAt = $pressRelease->published_at; $dateLabel = $publishedAt?->format('d.m.Y') ?? 'n/a'; $timeLabel = $publishedAt?->format('H:i') ?? null; $content .= $this->text(self::LEFT, 790, 'Veröffentlichungsnachweis', 22, 'F2'); $content .= $this->line(self::LEFT, 778, self::RIGHT, 778, 0.7); $content .= $this->text(self::LEFT, 760, $portalLabel, 11, 'F2'); $y = 712.0; $headline = $timeLabel ? "Diese Pressemitteilung wurde am {$dateLabel} um {$timeLabel} Uhr auf {$portalLabel} veröffentlicht." : "Diese Pressemitteilung wurde am {$dateLabel} auf {$portalLabel} veröffentlicht."; $y = $this->wrappedText($content, self::LEFT, $y, $headline, 11, self::RIGHT - self::LEFT, 16); $y -= 24; $content .= $this->text(self::LEFT, $y, 'Titel', 9, 'F2'); $y = $this->wrappedText($content, self::LEFT, $y - 15, (string) $pressRelease->title, 13, self::RIGHT - self::LEFT, 17, 'F2'); $y -= 18; foreach ($this->metaRows($pressRelease, $portalLabel, $dateLabel) as [$label, $value]) { $content .= $this->text(self::LEFT, $y, $label, 9, 'F2'); $this->wrappedText($content, self::LEFT + 110, $y, $value, 9, self::RIGHT - self::LEFT - 110, 12); $y -= 20; } $y -= 10; $content .= $this->text(self::LEFT, $y, 'Vorschau', 9, 'F2'); $this->wrappedText($content, self::LEFT, $y - 15, $this->previewText($pressRelease), 9, self::RIGHT - self::LEFT, 12); $content .= $this->line(self::LEFT, 70, self::RIGHT, 70, 0.5); $content .= $this->text(self::LEFT, 56, 'Erstellt über pressekonto – maschinell erzeugter Nachweis.', 8); return $this->buildPdf($content); } /** * @return list */ private function metaRows(PressRelease $pressRelease, string $portalLabel, string $dateLabel): array { return array_values(array_filter([ ['Portal', $portalLabel], ['Veröffentlicht', $dateLabel], ['URL', $this->publicUrl($pressRelease)], $pressRelease->category?->name ? ['Kategorie', (string) $pressRelease->category->name] : null, ])); } private function publicUrl(PressRelease $pressRelease): string { $base = match ($pressRelease->portal) { Portal::Presseecho => config('domains.domain_presseecho_url'), Portal::Businessportal24 => config('domains.domain_businessportal_url'), default => config('app.url'), }; return rtrim((string) $base, '/').'/'.ltrim((string) $pressRelease->slug, '/'); } private function previewText(PressRelease $pressRelease): string { $plain = trim(html_entity_decode(strip_tags((string) $pressRelease->text))); return Str::limit($plain, 600); } private function buildPdf(string $content): string { $objects = [ "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n", "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n", "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ".self::PAGE_WIDTH.' '.self::PAGE_HEIGHT."] /Resources << /Font << /F1 4 0 R /F2 5 0 R >> >> /Contents 6 0 R >>\nendobj\n", "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n", "5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\nendobj\n", "6 0 obj\n<< /Length ".strlen($content)." >>\nstream\n{$content}endstream\nendobj\n", ]; $pdf = "%PDF-1.4\n"; $offsets = [0]; foreach ($objects as $object) { $offsets[] = strlen($pdf); $pdf .= $object; } $xrefOffset = strlen($pdf); $pdf .= "xref\n0 ".(count($objects) + 1)."\n"; $pdf .= "0000000000 65535 f \n"; foreach (array_slice($offsets, 1) as $offset) { $pdf .= sprintf("%010d 00000 n \n", $offset); } $pdf .= "trailer\n<< /Size ".(count($objects) + 1)." /Root 1 0 R >>\n"; $pdf .= "startxref\n{$xrefOffset}\n%%EOF\n"; return $pdf; } private function escapePdfText(string $text): string { $encoded = iconv('UTF-8', 'Windows-1252//TRANSLIT//IGNORE', $text); return str_replace(['\\', '(', ')'], ['\\\\', '\(', '\)'], $encoded ?: Str::ascii($text)); } private function text(float $x, float $y, string $text, int $size = 9, string $font = 'F1'): string { return sprintf( "BT /%s %d Tf %.2F %.2F Td (%s) Tj ET\n", $font, $size, $x, $y, $this->escapePdfText($text), ); } private function line(float $x1, float $y1, float $x2, float $y2, float $width = 0.5): string { return sprintf("%.2F w %.2F %.2F m %.2F %.2F l S\n", $width, $x1, $y1, $x2, $y2); } private function wrappedText( string &$content, float $x, float $y, string $text, int $size, float $width, float $lineHeight = 12, string $font = 'F1', ): float { foreach ($this->wrap($text, $width, $size) as $line) { if ($line !== '') { $content .= $this->text($x, $y, $line, $size, $font); } $y -= $lineHeight; } return $y; } /** * @return list */ private function wrap(string $text, float $width, int $size): array { $lines = []; $maxCharacters = max(8, (int) floor($width / ($size * 0.5))); foreach (explode("\n", str_replace(["\r\n", "\r"], "\n", $text)) as $paragraph) { if (trim($paragraph) === '') { $lines[] = ''; continue; } $line = ''; foreach (preg_split('/\s+/', $paragraph) ?: [] as $word) { $candidate = trim($line.' '.$word); if ($line !== '' && Str::length($candidate) > $maxCharacters) { $lines[] = $line; $line = $word; continue; } $line = $candidate; } if ($line !== '') { $lines[] = $line; } } return $lines; } }