'1 (normal)', 2 => '2', 4 => '4', 6 => '6', 9 => '9'] as $n => $label) { $opts .= ''; } return ''; } function ext_lp_args(string $ext): array { $n = (int)($_POST['nup'] ?? 1); return (EXT_PAGES_PER_SHEET && in_array($n, [2, 4, 6, 9], true)) ? ['-o', 'number-up=' . $n] : []; } function ext_file_html(string $name): string { if (!EXT_PREVIEW) { return ''; } return 'Preview'; } /** * Called for every upload. Returns null to leave the file alone, ['error' => msg] on failure, * or ['path' => converted pdf, 'name' => new filename] after converting an Office file. */ function ext_convert(string $tmp, string $orig): ?array { $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION)); if (!EXT_OFFICE || !in_array($ext, EXT_OFFICE_TYPES, true)) { return null; } if (!is_executable(EXT_SOFFICE)) { return ['error' => 'can’t be converted: LibreOffice isn’t installed on the server.']; } @set_time_limit(EXT_TIMEOUT * 2); $tmpdir = sys_get_temp_dir(); $profile = $tmpdir . '/print-lo-profile'; $work = $tmpdir . '/print-lo-' . bin2hex(random_bytes(6)); $lock = fopen($tmpdir . '/print-lo.lock', 'c'); if ($lock) { flock($lock, LOCK_EX); } // LibreOffice allows one run per profile at a time $result = ['error' => 'could not be converted to PDF. Try saving it as a PDF first.']; if (@mkdir($work, 0700, true) && copy($tmp, $work . '/document.' . $ext)) { $cmd = ['/usr/bin/timeout', '-k', '5', (string)EXT_TIMEOUT, EXT_SOFFICE, '-env:UserInstallation=file://' . $profile, '--headless', '--norestore', '--convert-to', 'pdf', '--outdir', $work, $work . '/document.' . $ext]; $p = @proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $work, ['PATH' => '/usr/bin:/bin', 'HOME' => $work, 'LC_ALL' => 'C.UTF-8']); if (is_resource($p)) { stream_get_contents($pipes[1]); stream_get_contents($pipes[2]); fclose($pipes[1]); fclose($pipes[2]); proc_close($p); } $pdf = $work . '/document.pdf'; $dest = tempnam($tmpdir, 'prt'); if (is_file($pdf) && filesize($pdf) > 0 && $dest !== false && copy($pdf, $dest)) { $result = ['path' => $dest, 'name' => cut(pathinfo($orig, PATHINFO_FILENAME), 80) . '.pdf']; } else { ext_rmdir($profile); // a crashed run can leave a broken profile; start fresh next time } } ext_rmdir($work); if ($lock) { flock($lock, LOCK_UN); fclose($lock); } return $result; } function ext_css(): string { return '[hidden]{display:none!important}' . '@media (max-width:480px){.file{flex-wrap:wrap}.fn{flex-basis:100%}}' . '#pv{position:fixed;inset:0;z-index:50;display:flex;flex-direction:column;background:var(--bg)}' . '.pvbar{display:flex;align-items:center;gap:8px;padding:max(10px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right)) 10px max(12px,env(safe-area-inset-left));background:var(--card);border-bottom:1px solid var(--line)}' . '.pvbar b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' . '.pvbar span{color:var(--mute);font-size:.88rem;white-space:nowrap}' . '#pvbody{flex:1;overflow:auto;padding:12px;text-align:center;-webkit-overflow-scrolling:touch}' . '#pvbody canvas,#pvbody img{display:block;height:auto;max-width:100%;margin:0 auto 12px;background:#fff;border:1px solid var(--line)}' . '#pvbody pre{margin:0 auto;max-width:90ch;text-align:left;white-space:pre-wrap;overflow-wrap:anywhere;font:.9rem/1.45 ui-monospace,monospace;color:var(--ink)}'; } /** Preview overlay + small scripts (remembers each device's option choices). */ function ext_page_html(): string { $preview = !EXT_PREVIEW ? '' : <<<'HTML' HTML; $remember = <<<'HTML' HTML; return $preview . "\n" . $remember; } // ---------- Internals ---------- function ext_serve(string $name): void { $path = stored_path($name); $ext = $path === null ? '' : strtolower(pathinfo($path, PATHINFO_EXTENSION)); if ($path === null || !isset(ALLOWED[$ext])) { http_response_code(404); exit('Not found'); } header_remove('Content-Security-Policy'); header('Content-Type: ' . ALLOWED[$ext][0] . ($ext === 'txt' ? '; charset=utf-8' : '')); header('Content-Length: ' . (string)filesize($path)); header("Content-Disposition: inline; filename*=UTF-8''" . rawurlencode(basename($path))); readfile($path); exit; } function ext_asset(string $name): void { $file = EXT_VENDOR . '/' . $name; if (!in_array($name, ['pdf.min.mjs', 'pdf.worker.min.mjs'], true) || !is_file($file)) { http_response_code(404); exit('Not found'); } header_remove('Content-Security-Policy'); header('Content-Type: text/javascript; charset=utf-8'); header('Cache-Control: public, max-age=86400'); header('Content-Length: ' . (string)filesize($file)); readfile($file); exit; } function ext_rmdir(string $dir): void { if (!is_dir($dir)) { return; } $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST); foreach ($it as $f) { ($f->isDir() && !$f->isLink()) ? @rmdir($f->getPathname()) : @unlink($f->getPathname()); } @rmdir($dir); }