Skip to content

Instantly share code, notes, and snippets.

@zb3
Last active August 18, 2023 13:50
Show Gist options
  • Save zb3/36ed58f8d9ba8d711f9f555bc613f733 to your computer and use it in GitHub Desktop.
Save zb3/36ed58f8d9ba8d711f9f555bc613f733 to your computer and use it in GitHub Desktop.
Merge PDF files with custom titles using Ghostscript
<?php
// merge_pdf_files(['file1.pdf', 'file2.pdf', 'file3.pdf'], ["title a", "title b", "title c"], "outfile.pdf")
function encode_ps_base($str) { // <hex encoded UTF-16BE with BOM>
return '<'.bin2hex($str).'>';
}
function encode_ps_utf16($str) { // <hex encoded UTF-16BE with BOM>
$utf16be = "\xFE\xFF".mb_convert_encoding($str, 'UTF-16BE', 'UTF-8');
return encode_ps_base($utf16be);
}
function merge_pdf_files($files, $titles, $outfile) {
$cmd = 'gs -q -dNOSAFER -sDEVICE=pdfwrite -o '.escapeshellarg($outfile).' - 2>&1';
$pipes = [];
$proc = proc_open($cmd, [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
], $pipes);
if (!is_resource($proc)) {
return false;
}
$encoded_files = implode(' ', array_map('encode_ps_base', $files));
$encoded_titles = implode(' ', array_map('encode_ps_utf16', $titles));
$script = <<<SCRIPT
/files [{$encoded_files}] def
/titles [{$encoded_titles}] def
/fileCount files length def
0 1 fileCount 1 sub {
/index exch def
files index get /fileName exch def
titles index get /title exch def
/pagesDone currentpagedevice /PageCount get 1 add def
fileName run [ /Page pagesDone /Title title /OUT pdfmark
} for
SCRIPT;
fwrite($pipes[0], $script);
fclose($pipes[0]);
$err = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$exit_code = proc_close($proc);
if ($exit_code) {
trigger_error("Ghostscript error: $err", E_USER_WARNING);
}
return $exit_code === 0;
}
% gs -dNOSAFER -sDEVICE=pdfwrite -o finished.pdf mergepdf.ps
% replace these
/files [(file1.pdf) (file2.pdf) (file3.pdf)] def
% might want to use hex encoding, but then it must be UTF-16BE with BOM
/titles [(title1) (title2) (title3)] def
/fileCount files length def
0 1 fileCount 1 sub {
/index exch def
files index get /fileName exch def
titles index get /title exch def
/pagesDone currentpagedevice /PageCount get 1 add def
% "[" is a ... mark operator, so it doesn't necessarily start the array
% so it's like the "pdfmark" pops it, maybe "]pdfmark" 'd make it more clear
fileName run [ /Page pagesDone /Title title /OUT pdfmark
} for
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment