]> mj.ucw.cz Git - paperjam.git/commitdiff
Shuffling code around
authorMartin Mares <mj@ucw.cz>
Mon, 2 Apr 2018 10:29:28 +0000 (12:29 +0200)
committerMartin Mares <mj@ucw.cz>
Mon, 2 Apr 2018 10:29:28 +0000 (12:29 +0200)
Makefile
cmds.cc
gs.cc [deleted file]
jam.h
paperjam.cc
parse.cc
pdf-tools.cc
pdf-tools.h
pdf.cc [new file with mode: 0644]

index bb74b758783b4ae6b1936893045d78c0f63429b1..9344b44e8a3ab196d55073a838c1035081051203 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ CXXFLAGS=-O2 -Wall -Wextra -Wno-parentheses -std=gnu++11 -g
 
 all: paperjam
 
-paperjam: paperjam.o pdf-tools.o parse.o cmds.o gs.o
+paperjam: paperjam.o pdf-tools.o parse.o cmds.o pdf.o
        $(LD) -o $@ $^ $(LDLIBS)
 paperjam: LDLIBS += -lqpdf
 paperjam: LD=$(CXX)
@@ -11,4 +11,4 @@ paperjam.o: jam.h pdf-tools.h
 pdf-tools.o: jam.h pdf-tools.h
 parse.o: jam.h pdf-tools.h
 cmds.o: jam.h pdf-tools.h
-gs.o: jam.h pdf-tools.h
+pdf.o: jam.h pdf-tools.h
diff --git a/cmds.cc b/cmds.cc
index 71f9c29ae97fc5c796a7fdaceb34b7e418d30edd..9ca7637d684dd1966615086ebf068f14e35643f5 100644 (file)
--- a/cmds.cc
+++ b/cmds.cc
@@ -1,3 +1,9 @@
+/*
+ *     PaperJam -- Commands
+ *
+ *     (c) 2018 Martin Mares <mj@ucw.cz>
+ */
+
 #include <cassert>
 #include <cstdlib>
 #include <cstdio>
diff --git a/gs.cc b/gs.cc
deleted file mode 100644 (file)
index b52448a..0000000
--- a/gs.cc
+++ /dev/null
@@ -1,62 +0,0 @@
-#include <cassert>
-#include <cstdlib>
-#include <cstdio>
-#include <unistd.h>
-#include <sys/wait.h>
-
-#include "jam.h"
-
-vector<BBox> gs_bboxes(const char *in)
-{
-  int pipes[2];
-  if (pipe(pipes) < 0)
-    die("Cannot create pipe: %m");
-
-  pid_t pid = fork();
-  if (pid < 0)
-    die("Cannot fork: %m");
-
-  if (!pid)
-    {
-      close(pipes[0]);
-      dup2(pipes[1], 1);
-      dup2(pipes[1], 2);
-      close(pipes[1]);
-      execlp("gs", "gs", "-sDEVICE=bbox", "-dSAFER", "-dBATCH", "-dNOPAUSE", "-q", in, NULL);
-      die("Cannot execute gs: %m");
-    }
-
-  close(pipes[1]);
-  FILE *f = fdopen(pipes[0], "r");
-  if (!f)
-    die("fdopen failed: %m");
-
-  char line[1024];
-  vector<BBox> bboxes;
-  while (fgets(line, sizeof(line), f))
-    {
-      char *eol = strchr(line, '\n');
-      if (!eol)
-       die("Ghostscript produced too long lines");
-      *eol = 0;
-
-      if (!strncmp(line, "%%HiResBoundingBox: ", 20))
-       {
-         double x1, y1, x2, y2;
-         if (sscanf(line+20, "%lf%lf%lf%lf", &x1, &y1, &x2, &y2) != 4)
-           die("Cannot parse Ghostscript output: %s", line);
-         bboxes.push_back(BBox(x1, y1, x2, y2));
-       }
-      else if (line[0] != '%')
-       fprintf(stderr, "%s\n", line);
-    }
-  fclose(f);
-
-  int stat;
-  if (waitpid(pid, &stat, 0) < 0)
-    die("wait failed: %m");
-  if (!WIFEXITED(stat) || WEXITSTATUS(stat))
-    die("Ghostscript failed");
-
-  return bboxes;
-}
diff --git a/jam.h b/jam.h
index 7fd316a8f4a049f7d3e944861b43b0e8b38aeb84..ca5dd9c0472f9a7e29dc91e0431eaeb826531682 100644 (file)
--- a/jam.h
+++ b/jam.h
@@ -1,14 +1,23 @@
+/*
+ *     PaperJam -- Common declarations
+ *
+ *     (c) 2018 Martin Mares <mj@ucw.cz>
+ */
+
 #include <vector>
 #include <list>
 
 using namespace std;
 
-#include "pdf-tools.h"
-
 typedef unsigned int uint;
 
 #define NONRET __attribute__((noreturn))
 #define UNUSED __attribute__((unused))
+#define FORMAT_CHECK(x,y,z) __attribute__((format(x,y,z)))
+
+#include "pdf-tools.h"
+
+/*** Representation of commands ***/
 
 struct pipeline;
 struct cmd;
@@ -99,19 +108,31 @@ struct pipeline {
   vector<pipeline_branch *> branches;
 };
 
+/*** Modules ***/
+
 // paperjam.cc
 
-vector<page *> run_command_list(list<cmd *> &cmds, vector<page *> &pages);
+extern const char *in_name, *out_name;
+extern bool recalc_bbox;
+extern int debug_mode;
+extern int debug_indent;
+
+void debug(const char *msg, ...) FORMAT_CHECK(printf, 1, 2);
+void warn(const char *msg, ...) FORMAT_CHECK(printf, 1, 2);
+void die(const char *msg, ...) FORMAT_CHECK(printf, 1, 2) NONRET;
 
 // parse.cc
 
 void parse(const char *in, list<cmd *> &cmds);
-void help();
+void parser_help();
 
 // cmds.cc
 
 extern const cmd_def cmd_table[];
 
-// gs.cc
+// pdf.cc
 
+void debug_pages(vector<page *> &pages);
+void process(list<cmd *> &cmds);
+vector<page *> run_command_list(list<cmd *> &cmds, vector<page *> &pages);
 vector<BBox> gs_bboxes(const char *in);
index c8e107181d7ad97b1b1bdb195c20e45405a1b6eb..c8e0a4f003a2af2557de026871668970dc590722 100644 (file)
+/*
+ *     PaperJam -- Main program
+ *
+ *     (c) 2018 Martin Mares <mj@ucw.cz>
+ */
+
 #include <cassert>
 #include <cstdarg>
 #include <cstdlib>
 #include <cstdio>
+#include <getopt.h>
 
 #include "jam.h"
 
-#include <qpdf/QPDFWriter.hh>
-
-static QPDF in_pdf;
-static QPDF out_pdf;
-
-string out_context::new_resource(const string type)
-{
-  return "/" + type + to_string(++res_cnt);
-}
+/*** Options ***/
 
-class in_page : public page {
-  QPDFObjectHandle pdf_page;
-  QPDFObjectHandle xobject;
-public:
-  BBox media_box;
-  void render(out_context *out, pdf_matrix xform);
-  in_page(QPDFObjectHandle inpg, int idx);
-};
+const char *in_name;
+const char *out_name;
+bool recalc_bbox;
+int debug_mode;
+int debug_indent;
 
-in_page::in_page(QPDFObjectHandle inpg, int idx)
-{
-  pdf_page = inpg;
-  xobject = QPDFObjectHandle::newNull();
-  index = idx;
-
-  media_box = BBox(inpg.getKey("/MediaBox"));
-  width = media_box.width();
-  height = media_box.height();
-
-  QPDFObjectHandle art_box = inpg.getKey("/ArtBox");
-  if (art_box.isNull())
-    art_box = inpg.getKey("/CropBox");
-  if (art_box.isNull())
-    bbox = BBox(width, height);
-  else
-    {
-      bbox = BBox(art_box);
-      bbox.x_min -= media_box.x_min;
-      bbox.x_max -= media_box.x_min;
-      bbox.y_min -= media_box.y_min;
-      bbox.y_max -= media_box.y_min;
-    }
-}
+/*** Messages ***/
 
-void in_page::render(out_context *out, pdf_matrix xform)
-{
-  // Convert page to xobject
-  if (xobject.isNull())
-    xobject = out_pdf.makeIndirectObject( page_to_xobject(&out_pdf, out_pdf.copyForeignObject(pdf_page)) );
-  string xobj_res = out->new_resource("XO");
-  out->xobjects.replaceKey(xobj_res, xobject);
-
-  pdf_matrix m;
-  m.shift(-media_box.x_min, -media_box.y_min);
-  m.concat(xform);
-
-  out->contents += "q " + m.to_string() + " cm " + xobj_res + " Do Q ";
-}
-
-static void debug_pages(vector<page *> &pages)
+void debug(const char *msg, ...)
 {
   if (!debug_mode)
     return;
-
-  for (auto pg: pages)
-    debug("Page #%d: media[%.3f %.3f] bbox[%.3f %.3f %.3f %.3f]",
-      pg->index,
-      pg->width, pg->height,
-      pg->bbox.x_min, pg->bbox.y_min, pg->bbox.x_max, pg->bbox.y_max);
+  va_list args;
+  va_start(args, msg);
+  fprintf(stderr, "%*s", debug_indent, "");
+  vfprintf(stderr, msg, args);
+  fputc('\n', stderr);
+  va_end(args);
 }
 
-vector<page *> run_command_list(list<cmd *> &cmds, vector<page *> &pages)
+void warn(const char *msg, ...)
 {
-  debug("# Input");
-  debug_pages(pages);
-
-  for (auto c: cmds)
-    {
-      debug("# Executing %s", c->def->name);
-      debug_indent += 4;
-      pages = c->exec->process(pages);
-      debug_indent -= 4;
-      debug_pages(pages);
-    }
-
-  return pages;
+  va_list args;
+  va_start(args, msg);
+  fprintf(stderr, "Warning: ");
+  vfprintf(stderr, msg, args);
+  fputc('\n', stderr);
+  va_end(args);
 }
 
-static void find_bboxes(vector<page *> &pages, const char *in_name)
+void die(const char *msg, ...)
 {
-  vector<BBox> bboxes = gs_bboxes(in_name);
-  if (pages.size() != bboxes.size())
-    die("Ghostscript failed to produce the right number of bboxes");
-
-  for (size_t i=0; i<pages.size(); i++)
-    pages[i]->bbox = bboxes[i];
+  va_list args;
+  va_start(args, msg);
+  vfprintf(stderr, msg, args);
+  fputc('\n', stderr);
+  va_end(args);
+  exit(1);
 }
 
-static void process(list<cmd *> &cmds, const char *in_name, const char *out_name)
-{
-  in_pdf.processFile(in_name);
-  in_pdf.pushInheritedAttributesToPage();
-  out_pdf.emptyPDF();
-
-  vector<QPDFObjectHandle> const &in_pages = in_pdf.getAllPages();
-  vector<page *> pages;
-
-  QPDFObjectHandle page_copy = out_pdf.copyForeignObject(in_pages[0]);
+/*** Arguments ***/
 
-  int cnt = 0;
-  for (auto inpg: in_pages)
-    pages.push_back(new in_page(inpg, ++cnt));
-
-  find_bboxes(pages, in_name);
+enum opt {
+  OPT_HELP = 256,
+  OPT_VERSION,
+};
 
-  pages = run_command_list(cmds, pages);
+static const struct option long_opts[] = {
+  { "debug",   0, 0, 'd' },
+  { "help",    0, 0, OPT_HELP },
+  { "version", 0, 0, OPT_VERSION },
+  { 0,         0, 0, 0 }
+};
 
-  for (auto pg: pages)
-    {
-      out_context out;
-      out.resources = QPDFObjectHandle::newDictionary();
-      out.resources.replaceKey("/ProcSet", QPDFObjectHandle::parse("[/PDF]"));
-      out.xobjects = QPDFObjectHandle::newDictionary();
-      out.resources.replaceKey("/XObject", out.xobjects);
-      pg->render(&out, pdf_matrix());
-
-      QPDFObjectHandle contents = QPDFObjectHandle::newStream(&out_pdf, out.contents);
-
-      // Create the page object
-      QPDFObjectHandle out_page = out_pdf.makeIndirectObject(QPDFObjectHandle::newDictionary());
-      out_page.replaceKey("/Type", QPDFObjectHandle::newName("/Page"));
-      out_page.replaceKey("/MediaBox", BBox(pg->width, pg->height).to_array());
-      // FIXME:
-      // out_page.replaceKey("/CropBox", pg->bbox.to_array());
-      out_page.replaceKey("/Contents", contents);
-      out_page.replaceKey("/Resources", out.resources);
-      out_pdf.addPage(out_page, false);
-    }
+static const char short_opts[] = "bd";
 
-  // Produce info dictionary
-  QPDFObjectHandle trailer = out_pdf.getTrailer();
-  QPDFObjectHandle info = trailer.getKey("/Info");
-  if (info.isNull())
-    {
-      info = QPDFObjectHandle::newDictionary();
-      trailer.replaceKey("/Info", info);
-    }
-  else
-    assert(info.isDictionary());
-  // FIXME: More meta-data
-  info.replaceKey("/Producer", unicode_string("PaperJam"));
-
-  // Write the output file
-  QPDFWriter writer(out_pdf, out_name);
-  writer.write();
+static void usage()
+{
+  printf("Usage: paperjam [<options>] <commands> <in> <out>\n\
+\n\
+Options:\n\
+-b, --bbox             Recalculate bounding boxes\n\
+-d, --debug            Show debugging messages\n\
+\n\
+Commands: (FIXME)\n\
+");
+  parser_help();
 }
 
 int main(int argc, char **argv)
 {
-  if (argc <= 1 || argc > 1 && !strcmp(argv[1], "--help"))
-    {
-      help();
-      return 0;
-    }
-
-  if (argc != 4)
-    {
-      fprintf(stderr, "Usage: paperjam <commands> <input> <output>\n");
-      return 1;
-    }
-
-  debug_mode = 100;
+  int c;
+  while ((c = getopt_long(argc, argv, short_opts, long_opts, NULL)) >= 0)
+    switch (c)
+      {
+      case 'b':
+       recalc_bbox = 1;
+       break;
+      case 'd':
+       debug_mode++;
+       break;
+      case OPT_VERSION:
+       printf("This is paperjam with no version yet.\n");      // FIXME
+       return 0;
+      case OPT_HELP:
+       usage();
+       return 0;
+      default:
+       exit(1);
+      }
+
+  if (optind + 3 != argc)
+    die("Exactly three positional parameters should be given");
 
   list<cmd *> cmds;
-  parse(argv[1], cmds);
+  parse(argv[optind], cmds);
+  in_name = argv[optind+1];
+  out_name = argv[optind+2];
 
   try
     {
-      process(cmds, argv[2], argv[3]);
+      process(cmds);
     }
   catch (exception& e)
     {
index d3d92cd083df81a98bef9f263b85869bcb46b992..48a32c717c0318a806ce6edfb5eb077a0a4dc06d 100644 (file)
--- a/parse.cc
+++ b/parse.cc
@@ -1,3 +1,9 @@
+/*
+ *     PaperJam -- Command parser
+ *
+ *     (c) 2018 Martin Mares <mj@ucw.cz>
+ */
+
 #include <cassert>
 #include <cstdarg>
 #include <cstdlib>
@@ -483,7 +489,7 @@ void parse(const char *in, list<cmd *> &cmds)
   instantiate(cmds);
 }
 
-void help()
+void parser_help()
 {
   for (int i=0; cmd_table[i].name; i++)
     {
index c170d8ae9a3b0fc2dfbddadfb249ca59d9c77ab0..136fe99d72b2037fea0e19ddd82b9a38fdd5a774 100644 (file)
@@ -4,69 +4,16 @@
  *     (c) 2018 Martin Mares <mj@ucw.cz>
  */
 
-#include <cstdarg>
 #include <cstdio>
 #include <cstdlib>
 
 #include <iconv.h>
 
-using namespace std;
-
-#include "pdf-tools.h"
+#include "jam.h"
 
 #include <qpdf/QUtil.hh>
 #include <qpdf/Pl_Concatenate.hh>
 
-/*** Messages ***/
-
-int debug_mode;
-int debug_indent;
-
-void debug(const char *msg, ...)
-{
-       if (!debug_mode)
-               return;
-       va_list args;
-       va_start(args, msg);
-       fprintf(stderr, "%*s", debug_indent, "");
-       vfprintf(stderr, msg, args);
-       fputc('\n', stderr);
-       va_end(args);
-}
-
-void warn(const char *msg, ...)
-{
-       va_list args;
-       va_start(args, msg);
-       fprintf(stderr, "WARNING: ");
-       vfprintf(stderr, msg, args);
-       fputc('\n', stderr);
-       va_end(args);
-}
-
-void die(const char *msg, ...)
-{
-       va_list args;
-       va_start(args, msg);
-       fprintf(stderr, "ERROR: ");
-       vfprintf(stderr, msg, args);
-       fputc('\n', stderr);
-       va_end(args);
-       exit(1);
-}
-
-void bad(const char *msg, ...)
-{
-       va_list args;
-       va_start(args, msg);
-       char buf[1024];
-       vsnprintf(buf, sizeof(buf), msg, args);
-       va_end(args);
-
-       printf("error: %s\n", buf);
-       die("BAD: %s", buf);
-}
-
 /*** Transformation matrices ***/
 
 // Construct string representation of a transformation matrix
@@ -119,6 +66,14 @@ bool BBox::parse(QPDFObjectHandle h)
        return true;
 }
 
+BBox::BBox(QPDFObjectHandle box) {
+       if (!parse(box)) {
+               warn("Invalid bounding box, falling back to A4");
+               x_min = 0, x_max = A4_WIDTH;
+               y_min = 0, y_max = A4_HEIGHT;
+       }
+}
+
 void BBox::transform(pdf_matrix &m)
 {
        m.apply(&x_min, &y_min);
index 9563ec024ceb132c7a1fb457b83f04c8b61bada6..18aac2476761ef8ed7ff31674d154eb1fe60e272 100644 (file)
 
 /*** Basic macros and constants ***/
 
-#define UNUSED __attribute__((unused))
-#define FORMAT_CHECK(x,y,z) __attribute__((format(x,y,z)))
-#define NONRET __attribute__((noreturn))
-
 #define A4_WIDTH 595
 #define A4_HEIGHT 842
 static const double mm = 72/25.4;
 
-/*** Messages ***/
-
-void debug(const char *msg, ...) FORMAT_CHECK(printf, 1, 2);
-void warn(const char *msg, ...) FORMAT_CHECK(printf, 1, 2);
-void die(const char *msg, ...) FORMAT_CHECK(printf, 1, 2) NONRET;
-void bad(const char *msg, ...) FORMAT_CHECK(printf, 1, 2) NONRET;
-
-extern int debug_mode;
-extern int debug_indent;
-
 /*** Transformation matrices ***/
 
 struct pdf_matrix {
@@ -137,13 +123,7 @@ struct BBox {
                x_min = 0, x_max = x;
                y_min = 0, y_max = y;
        }
-       BBox(QPDFObjectHandle box) {
-               if (!parse(box)) {
-                       warn("Invalid bounding box, falling back to A4");
-                       x_min = 0, x_max = A4_WIDTH;
-                       y_min = 0, y_max = A4_HEIGHT;
-               }
-       }
+       BBox(QPDFObjectHandle box);
        QPDFObjectHandle to_array();
        string to_rect();
        double width() { return x_max - x_min; }
@@ -159,4 +139,3 @@ QPDFObjectHandle unicode_string(std::string s);
 QPDFObjectHandle page_to_xobject(QPDF *out, QPDFObjectHandle page);
 
 #endif
-
diff --git a/pdf.cc b/pdf.cc
new file mode 100644 (file)
index 0000000..daf68c2
--- /dev/null
+++ b/pdf.cc
@@ -0,0 +1,230 @@
+/*
+ *     PaperJam -- Low-level handling of PDFs
+ *
+ *     (c) 2018 Martin Mares <mj@ucw.cz>
+ */
+
+#include <cassert>
+#include <cstdlib>
+#include <cstdio>
+#include <unistd.h>
+#include <sys/wait.h>
+
+#include "jam.h"
+
+#include <qpdf/QPDFWriter.hh>
+
+static QPDF in_pdf;
+static QPDF out_pdf;
+
+static void do_recalc_bbox(vector<page *> &pages, const char *in_name);
+
+string out_context::new_resource(const string type)
+{
+  return "/" + type + to_string(++res_cnt);
+}
+
+class in_page : public page {
+  QPDFObjectHandle pdf_page;
+  QPDFObjectHandle xobject;
+public:
+  BBox media_box;
+  void render(out_context *out, pdf_matrix xform);
+  in_page(QPDFObjectHandle inpg, int idx);
+};
+
+in_page::in_page(QPDFObjectHandle inpg, int idx)
+{
+  pdf_page = inpg;
+  xobject = QPDFObjectHandle::newNull();
+  index = idx;
+
+  media_box = BBox(inpg.getKey("/MediaBox"));
+  width = media_box.width();
+  height = media_box.height();
+
+  QPDFObjectHandle art_box = inpg.getKey("/ArtBox");
+  if (art_box.isNull())
+    art_box = inpg.getKey("/CropBox");
+  if (art_box.isNull())
+    bbox = BBox(width, height);
+  else
+    {
+      bbox = BBox(art_box);
+      bbox.x_min -= media_box.x_min;
+      bbox.x_max -= media_box.x_min;
+      bbox.y_min -= media_box.y_min;
+      bbox.y_max -= media_box.y_min;
+    }
+}
+
+void in_page::render(out_context *out, pdf_matrix xform)
+{
+  // Convert page to xobject
+  if (xobject.isNull())
+    xobject = out_pdf.makeIndirectObject( page_to_xobject(&out_pdf, out_pdf.copyForeignObject(pdf_page)) );
+  string xobj_res = out->new_resource("XO");
+  out->xobjects.replaceKey(xobj_res, xobject);
+
+  pdf_matrix m;
+  m.shift(-media_box.x_min, -media_box.y_min);
+  m.concat(xform);
+
+  out->contents += "q " + m.to_string() + " cm " + xobj_res + " Do Q ";
+}
+
+void debug_pages(vector<page *> &pages)
+{
+  if (!debug_mode)
+    return;
+
+  for (auto pg: pages)
+    debug("Page #%d: media[%.3f %.3f] bbox[%.3f %.3f %.3f %.3f]",
+      pg->index,
+      pg->width, pg->height,
+      pg->bbox.x_min, pg->bbox.y_min, pg->bbox.x_max, pg->bbox.y_max);
+}
+
+vector<page *> run_command_list(list<cmd *> &cmds, vector<page *> &pages)
+{
+  debug("# Input");
+  debug_pages(pages);
+
+  for (auto c: cmds)
+    {
+      debug("# Executing %s", c->def->name);
+      debug_indent += 4;
+      pages = c->exec->process(pages);
+      debug_indent -= 4;
+      debug_pages(pages);
+    }
+
+  return pages;
+}
+
+void process(list<cmd *> &cmds)
+{
+  in_pdf.processFile(in_name);
+  in_pdf.pushInheritedAttributesToPage();
+  out_pdf.emptyPDF();
+
+  vector<QPDFObjectHandle> const &in_pages = in_pdf.getAllPages();
+  vector<page *> pages;
+
+  QPDFObjectHandle page_copy = out_pdf.copyForeignObject(in_pages[0]);
+
+  int cnt = 0;
+  for (auto inpg: in_pages)
+    pages.push_back(new in_page(inpg, ++cnt));
+
+  if (recalc_bbox)
+    do_recalc_bbox(pages, in_name);
+
+  pages = run_command_list(cmds, pages);
+
+  for (auto pg: pages)
+    {
+      out_context out;
+      out.resources = QPDFObjectHandle::newDictionary();
+      out.resources.replaceKey("/ProcSet", QPDFObjectHandle::parse("[/PDF]"));
+      out.xobjects = QPDFObjectHandle::newDictionary();
+      out.resources.replaceKey("/XObject", out.xobjects);
+      pg->render(&out, pdf_matrix());
+
+      QPDFObjectHandle contents = QPDFObjectHandle::newStream(&out_pdf, out.contents);
+
+      // Create the page object
+      QPDFObjectHandle out_page = out_pdf.makeIndirectObject(QPDFObjectHandle::newDictionary());
+      out_page.replaceKey("/Type", QPDFObjectHandle::newName("/Page"));
+      out_page.replaceKey("/MediaBox", BBox(pg->width, pg->height).to_array());
+      // FIXME:
+      // out_page.replaceKey("/CropBox", pg->bbox.to_array());
+      out_page.replaceKey("/Contents", contents);
+      out_page.replaceKey("/Resources", out.resources);
+      out_pdf.addPage(out_page, false);
+    }
+
+  // Produce info dictionary
+  QPDFObjectHandle trailer = out_pdf.getTrailer();
+  QPDFObjectHandle info = trailer.getKey("/Info");
+  if (info.isNull())
+    {
+      info = QPDFObjectHandle::newDictionary();
+      trailer.replaceKey("/Info", info);
+    }
+  else
+    assert(info.isDictionary());
+  // FIXME: More meta-data
+  info.replaceKey("/Producer", unicode_string("PaperJam"));
+
+  // Write the output file
+  QPDFWriter writer(out_pdf, out_name);
+  writer.write();
+}
+
+/*** Re-calculation of bboxes ***/
+
+vector<BBox> gs_bboxes(const char *in)
+{
+  int pipes[2];
+  if (pipe(pipes) < 0)
+    die("Cannot create pipe: %m");
+
+  pid_t pid = fork();
+  if (pid < 0)
+    die("Cannot fork: %m");
+
+  if (!pid)
+    {
+      close(pipes[0]);
+      dup2(pipes[1], 1);
+      dup2(pipes[1], 2);
+      close(pipes[1]);
+      execlp("gs", "gs", "-sDEVICE=bbox", "-dSAFER", "-dBATCH", "-dNOPAUSE", "-q", in, NULL);
+      die("Cannot execute gs: %m");
+    }
+
+  close(pipes[1]);
+  FILE *f = fdopen(pipes[0], "r");
+  if (!f)
+    die("fdopen failed: %m");
+
+  char line[1024];
+  vector<BBox> bboxes;
+  while (fgets(line, sizeof(line), f))
+    {
+      char *eol = strchr(line, '\n');
+      if (!eol)
+       die("Ghostscript produced too long lines");
+      *eol = 0;
+
+      if (!strncmp(line, "%%HiResBoundingBox: ", 20))
+       {
+         double x1, y1, x2, y2;
+         if (sscanf(line+20, "%lf%lf%lf%lf", &x1, &y1, &x2, &y2) != 4)
+           die("Cannot parse Ghostscript output: %s", line);
+         bboxes.push_back(BBox(x1, y1, x2, y2));
+       }
+      else if (line[0] != '%')
+       fprintf(stderr, "%s\n", line);
+    }
+  fclose(f);
+
+  int stat;
+  if (waitpid(pid, &stat, 0) < 0)
+    die("wait failed: %m");
+  if (!WIFEXITED(stat) || WEXITSTATUS(stat))
+    die("Ghostscript failed");
+
+  return bboxes;
+}
+
+static void do_recalc_bbox(vector<page *> &pages, const char *in_name)
+{
+  vector<BBox> bboxes = gs_bboxes(in_name);
+  if (pages.size() != bboxes.size())
+    die("Ghostscript failed to produce the right number of bboxes");
+
+  for (size_t i=0; i<pages.size(); i++)
+    pages[i]->bbox = bboxes[i];
+}