|
| 1 | +.. include:: header.rst |
| 2 | + |
| 3 | +.. _CompressingFiles: |
| 4 | + |
| 5 | +============================== |
| 6 | +Compressing Files |
| 7 | +============================== |
| 8 | + |
| 9 | + |
| 10 | +PyMuPDF & Compression |
| 11 | +------------------------- |
| 12 | + |
| 13 | +There are various ways to reduce the size of a PDF file and a variety of options for doing so with the :meth:`Document.save()` method parameters. |
| 14 | + |
| 15 | +By using the `mupdf_explored <https://mupdf.readthedocs.io/en/1.28.2/cookbook/mupdf-explored.html>`_ PDF as a control file let's look at the effect of various save-time parameters on the file size of a PDF document. |
| 16 | + |
| 17 | +The original file is 1.8 MB in size. Let's see what happens when we save it with various optional parameters defined. |
| 18 | + |
| 19 | + |
| 20 | +`use_objstms` |
| 21 | +~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 22 | + |
| 23 | +This boolean option packs object definitions into compressible streams. |
| 24 | + |
| 25 | +**Example** |
| 26 | + |
| 27 | +.. code-block:: python |
| 28 | +
|
| 29 | + import pymupdf |
| 30 | +
|
| 31 | + doc = pymupdf.open("mupdf_explored.pdf") |
| 32 | +
|
| 33 | + doc.save( |
| 34 | + "output.pdf", |
| 35 | + use_objstms=True, # pack object definitions into compressible streams |
| 36 | + ) |
| 37 | + doc.close() |
| 38 | +
|
| 39 | +
|
| 40 | +The result is a file size of 1.5 MB. However, we can achieve a better result by using the `deflate` parameter to compress the uncompressed streams. |
| 41 | + |
| 42 | + |
| 43 | + |
| 44 | +`deflate` |
| 45 | +~~~~~~~~~~~~~~~~~~~~ |
| 46 | + |
| 47 | +By setting this option we can compress uncompressed streams. |
| 48 | + |
| 49 | +Availabe options are: |
| 50 | + |
| 51 | +.. list-table:: |
| 52 | + :header-rows: 1 |
| 53 | + |
| 54 | + * - **Value** |
| 55 | + - **Meaning** |
| 56 | + * - `0` |
| 57 | + - No compression (this is the default) |
| 58 | + * - `1` |
| 59 | + - Use standard `Flate compression <https://en.wikipedia.org/wiki/Deflate>`_ |
| 60 | + * - `2` |
| 61 | + - Use `Brotli compression <https://en.wikipedia.org/wiki/Brotli>`_ (slowest, smallest output, but experimental and unsupported in many tools and viewers) [1]_ |
| 62 | + |
| 63 | + |
| 64 | +Used in combination with `use_objstms` to pack object definitions into compressible streams we can achieve even better results. |
| 65 | + |
| 66 | +Flate |
| 67 | +""""""""""""""" |
| 68 | + |
| 69 | +**Flate Example** |
| 70 | + |
| 71 | +.. code-block:: python |
| 72 | +
|
| 73 | + import pymupdf |
| 74 | +
|
| 75 | + doc = pymupdf.open("mupdf_explored.pdf") |
| 76 | +
|
| 77 | + doc.save( |
| 78 | + "output.pdf", |
| 79 | + use_objstms=True, # pack object definitions into compressible streams |
| 80 | + deflate=1, # compress uncompressed streams with Flate compression |
| 81 | + ) |
| 82 | + doc.close() |
| 83 | +
|
| 84 | +The result is a file size of 903 KB. |
| 85 | + |
| 86 | + |
| 87 | +.. note:: |
| 88 | + |
| 89 | + :meth:`Document.ez_save()` automatically applies these paramters for you, so the same result can be achieved with: |
| 90 | + |
| 91 | + .. code-block:: python |
| 92 | +
|
| 93 | + doc.ez_save("output.pdf") |
| 94 | +
|
| 95 | +Brotli |
| 96 | +""""""""""""""" |
| 97 | + |
| 98 | +**Brotli Example** |
| 99 | + |
| 100 | +.. warning:: |
| 101 | + |
| 102 | + Brotli is experimental and unsupported in many tools and viewers. [1]_ |
| 103 | + |
| 104 | + |
| 105 | +.. code-block:: python |
| 106 | +
|
| 107 | + import pymupdf |
| 108 | +
|
| 109 | + doc = pymupdf.open("mupdf_explored.pdf") |
| 110 | +
|
| 111 | + doc.save( |
| 112 | + "output.pdf", |
| 113 | + use_objstms=True, # pack object definitions into compressible streams |
| 114 | + deflate=2, # compress uncompressed streams with Brotli compression |
| 115 | + ) |
| 116 | + doc.close() |
| 117 | +
|
| 118 | +The result is a file size of 863 KB. |
| 119 | + |
| 120 | +Brotli is expected to **yield meaningful differences** on text- and vector-heavy documents, and close to |
| 121 | +**none** on scanned or photo-heavy ones. |
| 122 | + |
| 123 | +`compression_effort` |
| 124 | +''''''''''''''''''''' |
| 125 | + |
| 126 | +When using Brotli (`deflate=2`) by setting a further parameter `compression_effort` we can control how hard MuPDF works when compressing stream data. It trades CPU time for file size and *never changes what the document looks like*. |
| 127 | + |
| 128 | +.. code-block:: python |
| 129 | +
|
| 130 | + import pymupdf |
| 131 | +
|
| 132 | + doc = pymupdf.open("mupdf_explored.pdf") |
| 133 | +
|
| 134 | + doc.save( |
| 135 | + "output.pdf", |
| 136 | + use_objstms=True, # pack object definitions into compressible streams |
| 137 | + deflate=2, # compress uncompressed streams with Brotli compression |
| 138 | + compression_effort=100, # ask Brotli to work hard at it |
| 139 | + ) |
| 140 | + doc.close() |
| 141 | +
|
| 142 | +The result now is a file size of 834 KB. |
| 143 | + |
| 144 | + |
| 145 | +`compression_effort`: What it does and does not affect |
| 146 | +''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' |
| 147 | + |
| 148 | +It is an `int`, not a `bool`! |
| 149 | + |
| 150 | +.. list-table:: |
| 151 | + :header-rows: 1 |
| 152 | + |
| 153 | + * - **Value** |
| 154 | + - **Meaning** |
| 155 | + * - `0` |
| 156 | + - Zero effort (this is the default) |
| 157 | + * - `1` |
| 158 | + - Minimum effort — fastest, largest output |
| 159 | + * - `100` |
| 160 | + - Maximum effort — slowest, smallest output |
| 161 | + |
| 162 | + |
| 163 | +This is the single most common mistake: |
| 164 | + |
| 165 | +.. code-block:: python |
| 166 | +
|
| 167 | + # WRONG — True == 1 == minimum effort |
| 168 | + doc.save("out.pdf", use_objstms=True, deflate=2, compression_effort=True) |
| 169 | + |
| 170 | + # CORRECT - maximum effort defined |
| 171 | + doc.save("out.pdf", use_objstms=True, deflate=2, compression_effort=100) |
| 172 | +
|
| 173 | +Because Python's `True` is `1`, passing a boolean silently selects an effort setting of `1`. |
| 174 | +No warning is raised, and the file simply comes out larger than |
| 175 | +you expected. |
| 176 | + |
| 177 | +Artifex published effort guidance in `Brotli is Here! <https://artifex.com/blog/brotli-is-here>`_ |
| 178 | +for the equivalent `-e` flag on `mutool clean`: |
| 179 | + |
| 180 | +- **40** — roughly the speed of default Flate compression. |
| 181 | +- **75** — roughly the speed of maximum Flate compression. |
| 182 | +- **100** — squeezes out everything available, at a noticeably longer runtime. |
| 183 | + |
| 184 | +That guidance is written in the context of Brotli compression, so treat the exact |
| 185 | +numbers as indicative. |
| 186 | + |
| 187 | + |
| 188 | +`compression_effort` tunes the compressor applied to stream objects as the file is |
| 189 | +written. It does not decide *whether* streams are compressed using Brotli — that is `deflate=2` |
| 190 | +— and it does not repack object definitions — that is `use_objstms=True`. Ensures that you have correctly set those two parameters before you start tuning `compression_effort`. |
| 191 | + |
| 192 | +It has an effect on: |
| 193 | + |
| 194 | +- content streams (text and vector graphics), |
| 195 | +- font programs and other uncompressed binary streams, |
| 196 | +- object streams produced by `use_objstms=True`. |
| 197 | + |
| 198 | +It has little or no effect on: |
| 199 | + |
| 200 | +- images already stored in a compressed format (JPEG, JPEG2000, JBIG2). Re-running |
| 201 | + a general-purpose compressor over them buys nothing. Use |
| 202 | + `Document.rewrite_images()` for those. |
| 203 | +- documents whose bulk is unreferenced objects. Use `garbage=3` or `garbage=4`. |
| 204 | + |
| 205 | + |
| 206 | +`garbage` |
| 207 | +~~~~~~~~~~~~~~~~~~~~ |
| 208 | + |
| 209 | +Documents with unreferenced objects can be reduced in size by setting the `garbage` parameter to `3` or `4`. This will de-duplicate and drop unreferenced objects. |
| 210 | + |
| 211 | +.. list-table:: |
| 212 | + :header-rows: 1 |
| 213 | + |
| 214 | + * - **Value** |
| 215 | + - **Meaning** |
| 216 | + * - `0` |
| 217 | + - none (default) |
| 218 | + * - `1` |
| 219 | + - remove unused (unreferenced) objects. |
| 220 | + * - `2` |
| 221 | + - in addition to 1, compact the :data:`xref` table. |
| 222 | + * - `3` |
| 223 | + - in addition to 2, merge duplicate objects. |
| 224 | + * - `4` |
| 225 | + - in addition to 3, check :data:`stream` objects for duplication. This may be slow because such data are typically large. |
| 226 | + |
| 227 | + |
| 228 | +How much is saved here depends on the document, and how much "garbage" it may contain. For example, if we set `garbage=4` we only save a few KB: |
| 229 | + |
| 230 | + |
| 231 | +.. code-block:: python |
| 232 | +
|
| 233 | + import pymupdf |
| 234 | +
|
| 235 | + doc = pymupdf.open("mupdf_explored.pdf") |
| 236 | +
|
| 237 | + doc.save( |
| 238 | + "output.pdf", |
| 239 | + use_objstms=True, # pack object definitions into compressible streams |
| 240 | + deflate=2, # compress uncompressed streams with Brotli compression |
| 241 | + compression_effort=100, # ask Brotli to work hard at it |
| 242 | + garbage=4 # drop unreferenced objects, compact xref table, merge duplicate objects, check streams for duplication |
| 243 | + ) |
| 244 | + doc.close() |
| 245 | +
|
| 246 | +
|
| 247 | +Now we have a resulting PDF with a file size of 831 KB. |
| 248 | + |
| 249 | + |
| 250 | +Results Summary |
| 251 | +-------------------- |
| 252 | + |
| 253 | +.. list-table:: |
| 254 | + :header-rows: 1 |
| 255 | + |
| 256 | + * - **Description** |
| 257 | + - **File size result** |
| 258 | + - **Comments** |
| 259 | + * - Original file |
| 260 | + - 1.8 MB |
| 261 | + - |
| 262 | + * - `use_objstms=True` |
| 263 | + - 1.5 MB |
| 264 | + - |
| 265 | + * - `use_objstms=True`, `deflate=1` |
| 266 | + - 903 KB |
| 267 | + - |
| 268 | + * - `use_objstms=True`, `deflate=2` |
| 269 | + - 863 KB |
| 270 | + - Warning: Brotli is experimental and unsupported in many tools and viewers [1]_ |
| 271 | + * - `use_objstms=True`, `deflate=2`, `compression_effort=100` |
| 272 | + - 834 KB |
| 273 | + - Warning: Brotli is experimental and unsupported in many tools and viewers [1]_ |
| 274 | + * - `use_objstms=True`, `deflate=2`, `compression_effort=100`, `garbage=4` |
| 275 | + - 831 KB |
| 276 | + - Warning: Brotli is experimental and unsupported in many tools and viewers [1]_ |
| 277 | + |
| 278 | +PyMuPDF & Compression Benchmarking |
| 279 | +---------------------------------------- |
| 280 | + |
| 281 | +Gains are corpus-dependent. Measure before paying the CPU cost in a batch job: |
| 282 | + |
| 283 | +.. code-block:: python |
| 284 | +
|
| 285 | + import pathlib |
| 286 | + import time |
| 287 | +
|
| 288 | + import pymupdf |
| 289 | +
|
| 290 | + src = "input.pdf" |
| 291 | + for effort in (0, 40, 75, 100): |
| 292 | + doc = pymupdf.open(src) |
| 293 | + out = f"out_{effort}.pdf" |
| 294 | + start = time.perf_counter() |
| 295 | + doc.save(out, garbage=4, deflate=1, use_objstms=True, |
| 296 | + compression_effort=effort) |
| 297 | + elapsed = time.perf_counter() - start |
| 298 | + doc.close() |
| 299 | + size = pathlib.Path(out).stat().st_size |
| 300 | + print(f"effort={effort:>3} {size/1e6:6.2f} MB {elapsed:5.2f}s") |
| 301 | +
|
| 302 | +See also |
| 303 | +-------------------- |
| 304 | + |
| 305 | +- :meth:`Document.save()` |
| 306 | +- :meth:`Document.ez_save()` |
| 307 | +- :meth:`Document.rewrite_images()` |
| 308 | +- :meth:`Document.subset_fonts()` |
| 309 | +- `Brotli is Here! <https://artifex.com/blog/brotli-is-here>`_ |
| 310 | + |
| 311 | + |
| 312 | + |
| 313 | +.. [1] If you've used Brotli to compress your PDFs and found that they don't work in your favorite viewer, please try `MUPDF GL <https://mupdf.readthedocs.io/en/1.28.2/tools/mupdf-gl.html>`_. If you still find a problem viewing the file then please file an issue on our `Github tracker <https://github.com/pymupdf/PyMuPDF/issues>`_. |
| 314 | +
|
| 315 | +
|
| 316 | +.. include:: footer.rst |
0 commit comments