<?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: Jimmy Dark</title>
    <description>The latest articles on DEV Community by Jimmy Dark (@funkyzx26).</description>
    <link>https://dev.to/funkyzx26</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%2F4197531%2F6050a1e8-4388-4f58-990b-1b62b689afd8.jpg</url>
      <title>DEV Community: Jimmy Dark</title>
      <link>https://dev.to/funkyzx26</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/funkyzx26"/>
    <language>en</language>
    <item>
      <title>Why PDF compression quietly turns your text into a picture</title>
      <dc:creator>Jimmy Dark</dc:creator>
      <pubDate>Wed, 21 Oct 2026 07:16:14 +0000</pubDate>
      <link>https://dev.to/funkyzx26/why-pdf-compression-quietly-turns-your-text-into-a-picture-4e36</link>
      <guid>https://dev.to/funkyzx26/why-pdf-compression-quietly-turns-your-text-into-a-picture-4e36</guid>
      <description>&lt;p&gt;&lt;em&gt;A 6-page text PDF went from 298.7 KB to 8.9 KB with every one of its 26,652 characters still selectable. The usual way to shrink a PDF gets to 30.5 KB and zero.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;There are two ways to make a PDF smaller. Only one of them keeps the document a document.&lt;/p&gt;

&lt;p&gt;The common way is to render every page to an image and rebuild the file from those images. It always works, it works on scanned documents, and it is what most compressors reach for when a file refuses to shrink. What it also does, quietly, is delete the text layer — the document still looks right on screen, but it is now a photograph of words.&lt;/p&gt;

&lt;p&gt;The other way is to leave the pages alone and shrink the file around them: collapse duplicated objects, and compress the streams that hold the actual content. When that is enough, you get a much smaller file &lt;em&gt;and&lt;/em&gt; the text survives.&lt;/p&gt;

&lt;p&gt;On the document I tested this with, the difference is not subtle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Extractable characters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original&lt;/td&gt;
&lt;td&gt;298.7 KB&lt;/td&gt;
&lt;td&gt;26,652&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lossless repack&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.9 KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26,652 (100%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raster, 150 dpi&lt;/td&gt;
&lt;td&gt;30.5 KB&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raster, 120 dpi&lt;/td&gt;
&lt;td&gt;21.5 KB&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raster, 96 dpi&lt;/td&gt;
&lt;td&gt;15.8 KB&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raster, 72 dpi&lt;/td&gt;
&lt;td&gt;11.2 KB&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Lossless came out &lt;strong&gt;3.4× smaller than the best raster result&lt;/strong&gt;, and it was the only row where the text survived at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why rasterizing costs you the text
&lt;/h2&gt;

&lt;p&gt;A PDF page is a draw-operations list. A paragraph of text is not pixels — it is a font reference and a set of glyph positions, and a reader can select it, search it, copy it, index it, and hand it to an applicant tracking system that parses it.&lt;/p&gt;

&lt;p&gt;Rasterizing replaces that list with a bitmap. The glyph positions are gone, because there is no longer any glyph — only dots. The document looks identical at 100% zoom. Everything a machine wanted to do with it is gone.&lt;/p&gt;

&lt;p&gt;That matters more in some places than others:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An ATS that counts words in an uploaded resume gets nothing.&lt;/li&gt;
&lt;li&gt;A portal that OCRs a transcript to check it is machine-readable has nothing to read.&lt;/li&gt;
&lt;li&gt;Search inside the document stops working.&lt;/li&gt;
&lt;li&gt;Copy-paste for a form that asks you to paste your details fails.&lt;/li&gt;
&lt;li&gt;Any later attempt to compress it &lt;em&gt;again&lt;/em&gt;, properly, is now working on a scanned document.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this fails loudly. The upload succeeds. The failure shows up weeks later as a request for a fresh copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that sent us down the raster path
&lt;/h2&gt;

&lt;p&gt;The interesting part is why a compressor ends up rastering a text PDF in the first place, when the text was never the problem.&lt;/p&gt;

&lt;p&gt;The repack is three steps: load the source, copy the pages into a fresh document, and save with &lt;code&gt;useObjectStreams: true&lt;/code&gt;. Object streams pack the cross-reference table and the object headers into a compressed stream of their own, which is where a lot of the easy savings are.&lt;/p&gt;

&lt;p&gt;What they do &lt;strong&gt;not&lt;/strong&gt; do is touch the page content streams. Those are copied across with whatever encoding the source already had.&lt;/p&gt;

&lt;p&gt;And some producers write content streams with &lt;strong&gt;no &lt;code&gt;/Filter&lt;/code&gt; at all&lt;/strong&gt; — raw, uncompressed. For those files, the repack came out at exactly the same size as the input. A text document that had already been through a "compression" tool, repacked and saved again, would sit there unchanged, miss its target, and get pushed down the raster path — where its text became a 45 dpi image.&lt;/p&gt;

&lt;p&gt;The file was not too big because of its images. It was too big because nobody had compressed the streams that held the text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is about twenty lines
&lt;/h2&gt;

&lt;p&gt;Walk every indirect object in the document. For each raw stream whose dictionary has no &lt;code&gt;/Filter&lt;/code&gt;, deflate it and label it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;flateUnfilteredStreams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PDFRawStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PDFName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PDFNumber&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PDFLib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enumerateIndirectObjects&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;PDFRawStream&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PDFName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Filter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// already encoded&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_DEFLATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;deflateBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// deflate made it worse&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PDFName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Filter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;PDFName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FlateDecode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PDFName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DecodeParms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;         &lt;span class="c1"&gt;// filters no longer apply&lt;/span&gt;
    &lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PDFName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;PDFNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PDFRawStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CompressionStream('deflate')&lt;/code&gt; gives you the deflate in the browser, so this costs nothing to ship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deflateBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CompressionStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deflate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details in that snippet are load-bearing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skip anything that already has a &lt;code&gt;/Filter&lt;/code&gt;.&lt;/strong&gt; Deflating an already-deflated stream grows it, and &lt;code&gt;DecodeParms&lt;/code&gt; (predictors, for the ASCII-hex and LZW-family filters) would no longer describe the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the original when deflate does not help.&lt;/strong&gt; &lt;code&gt;if (!z || z.length &amp;gt;= raw.length) continue;&lt;/code&gt; — compression is not guaranteed to be an improvement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot the object list first.&lt;/strong&gt; &lt;code&gt;context.assign()&lt;/code&gt; replaces entries while you are iterating them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the test document this is the entire difference between 298.7 KB and 8.9 KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you do have to rasterize, put a floor under it
&lt;/h2&gt;

&lt;p&gt;Rasterizing is sometimes the only option left: the page &lt;em&gt;is&lt;/em&gt; a scan, or the streams are already deflated at maximum, or the file is full of enormous images. Then the question is how badly you are allowed to render it.&lt;/p&gt;

&lt;p&gt;The failure mode is a scale ladder that walks quality down until the file fits. On a page with 9 or 10 pt body text, the bottom of that ladder turns the document into a picture of text that nobody can comfortably read — a submission that fits the limit and gets rejected by a human.&lt;/p&gt;

&lt;p&gt;So pages that carry text get a floor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 9-10 pt body text is a blur at that size. 1.3 is the ~94 dpi rung - still&lt;/span&gt;
&lt;span class="c1"&gt;// legible, and still a document rather than a photograph.&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;MIN_TEXT_SCALE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;SCALES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.62&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nx"&gt;ladders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scaleLadder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseVp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keepTextReadable&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;MIN_TEXT_SCALE&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below 1.3 the ladder is simply not available for a text page. An image-heavy page can still go to 0.62, because that is what image-heavy pages are for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that matters most: a bigger file is not a result
&lt;/h2&gt;

&lt;p&gt;Here is the case that changed how I think about it. A 124 KB text PDF, asked for a 100 KB target:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rasterizing it at a &lt;em&gt;readable&lt;/em&gt; 94 dpi produced &lt;strong&gt;190 KB&lt;/strong&gt; — it overshot the target by 90%.&lt;/li&gt;
&lt;li&gt;The lossless repack produced &lt;strong&gt;115 KB&lt;/strong&gt;. It also missed the target.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repack missed. But it was smaller &lt;em&gt;and&lt;/em&gt; it kept the text, so it wins. And if neither path gets under the target and the raster came out larger than the file you started with, hand back the original:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A 124 KB text PDF at a 100 KB target can rasterize to 190 KB at a readable&lt;/span&gt;
&lt;span class="c1"&gt;// 94 dpi; the 115 KB lossless repack is smaller *and* keeps the text, so it&lt;/span&gt;
&lt;span class="c1"&gt;// wins even though it misses the target. A result that grows the file is not&lt;/span&gt;
&lt;span class="c1"&gt;// a result.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;original&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;original&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing a size limit with a readable, selectable document beats hitting it with an unreadable one. A rejected upload can be retried. An unreadable one usually just fails quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check any tool, including this one
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Compress a PDF that contains real selectable text — not a scan.&lt;/li&gt;
&lt;li&gt;Open the result and drag across a sentence. If it selects, the text layer survived.&lt;/li&gt;
&lt;li&gt;Or open a terminal: &lt;code&gt;pdftotext out.pdf - | wc -c&lt;/code&gt;. Zero means it is a picture.&lt;/li&gt;
&lt;li&gt;If the tool claims nothing is uploaded, open DevTools, drop a file, and watch the network tab. A client-side tool makes no request carrying the document.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third step is the only one that is hard to fake.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Method note: sizes above are from one 6-page text PDF (298.7 KB, 26,652 extractable characters), measured on the same file in the same session. Raster figures are JPEG at PyMuPDF's default quality — I could not vary the quality setting through that API, so treat the dpi rows as the variable that was actually controlled. Character counts come from &lt;code&gt;get_text()&lt;/code&gt; on each output.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The tool I built does this in the browser, with no server involved: &lt;a href="https://millfile.com" rel="noopener noreferrer"&gt;millfile.com&lt;/a&gt;. The source is open — &lt;a href="https://github.com/funkyzx26-dot/millfile" rel="noopener noreferrer"&gt;github.com/funkyzx26-dot/millfile&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
