<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: OneFileKit</title>
    <description>The latest articles on DEV Community by OneFileKit (@onefilekit).</description>
    <link>https://dev.to/onefilekit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4224836%2F7a24d239-cf86-4bc0-ab49-f486913614b4.png</url>
      <title>DEV Community: OneFileKit</title>
      <link>https://dev.to/onefilekit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/onefilekit"/>
    <language>en</language>
    <item>
      <title>Merging PDFs from the Command Line: qpdf, pdfunite, Ghostscript and pdftk Compared</title>
      <dc:creator>OneFileKit</dc:creator>
      <pubDate>Tue, 03 Nov 2026 20:56:55 +0000</pubDate>
      <link>https://dev.to/onefilekit/merging-pdfs-from-the-command-line-qpdf-pdfunite-ghostscript-and-pdftk-compared-2g9h</link>
      <guid>https://dev.to/onefilekit/merging-pdfs-from-the-command-line-qpdf-pdfunite-ghostscript-and-pdftk-compared-2g9h</guid>
      <description>&lt;p&gt;I spend a lot of time around PDF files because I build file tools. One request comes up more than almost any other: "I just need these five PDFs as one file." If you live in a terminal, you don't need a GUI for that. Here are the four command-line tools I reach for, what each one is good at, and where each one can trip you up.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. qpdf: my default
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/qpdf/qpdf" rel="noopener noreferrer"&gt;qpdf&lt;/a&gt; is a structural PDF tool. It rewrites the file without re-rendering pages, so text, vector graphics and image quality stay exactly as they were.&lt;/p&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian/Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;qpdf
&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;qpdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merge whole files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qpdf &lt;span class="nt"&gt;--empty&lt;/span&gt; &lt;span class="nt"&gt;--pages&lt;/span&gt; a.pdf b.pdf c.pdf &lt;span class="nt"&gt;--&lt;/span&gt; merged.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--empty&lt;/code&gt; starts from a blank document, and everything between &lt;code&gt;--pages&lt;/code&gt; and &lt;code&gt;--&lt;/code&gt; is the list of inputs. You can also pick page ranges per file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qpdf &lt;span class="nt"&gt;--empty&lt;/span&gt; &lt;span class="nt"&gt;--pages&lt;/span&gt; report.pdf 1-3 appendix.pdf 2,5-z &lt;span class="nt"&gt;--&lt;/span&gt; out.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;z&lt;/code&gt; means the last page, so &lt;code&gt;5-z&lt;/code&gt; is "page 5 to the end". That range syntax is the main reason qpdf is my default: merging and trimming happen in a single command.&lt;/p&gt;

&lt;p&gt;Watch out for: encrypted inputs. If a file has a user password, pass it with &lt;code&gt;--password=...&lt;/code&gt; right after that file's name in the page list.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. pdfunite: the simplest option
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pdfunite&lt;/code&gt; ships with poppler-utils, which is probably installed already if you use &lt;code&gt;pdftotext&lt;/code&gt; or &lt;code&gt;pdftoppm&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;poppler-utils   &lt;span class="c"&gt;# or: brew install poppler&lt;/span&gt;
pdfunite a.pdf b.pdf c.pdf merged.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last argument is the output. There are no options worth mentioning, and that's the point. It's great in scripts where you just want everything, in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pdfunite &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;chapter-&lt;span class="k"&gt;*&lt;/span&gt;.pdf | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-V&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; book.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sort -V&lt;/code&gt; sorts &lt;code&gt;chapter-10&lt;/code&gt; after &lt;code&gt;chapter-9&lt;/code&gt; rather than after &lt;code&gt;chapter-1&lt;/code&gt;, which plain &lt;code&gt;ls&lt;/code&gt; ordering gets wrong.&lt;/p&gt;

&lt;p&gt;Watch out for: the output filename. If you forget it, pdfunite treats your last input as the output and overwrites it. Keep backups or double-check the argument order. It also can't select page ranges.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Ghostscript: merge and shrink in one pass
&lt;/h2&gt;

&lt;p&gt;Ghostscript doesn't copy pages. It interprets each input and writes a brand-new PDF, which means it can also recompress images while it merges.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gs &lt;span class="nt"&gt;-dBATCH&lt;/span&gt; &lt;span class="nt"&gt;-dNOPAUSE&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-sDEVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pdfwrite &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-sOutputFile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merged.pdf a.pdf b.pdf c.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a preset if you also want a smaller file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gs &lt;span class="nt"&gt;-dBATCH&lt;/span&gt; &lt;span class="nt"&gt;-dNOPAUSE&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-sDEVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pdfwrite &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-dPDFSETTINGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/ebook &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-sOutputFile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merged-small.pdf a.pdf b.pdf c.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/screen&lt;/code&gt; is the most aggressive, &lt;code&gt;/ebook&lt;/code&gt; is a reasonable middle ground, and &lt;code&gt;/printer&lt;/code&gt; and &lt;code&gt;/prepress&lt;/code&gt; keep more image detail.&lt;/p&gt;

&lt;p&gt;Watch out for: side effects of re-rendering. Form fields, some annotations and links may not survive, and fonts can be re-embedded or subset differently. Ghostscript is also much slower than qpdf on big files. Use it when you want to merge and compress in one go, not for a plain merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. pdftk: the classic
&lt;/h2&gt;

&lt;p&gt;pdftk has been around for a long time, and plenty of old scripts and Stack Overflow answers use it. On most distros today you'll get &lt;code&gt;pdftk-java&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;pdftk-java
pdftk a.pdf b.pdf c.pdf &lt;span class="nb"&gt;cat &lt;/span&gt;output merged.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It uses handles for page selection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pdftk &lt;span class="nv"&gt;A&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;report.pdf &lt;span class="nv"&gt;B&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;appendix.pdf &lt;span class="nb"&gt;cat &lt;/span&gt;A1-3 B2 B5-end output out.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch out for: availability. It isn't in Homebrew core, and the Java dependency is heavy if all you want is a merge. If you're starting fresh, qpdf does the same job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Page ranges&lt;/th&gt;
&lt;th&gt;Keeps original quality&lt;/th&gt;
&lt;th&gt;Can shrink output&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qpdf&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (structural only)&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pdfunite&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghostscript&lt;/td&gt;
&lt;td&gt;Via &lt;code&gt;-dFirstPage/-dLastPage&lt;/code&gt; per run&lt;/td&gt;
&lt;td&gt;Re-renders&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Slow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pdftk&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A small script I actually use
&lt;/h2&gt;

&lt;p&gt;This merges every PDF in a folder in natural order, then checks the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.pdf&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mapfile&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; files &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.pdf | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;$"&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-V&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
qpdf &lt;span class="nt"&gt;--empty&lt;/span&gt; &lt;span class="nt"&gt;--pages&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
qpdf &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK: &lt;/span&gt;&lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; files -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;qpdf --check&lt;/code&gt; is worth running after any merge. It catches broken cross-reference tables before you send the file to someone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the terminal isn't an option
&lt;/h2&gt;

&lt;p&gt;Not everyone you help will have a shell, Homebrew or apt. When a colleague or family member asks me to merge PDFs, installing qpdf on their laptop is overkill. For that case I built a free browser tool that needs no sign-up: &lt;a href="https://onefilekit.com/merge-pdf" rel="noopener noreferrer"&gt;Merge PDF on OneFileKit&lt;/a&gt;. You drop the files in, reorder them and download a single PDF.&lt;/p&gt;

&lt;p&gt;For anything scripted, repeatable or running on a server, I still use qpdf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain merge, with or without page ranges:&lt;/strong&gt; qpdf&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quickest merge with no options:&lt;/strong&gt; pdfunite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge and make it smaller:&lt;/strong&gt; Ghostscript with &lt;code&gt;-dPDFSETTINGS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintaining old scripts:&lt;/strong&gt; pdftk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What do you use? If there's a tool I've missed, especially on Windows, I'd like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>cli</category>
      <category>linux</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
