'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'