<?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: NullPointerZen</title>
    <description>The latest articles on DEV Community by NullPointerZen (@yue_shu_c621a4a637f22396f).</description>
    <link>https://dev.to/yue_shu_c621a4a637f22396f</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%2F4091461%2F67d2ab23-740e-4763-8279-b58904d35440.png</url>
      <title>DEV Community: NullPointerZen</title>
      <link>https://dev.to/yue_shu_c621a4a637f22396f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yue_shu_c621a4a637f22396f"/>
    <language>en</language>
    <item>
      <title>Sharp but wrong: testing an AI upscaler against an original it never saw</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:04:30 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/sharp-but-wrong-testing-an-ai-upscaler-against-an-original-it-never-saw-7bo</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/sharp-but-wrong-testing-an-ai-upscaler-against-an-original-it-never-saw-7bo</guid>
      <description>&lt;p&gt;Most AI upscaler comparisons you see online are useless, and it took me a while to work out why. They show you "before" and "after" — a blurry thumbnail next to a crisp result. Of course the crisp one wins. What's missing is the only thing that can settle the argument: what the picture actually looked like before anyone shrank it.&lt;/p&gt;

&lt;p&gt;So I ran it the other way around. I made four sharp images first and kept them as ground truth, then downscaled each to a quarter of its size with plain interpolation. The small versions went into the upscaler, 4x, which lands exactly back on the original dimensions. Now every output pixel has a correct answer to be compared against. Two terms, in case you don't live in this world: interpolation is the old way of enlarging, where new pixels are averaged from their neighbours, so nothing is added and the result is soft. AI upscaling has a model guess what the detail should have been and paint it in, so the result is crisp. PSNR is just the pixel-by-pixel distance between two images, in dB, higher meaning closer to the truth.&lt;/p&gt;

&lt;p&gt;The samples were a fictional product shot I drew with a script (invented brand, invented batch code), a fictional order form screenshot, a line-art icon, and a CC0 airport photo from Wikimedia Commons — that last one isn't mine. I ran everything through the AI upscaler on ImgIng (&lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt; ), all five model tiers at 2x and 4x, forty runs without a single failure. The practical reason I picked it: the work happens inside the browser, and with the Network panel open I could confirm the images never left the machine, so hammering it forty times cost nothing and leaked nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sharpest output was the wrong one
&lt;/h2&gt;

&lt;p&gt;The batch code on the product packaging reads A7K-2291 in the truth image. At 20px in the original it survives as five pixel rows in the thumbnail. Lanczos interpolation gives you a smeared but perfectly legible A7K-2291. All five AI tiers agreed on A7K(2201 — the tail of the 9 flattened into a 0, the hyphen drawn as an opening bracket. On the order form, a total of 6,491.40 came back looking like 0,101.40 in two of the tiers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Four panels of the same batch code: truth, thumbnail stretched, Lanczos reading A7K-2291, AI reading A7K(2201. The characters in frame are Chinese because I generated the sample myself" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scores tell the same story once you pick the right one. On the two samples containing small text, the best AI tier scored &lt;em&gt;below&lt;/em&gt; plain interpolation: 26.48 dB against 27.85 on the product shot, 22.88 against 23.62 on the document. Meanwhile SSIM, which rewards local contrast and structure rather than pixel accuracy, ranked AI first on all four samples. It doesn't check whether that stroke is the stroke that was there. I looked at SSIM first and nearly wrote the opposite conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dividing line is pixel height, not "is it text"
&lt;/h2&gt;

&lt;p&gt;The airport photo makes this obvious in one frame. Large lettering on the fuselage came back sharper than interpolation and genuinely closer to the truth — real recovery. A smaller URL on the same aircraft was wiped out into a smudge. Distant road signs with three groups of numbers were redrawn as diagonal hatching and checkerboard, with every lowercase a turned into an o. Same image, same model, same run.&lt;/p&gt;

&lt;p&gt;In numbers: a 28px line of text, seven pixels tall in the thumbnail, kept 9 of 9 characters readable through interpolation and 3 of 9 through the best AI tier. The same sentence at 20px, five pixels tall, scored 0 of 9 on both — but interpolation hands you an even grey smear, while the AI hands you neat, sharp, evenly spaced shapes that look exactly like characters. One of those tells you that you failed to read it.&lt;/p&gt;

&lt;p&gt;The cleanest evidence needs no eyeballs at all. The body of the order form is pure greyscale. Counting pixels where the RGB channels diverge noticeably: the truth is 0.000%, interpolation is 0.000%, and the five AI tiers produced between 0.305% and 2.595%, with one tier hitting a channel gap of 98 at a single point. Colour cannot be recovered from an image that never contained any.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it genuinely wins
&lt;/h2&gt;

&lt;p&gt;None of this makes AI upscaling a bad deal. It won two of my four samples, and on the line-art icon it won hard: 27.58 dB against 22.87, and the thin strokes that interpolation smears into grey bands come back as clean solid lines. The photo went to AI too, by a narrow 0.47 dB. The rule I ended up with is about content, not model tiers. Icons, line art, flat colour, large lettering — upscale away. Serial numbers, amounts, spec digits, small text — check against the original regardless of how confident the output looks. Around eight pixels of height is the tipping point, where digits tend to survive and characters start turning into other characters.&lt;/p&gt;

&lt;p&gt;The ImgIng docs say upscaling infers detail and shouldn't be used for identity documents, forensics, historical research or verifying OCR source text. My run backs that up for the small-text half of it. For the rest I have nothing: no faces, no ID documents, no old photographs in my samples, so I'm quoting that part rather than confirming it. I also don't know yet whether the five-pixel threshold moves with a different typeface or a different script — I varied the size, not the font.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Your GIF didn't speed up because of compression. Dropping frames did</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Mon, 21 Sep 2026 08:04:18 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/your-gif-didnt-speed-up-because-of-compression-dropping-frames-did-29ja</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/your-gif-didnt-speed-up-because-of-compression-dropping-frames-did-29ja</guid>
      <description>&lt;p&gt;Shrink a GIF and two complaints come back over and over: the colours went blotchy, and the thing now plays faster than it used to. I had assumed both were the compressor being heavy-handed. Only one of them is about compression at all, and the faster playback isn't it. I found that out by drawing a few test animations with a script and reading numbers out of the files instead of watching them loop.&lt;/p&gt;

&lt;p&gt;Two words first, because the rest depends on them. A GIF is a stack of still images played in order; each one is a frame, and each frame carries a number saying how long it stays on screen. That number is the frame delay, and the running time of the animation is just the sum of those delays. Nothing else in the file controls playback speed.&lt;/p&gt;

&lt;p&gt;So I exported the same source several ways and read frame count, per-frame delay, total duration and loop flag back out of every file. The source was a gradient clip I drew with a script: 480×270, 30 frames, 80 ms each, 2400 ms total, infinite loop. Across seven cases — palette cut to 64 colours, cut to 16, dithering on and off, a different animated container — every output came back as 30 frames, 80 ms per frame, 2400 ms total, looping forever. Identical to the source, down to the field.&lt;/p&gt;

&lt;p&gt;What does change playback speed is dropping frames. If you keep every second frame, you have half as many frames, but the ones you kept still carry their original delay — nobody doubles it for you. The total duration halves, so the animation plays at 2x. On a second clip I drew the same way, a fake admin UI at 960×600, 75 frames, 60 ms each, keeping every second frame cut the file by 28.8% with the surviving frames measuring 68.5 dB against the source, while the duration went from 4500 ms to 2280 ms. Every third frame saved 42.5% and gave me 3x speed. That is the format working as designed: dropping frames and adjusting delays are two separate operations, and doing one doesn't imply the other. Set playback back to 0.5x and you're at the original pace, at no cost in bytes — I compared a speed-adjusted export against the baseline and the two files were byte-for-byte identical, only the delay field written differently.&lt;/p&gt;

&lt;p&gt;The other half of "it looks wrong now" is banding, and that one really is caused by reducing colours. A GIF frame can use at most 256 colours, and that list is the palette. Cut the list and smooth gradients collapse into visible bands. I measured it instead of judging by eye: take one row of pixels across a frame, count the distinct colours and how wide each run of identical colour is. At 256 colours my sample row had 22 colours, runs averaging 17.8 px. At 16 colours it was down to 5 colours, average run 96 px, widest 134 px — on a frame 480 px wide, one colour covering a quarter of it. The file dropped from 670 KB to 186 KB, so the saving is real, and so is the cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Four exports of the same gradient clip compared at 3x zoom: 256, 64 and 16 colours, plus 64 colours with dithering at 981 KB. The sample text inside the frames is Chinese because the clip is one I generated myself" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dithering is the usual fix for banding — scatter two available colours so they read as a third from a distance — and on a GIF it costs more than the banding does. At 64 colours it took the file from 363,719 bytes to 1,004,544 bytes: 2.76x, and 69% larger than exporting at 256 colours with no reduction at all. LZW only rewards runs of repeated pixels along a row, and dithering replaces smooth areas with pixel-level noise, so there is nothing left to collapse. My sample row went from 20 runs to 245. Dithering a GIF is effectively opting out of compression.&lt;/p&gt;

&lt;p&gt;If the clip is a gradient or a photo, the 256-colour ceiling is the actual problem and no parameter gets around it. The same source as animated WebP came out at 167 KB with 129 colours on the sampled row — slightly smaller than the 16-colour GIF and twenty times richer. For UI captures and flat line art I still use GIF; the colour count is low to begin with.&lt;/p&gt;

&lt;p&gt;I ran these exports in ImgIng (&lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt; ), mostly because the work happens locally in the browser — I watched the Network panel from import to save and there wasn't a single non-GET request — and because all 16 exports matched the source on frame count, delay, duration and loop. Had the tool rewritten the timeline, the claim above would be worthless. What I still don't have is a rule for how far you can drop frames before it reads as broken: two-for-one is usually invisible on a UI capture, three-for-one makes cursor movement jump, and past that it depends on the footage.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Three ways to get a picture out of a PDF, and the page that returned 0 results</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Sun, 20 Sep 2026 08:04:20 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/three-ways-to-get-a-picture-out-of-a-pdf-and-the-page-that-returned-0-results-3088</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/three-ways-to-get-a-picture-out-of-a-pdf-and-the-page-that-returned-0-results-3088</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nmbqld5cz7mr9n529o4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nmbqld5cz7mr9n529o4.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;br&gt;
I dropped a one-page PDF with a flowchart and a bar chart into an image extractor, pressed Start, and got this: "Complete", "0 results · 0 KB", and a pale, disabled button that still said "Save this image". No hint about what went wrong. Nothing had gone wrong, as it turns out. The page just didn't contain any images, and it took me a minute to see why that's the right answer. It also made me sort out the three ways I actually know to get a picture out of a PDF, because they're good at completely different things.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Extract PDF images on a vector-only test page I drew: " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The test page is one I drew myself with reportlab: four boxes with arrows between them and six coloured bars. Every shape on it is a line or a filled rectangle described in the PDF's own drawing instructions. No photo, no bitmap anywhere. So a tool whose whole job is pulling out embedded images has nothing to pull, and it doesn't fake it with a screenshot of the page. I like that it doesn't fake it. I don't like that it leaves you on a "0" with no next step, because the next step exists and sits one click away in the same workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way 1: pull out the original embedded image
&lt;/h2&gt;

&lt;p&gt;This is the one to use for photos, product shots, anything that was a picture file before it went into the PDF. ImgIng's Extract PDF images runs in a browser tab and doesn't ask you to sign up. On my own 5-page test file, where a photo is reused on three pages and a logo sits on three pages too, it returned 3 files, each card listing the pages it appears on. The JPEGs it gave back were byte-for-byte the same as the JPEGs I had put in, SHA-256 and all, which is about as "original" as a file can be.&lt;/p&gt;

&lt;p&gt;If you're comfortable in a terminal, poppler's &lt;code&gt;pdfimages&lt;/code&gt; does the same family of job. Going by its man page, &lt;code&gt;-list&lt;/code&gt; prints every image with its page, size, colour space and encoding, and &lt;code&gt;-all&lt;/code&gt; writes JPEG, JPEG2000, JBIG2 and CCITT images in their native format, CMYK as TIFF and everything else as PNG. I haven't run it for this post, it isn't installed on this laptop, so treat that as the documentation talking rather than me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way 2: render the page and crop
&lt;/h2&gt;

&lt;p&gt;This is what the 0-result page actually needs. Same workspace, left column, switch to PDF to images, set the format and resolution, pick the page. At 216 DPI my A4 test page came out as a 1786×2526 PNG of 142 KB, flowchart and bars nice and sharp. At 300 DPI an A4 page is 2480×3508. Since the chart is drawn from shapes, you can render it bigger and it just gets crisper; there is no "original size" to lose.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3hj8k94ia0ztdwfxz2m1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3hj8k94ia0ztdwfxz2m1.png" alt="PDF to images on the same vector page: PNG at 216 DPI, 1786 × 2526" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two catches. It's the whole page, and the workspace has no crop tool, so cutting the chart out is a job for whatever image editor you already have. And for photos, rendering is the worse deal, not an equivalent one. In another of my test files a 1600×1067 photo was placed smaller on the page, and a 216 DPI render gave me that photo at 1361×908 after cropping. It looked fine, but it was a smaller, re-encoded copy of something the PDF had stored at full size the whole time. A chart figure I stored at 1200×800 and placed 8 cm wide came out at 680×454 from a 216 DPI render and 945×630 at 300 DPI, still short of the 1200 it really was.&lt;/p&gt;

&lt;p&gt;So: bitmaps, Way 1. Anything drawn, Way 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way 3: ask for the source file
&lt;/h2&gt;

&lt;p&gt;Slightly boring, often the best. A render of a vector chart is still pixels at the end; if the person who made the PDF can send the SVG, the spreadsheet behind the chart or the design file, you get something you can resize and edit forever. I went looking for an SVG export in the workspace because the footer literally reads "PDF · SVG · OCR", but none of the three modes I used, PDF to images, Extract PDF images and Extract PDF content, offered SVG as an output. Maybe it lives in another tool on the site; I didn't go digging.&lt;/p&gt;

&lt;p&gt;My rule of thumb now, for what it's worth: if Extract PDF images shows "0 results" on a page that clearly has a graphic, that graphic is drawn, not embedded, so I switch to PDF to images at 300 DPI and crop. If it shows results, I take those and don't render anything. Both modes live in the same place, &lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt; , under the PDF tools.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I tried three ways to get a PDF under 2 MB. Whether it was a scan mattered more than the tool</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Sat, 19 Sep 2026 08:04:12 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/i-tried-three-ways-to-get-a-pdf-under-2-mb-whether-it-was-a-scan-mattered-more-than-the-tool-3n02</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/i-tried-three-ways-to-get-a-pdf-under-2-mb-whether-it-was-a-scan-mattered-more-than-the-tool-3n02</guid>
      <description>&lt;p&gt;Upload forms love the line "PDF, max 2 MB". I wanted to know which of the no-install, no-account options actually gets a file under that line, so I built a few test PDFs and ran them through three methods on a Mac. The short version: whether the PDF is a scan mattered more than which tool I picked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test files
&lt;/h2&gt;

&lt;p&gt;All of them are self-made with fictional content. An 8-page brochure for a made-up brand (19,458,294 bytes), with photos adapted from a CC0 image on Wikimedia Commons and screenshots of a fake dashboard. A 4-page "scan": a fictional form rendered as full-page 300 DPI images with noise and a slight tilt, 8,948,995 bytes, no text layer. (A text layer is the part of a PDF stored as real text, the part you can select and search. Scans don't have one; each page is just a picture.) And a 10-page text-only document.&lt;/p&gt;

&lt;p&gt;DPI here means pixels per inch of the page. A 4032-pixel photo squeezed into a 9 cm box works out to 1138 DPI, far more detail than anyone will see on screen, and that is where most of the file size hides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: just re-save it
&lt;/h2&gt;

&lt;p&gt;Writing the brochure back out through macOS's PDF framework without any filter produced 20,040,400 bytes. That's 3% bigger. Re-saving is not compressing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: macOS "Reduce File Size"
&lt;/h2&gt;

&lt;p&gt;This is the Quartz filter you can pick when exporting from Preview. I applied the same system filter file directly rather than clicking through Preview's UI, so treat it as the same filter, not a byte-for-byte check of Preview's export. Its settings: JPEG quality 0.7, resample to 144 DPI, max 2400 px per side.&lt;/p&gt;

&lt;p&gt;On the brochure it did well: 1,432,231 bytes, about 1.37 MB, text intact. The screenshots were converted to JPEG along the way, and PSNR came out at 38.0. PSNR is a score from comparing the compressed pages to the original pixel by pixel; higher means closer to the original.&lt;/p&gt;

&lt;p&gt;On the scan: 2,905,274 bytes, about 2.77 MB. Still over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: ImgIng, locally in the browser
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;ImgIng&lt;/a&gt; compresses PDFs inside the browser. I ran it on the Chinese-language version of the site with DevTools open; no request sent the file anywhere. It has four levels: Screen (72 DPI), eBook (150 DPI, the default), Print (300 DPI) and Lossless structure.&lt;/p&gt;

&lt;p&gt;eBook on the brochure: 864,614 bytes (shown as 844 KB), 8 pages, text identical, PSNR 41.3. eBook on the scan: 1,927,707 bytes (shown as 1.84 MB). Under the line.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Scan compressed four ways" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Detail from the self-made 4-page scan (fictional form; the sample text is Chinese). Top right: ImgIng Screen, 347 KB, soft edges. Bottom left: ImgIng eBook, 1.84 MB. Bottom right in red: macOS Reduce File Size, 2.77 MB, still over 2 MB.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why scans behave differently
&lt;/h2&gt;

&lt;p&gt;With the macOS filter the brochure shrank to 7.4% of its size and the scan only to 32.5%. ImgIng eBook: 4.4% versus 21.5%. The brochure's photos were wildly oversampled, so dropping to ~150 DPI throws away a lot. The scan started at 300 DPI and every page is one big image, so halving the resolution has less to give. Both tools land near the same resolution on the scan yet end up almost 1 MB apart; from the outside I can't tell why, my guess is different JPEG settings.&lt;/p&gt;

&lt;p&gt;Text-only PDFs barely move. My 10-page sample was 85% embedded fonts; ImgIng saved 4.8% at every level and the macOS filter made it 3.4% bigger. That's one sample, so I wouldn't generalize, but if a mostly-text PDF is over the limit, compression probably isn't the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things I check before uploading
&lt;/h2&gt;

&lt;p&gt;Don't grab the smallest level by reflex. Screen got the brochure down to 352,318 bytes, but the small table text in the screenshots became unreadable. I start with eBook.&lt;/p&gt;

&lt;p&gt;And check the unit. ImgIng shows sizes in 1024-based MB, while Finder uses 1000. The brochure reads 18.56 MB in one and about 19.5 MB in the other. I don't know which one a given upload form uses, so I check both: 1,927,707 bytes is under 2 MB either way; 2,905,274 bytes is over either way.&lt;/p&gt;

&lt;p&gt;What I didn't test: "print to PDF", online upload-based compressors, and whether any specific site accepts the result. I only measured file sizes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ezejowx61eecd0dvvt4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ezejowx61eecd0dvvt4.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>PNG is lossless, palette reduction is not</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Fri, 18 Sep 2026 08:04:10 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/png-is-lossless-palette-reduction-is-not-53h0</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/png-is-lossless-palette-reduction-is-not-53h0</guid>
      <description>&lt;p&gt;A designer on my team asked last week why the gradient banner I had compressed looked banded. I said PNG is a lossless format, so that could not happen, then opened both files side by side and it very obviously had. I had been wrong about this for years, in a way that is easy to be wrong about: lossless describes the encoder, not the button you clicked.&lt;/p&gt;

&lt;p&gt;Here is the split. The PNG encoder takes an array of pixels, folds it up by looking for repetition along each scanline, and gives you back exactly the same array when you decode it. Nothing is lost there. But almost every tool sold as a PNG compressor saves most of its bytes before the encoder ever runs, by converting the image to indexed color, PNG-8. It counts the colors in the image, picks at most 256 of them as a palette, and snaps every other pixel to the nearest one. That snap is the lossy step, and it happens first. The encoder then losslessly stores an image that has already been altered.&lt;/p&gt;

&lt;p&gt;The cheapest way I found to see how much got thrown away is to take one row of pixels and count the distinct values in the red channel. On a 960×480 gradient I built for this, the original PNG is 53.5 KB and that row holds 200 distinct values. At 256 colors the file is 12.8 KB and the row is down to 16 values. At 64 colors, 7.7 KB and 10 values. At 16 colors, 5.0 KB and 5 values. A gradient is nothing but closely spaced neighboring colors, so once the palette runs out the transition has to round off, and the rounding edges are the bands you see.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Palette reduction on a gradient" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Dithering does not buy what I thought it did
&lt;/h2&gt;

&lt;p&gt;Most tools offer a dithering switch that scatters pixels of two palette colors along the boundary so your eye mixes them back into a gradient. It does look better. It also costs an absurd amount: the 16-color version went from 5.0 KB to 74.4 KB with dithering on, 14.9 times larger, and bigger than the untouched original. The 64-color version landed at 95.4 KB, 12.3 times its undithered size. My guess is that PNG earns its bytes from runs of identical values along a scanline, and dithering is precisely the thing that breaks those runs into noise.&lt;/p&gt;

&lt;p&gt;The measurement surprised me more than the size did. PSNR compares the compressed image to the original pixel by pixel, and higher means closer. The undithered 16-color file scored 25.3, the dithered one 22.6. Dithering moved the result further from the original while making it easier to look at. Pleasant and accurate turn out to be separate axes, and I had been treating them as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where reduction is basically free
&lt;/h2&gt;

&lt;p&gt;None of this makes palette reduction a bad idea. I ran a 1280×800 UI screenshot I built for testing through ImgIng's image compression on defaults: 94.0 KB down to 37.6 KB, 60% saved, a 208-color palette, PSNR 60.7 and SSIM 1.000. Zoomed to 300%, the small text in the code block is essentially pixel-identical to the source. A UI screenshot is flat fills, borders and text, so it never had many colors to begin with and snapping to 256 costs it nothing. For comparison I tried the other common trick on the same file, halving it and saving as JPEG: 20.1 KB, smaller still, PSNR 27.0, and the code block visibly mushy. Screenshots exist to be read, so that trade is a bad one.&lt;/p&gt;

&lt;p&gt;My rule now is about the material rather than the tool. Screenshots, flat icons and line art go through palette reduction. Photos, gradients, soft-edged and semi-transparent assets get exported as WebP or AVIF instead. The same shadowed transparent badge is 35.2 KB as PNG, 20.2 KB as WebP for 43% off, and 2.4 KB as AVIF for 93% off, with the edge transition intact; a soft-edged icon went from 11.0 KB to 5.4 KB as WebP. The AVIF numbers are tempting, but I did not test decoding on older devices this round, so anything I hand to someone else still ships as WebP.&lt;/p&gt;

&lt;p&gt;One last thing, found by clicking around. That gradient, fed to the same compressor, got classified into the icon and line-art bucket and went through reduction anyway, and came out larger than the input. The tool did not hand me that file: the panel said the original was already the smallest one and kept it. Turning dithering off gave me 13.5 KB, a 75% saving, and the bands right back. So before compressing anything I now ask one question, whether the image contains a large smooth gradient, and afterwards I zoom to 300% and check the gradient area and the smallest text, because neither shows up in a thumbnail. The tool is at &lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt; and it runs in the browser. The screenshot, gradient, badge and icon above are all test images I made myself.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Which tools drop the color profile? Pillow, ffmpeg and sips across JPG, PNG and WebP</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Thu, 17 Sep 2026 08:21:16 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/which-tools-drop-the-color-profile-pillow-ffmpeg-and-sips-across-jpg-png-and-webp-1b3h</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/which-tools-drop-the-color-profile-pillow-ffmpeg-and-sips-across-jpg-png-and-webp-1b3h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8sd3dsaudzvfk7vjl759.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8sd3dsaudzvfk7vjl759.png" alt=" " width="800" height="327"&gt;&lt;/a&gt;&lt;br&gt;
If a photo looks washed out after you compress or convert it, the encoder is usually not the culprit. More often the tool dropped the color profile on save, and the viewer is now reading the same pixel values with a different color standard. I ran one Display P3 image through Pillow, ffmpeg and macOS sips, saved it as JPG, PNG and WebP, and wrote down which combinations kept the profile. The answer depends on the tool and the output format together, and WebP was the worst column.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the profile does
&lt;/h2&gt;

&lt;p&gt;An ICC color profile is a small block of data inside the image file. It tells software which color standard the RGB numbers are meant to be read in. The same numbers read as Display P3 come out more saturated than when read as sRGB, and Apple's developer documentation notes that iPhone cameras can capture P3 starting with the iPhone 7. When the profile is missing, software generally falls back to sRGB. Nothing about the pixels changes. Only the interpretation does, which is why the result looks faded rather than blocky or blurry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sample
&lt;/h2&gt;

&lt;p&gt;The source is a CC0 photo from Wikimedia Commons, taken on an iPhone 6 at Madrid airport, resized to 2048×1536. An iPhone 6 cannot shoot P3, so I made the P3 version myself: Pillow's ImageCms converted the pixels to Display P3 using the macOS system profiles, embedded the profile, and saved a quality 92 JPEG. The colors in this photo sit inside sRGB. I have no numbers for real wide-gamut photos, and the gap there could be different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool × format results
&lt;/h2&gt;

&lt;p&gt;All saves used default settings with the P3 JPEG as input.&lt;/p&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;JPG&lt;/th&gt;
&lt;th&gt;PNG&lt;/th&gt;
&lt;th&gt;WebP&lt;/th&gt;
&lt;th&gt;Resized&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pillow 11.3&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;dropped (resize and thumbnail, saved as JPG)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg 7.1&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;not tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS sips&lt;/td&gt;
&lt;td&gt;kept (also at quality 60)&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;not tested (no output file on my machine)&lt;/td&gt;
&lt;td&gt;kept (-Z 1024)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Table of whether Pillow 11.3, ffmpeg 7.1 and macOS sips keep the color profile when saving JPG, PNG, WebP and resized output. Sample: Wikimedia Commons CC0 photo (iPhone 6), converted locally to a Display P3 profile" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The row that surprised me was ffmpeg. Same command, same input, and the only change is the output extension: .jpg keeps the profile and .webp loses it. If you only spot-check JPG output, you would never notice. Pillow is the opposite shape. Its defaults drop the profile for JPG and WebP, but passing the original image's icc_profile back into save kept it for both JPG and WebP when I tried it. I did not find a verified way to make ffmpeg carry the profile into WebP, so I'm leaving that cell honest rather than guessing a flag.&lt;/p&gt;

&lt;p&gt;Raising quality does not help. I saved the Pillow JPG again at quality 100 and the profile was still gone. Quality controls how much detail the encoder throws away, and the profile is a separate chunk of the file that either gets written or doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much color you lose
&lt;/h2&gt;

&lt;p&gt;I rendered both versions as sRGB and measured CIELAB ΔE76. On this sample, the P3 version without its profile had a mean ΔE of 1.83 and a 95th percentile of 3.9, with only 1.7% of pixels above ΔE 5. Mean chroma dropped 16.6% though, and the most saturated 5% of pixels lost 15.3% chroma at about ΔE 5.4. An Adobe RGB version of the same photo was worse: mean chroma down 20.1%, and 7.4% of pixels above ΔE 5. Neutral grays and whites stay almost where they were. The red tail fin and the blue sky are where it shows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv55d3b8wvdvbrbed0szb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv55d3b8wvdvbrbed0szb.png" alt="The same pixels shown with the Display P3 profile and after the profile is dropped and read as sRGB. Sample: Wikimedia Commons CC0 photo (iPhone 6), converted locally to a Display P3 profile" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking a file
&lt;/h2&gt;

&lt;p&gt;On macOS, &lt;code&gt;sips -g profile file&lt;/code&gt; prints the profile name. The P3 source reports Display P3, and Pillow's default WebP reports nil. The catch is JPG: a JPG with no profile at all reports sRGB IEC61966-2.1, the same as a file with a real embedded sRGB profile. So I check the source and the output side by side. Source says Display P3, output says sRGB, the profile was lost. Other platforms have image info tools that show embedded profiles too, but I haven't checked where each one puts it.&lt;/p&gt;

&lt;p&gt;What I do now before trusting any new compression step: run one P3 image through it, check every output format with that command, and only then batch the rest.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>4 ways to turn iPhone HEIC into JPG, compared: orientation, EXIF, and a 38% bigger file</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Wed, 16 Sep 2026 08:06:25 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/4-ways-to-turn-iphone-heic-into-jpg-compared-orientation-exif-and-a-38-bigger-file-4oe8</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/4-ways-to-turn-iphone-heic-into-jpg-compared-orientation-exif-and-a-38-bigger-file-4oe8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3sg6ipwbwpw0g4vw16lx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3sg6ipwbwpw0g4vw16lx.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;br&gt;
Short version: all four common ways to turn an iPhone HEIC photo into a JPG "work", but they disagree on three things people only notice later. A portrait photo can come out with sideways pixels, the camera metadata may or may not survive, and the JPG can end up bigger than the HEIC you started with. On my test file a default browser conversion grew from 745.1 KB to 1.01 MB, a 38% increase.&lt;/p&gt;

&lt;p&gt;If HEIC is new to you: it is the format iPhones save photos in by default (the camera setting calls it "High Efficiency"). The files are small, but plenty of places reject them. Windows needs both the "HEIF Image Extensions" and the "HEVC Video Extensions" to open them, and the HEVC one may cost money in the Store.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sample
&lt;/h2&gt;

&lt;p&gt;I did not use my own phone photo. The source is a CC0 photo from Wikimedia Commons, shot on an iPhone 6 (an airport apron in Madrid, 2015, 3264×2448). I encoded it to HEIC with the macOS system encoder and set the orientation to portrait, which gave a 745.1 KB file. That makes it a HEIC produced on a Mac, not one straight out of an iPhone, so anything about HDR or Live Photos is outside what I can say here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison table
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Four ways to convert iPhone HEIC to JPG, compared by where it runs, output, portrait orientation, EXIF, batch size and source of evidence. Sample: Wikimedia Commons CC0 photo (iPhone 6), encoded to portrait HEIC with the macOS system encoder, not straight from an iPhone" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Portrait orientation&lt;/th&gt;
&lt;th&gt;EXIF&lt;/th&gt;
&lt;th&gt;How many at once&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Camera format "Most Compatible"&lt;/td&gt;
&lt;td&gt;iPhone Settings › Camera › Formats&lt;/td&gt;
&lt;td&gt;n/a, new photos are JPEG&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;no conversion&lt;/td&gt;
&lt;td&gt;Apple docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer to Mac or PC: "Automatic"&lt;/td&gt;
&lt;td&gt;iPhone Settings › Photos&lt;/td&gt;
&lt;td&gt;not tested&lt;/td&gt;
&lt;td&gt;not tested&lt;/td&gt;
&lt;td&gt;per import&lt;/td&gt;
&lt;td&gt;Apple docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sips&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;macOS Terminal&lt;/td&gt;
&lt;td&gt;pixels stay landscape, EXIF Orientation 6&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;many (tested 2)&lt;/td&gt;
&lt;td&gt;my test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser, local decode (ImgIng)&lt;/td&gt;
&lt;td&gt;browser tab (tested in Chromium)&lt;/td&gt;
&lt;td&gt;pixels rotated upright, Orientation 1&lt;/td&gt;
&lt;td&gt;EXIF and XMP kept&lt;/td&gt;
&lt;td&gt;one&lt;/td&gt;
&lt;td&gt;my test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Row by row
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Most Compatible.&lt;/strong&gt; Apple's support page says that after switching Settings › Camera › Formats to "Most Compatible", new photos are saved as JPEG (and videos as H.264). Photos you already took stay HEIC. New photos will also take more space. I did not shoot comparison photos for this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transfer to Mac or PC.&lt;/strong&gt; On newer iOS versions the setting lives under Settings › Apps › Photos. Apple says HEIF/HEVC media may be converted to JPEG/H.264 when you import to a computer, and "Keep Originals" turns that off. That "may" is doing a lot of work. I had no way to test when it converts or what the orientation and metadata look like afterwards, so treat it as something to try, not a guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sips on macOS.&lt;/strong&gt; The one-liner is &lt;code&gt;sips -s format jpeg *.heic --out out/&lt;/code&gt;, and it happily converts a whole folder (I ran two files in one go). The catch shows up with portrait photos. The output JPG is still 3264×2448 landscape pixels with EXIF Orientation = 6, which means "rotate 90° clockwise when displaying". Preview on a Mac honors that, so everything looks fine locally. Anything that ignores the tag may show the photo lying on its side. Camera metadata such as the model "iPhone 6" is preserved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser, local decode.&lt;/strong&gt; I used ImgIng. After you drop in a HEIC it tells you the browser cannot decode it natively and offers a "load decoder" button. That downloads a decoder component. The photo itself is not uploaded; I watched the network log and there were zero POST requests. The output JPG is 2448×3264 with Orientation = 1, so the pixels themselves are upright, and EXIF and XMP are kept. Two things you have to do by hand: WebP is pre-selected after upload, so you need to click JPG, and the file picker takes one image at a time. (Converting &lt;em&gt;to&lt;/em&gt; HEIC in that tool is marked as server-side; I only converted from HEIC.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The size surprise
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fskgz28d3wgmvj7n4b92y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fskgz28d3wgmvj7n4b92y.png" alt="ImgIng conversion result: 745.1 KB to 1.01 MB, " width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The default JPG quality was 88, because the tool classified this airport photo as "Graphics / text". I still don't know why it didn't pick the Photo preset (80). Results on the portrait sample:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Quality&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;vs. HEIC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPG&lt;/td&gt;
&lt;td&gt;88 (default)&lt;/td&gt;
&lt;td&gt;1.01 MB&lt;/td&gt;
&lt;td&gt;+38%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPG&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;742.2 KB&lt;/td&gt;
&lt;td&gt;about the same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;84 (default)&lt;/td&gt;
&lt;td&gt;423.2 KB&lt;/td&gt;
&lt;td&gt;−43%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;lossless&lt;/td&gt;
&lt;td&gt;8.64 MB&lt;/td&gt;
&lt;td&gt;about 11.9×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An upscaled 4032×3024 version of the same photo went +61% at quality 88 and still +17% at 80. HEIC simply compresses better than JPG, so a high quality setting costs bytes, which matters when an upload form has a size limit. I didn't record the sips output size, so it's not in the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one I'd pick
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You keep sending photos to Windows users: switch the camera to Most Compatible and stop converting.&lt;/li&gt;
&lt;li&gt;A batch of files on a Mac that stays on your machine: &lt;code&gt;sips&lt;/code&gt;, then upload one test image wherever it's going and check it isn't sideways.&lt;/li&gt;
&lt;li&gt;One or two photos on someone else's computer, or the destination is a website you don't control: convert in the browser, click JPG, look at the size, and drop quality to 80 (or send WebP if they accept it).&lt;/li&gt;
&lt;li&gt;Importing over a cable anyway: set transfer to Automatic, but check what actually arrives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser tool I tested is at &lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Browser video background removal held up on a talking head and fell apart in a street crowd</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Tue, 15 Sep 2026 08:06:09 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/browser-video-background-removal-held-up-on-a-talking-head-and-fell-apart-in-a-street-crowd-2fgm</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/browser-video-background-removal-held-up-on-a-talking-head-and-fell-apart-in-a-street-crowd-2fgm</guid>
      <description>&lt;p&gt;![ ](&lt;a href="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/bSwapping" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/bSwapping&lt;/a&gt; the background on a video in a browser tab, with the video never leaving your machine, is the kind of thing I try the same evening I hear about it. The imging video matting page is marked Beta and gives you three models: Compatible (RVM MobileNetV3, about 7.2 MB), Quality first (RVM ResNet50, about 51.3 MB) and an Experimental ultimate tier that downloads about 345 MB and is for non-commercial testing only. I stuck to the small one. Everything below is the Compatible model on three free clips from Wikimedia Commons, all CC0, exported on a plain white background so the mistakes are easy to spot. The short version: on one person talking it looks fine, and each thing you add to the frame, faster movement or more people, makes it look fake in a different place.&lt;/p&gt;

&lt;p&gt;The easy case first. "Tarun speaking 01" is a man talking at an event, 464×832, phone-like framing, with a projector screen full of text behind him. At 4 seconds his hair and shoulder line are clean, and I couldn't see the edge shimmer I was bracing for. The top of the head is where I found something: a few faint specks and a ghost of the projector lettering left above his hair. On white you have to look for it. On a busy replacement background I doubt anyone would notice. On a dark one I'm less sure.&lt;/p&gt;

&lt;p&gt;The surprise in the same clip was everyone else. The tool doesn't cut out "the speaker", it cuts out people, so the women standing to the right got kept as foreground too, with their lower bodies turned into a semi-transparent smear. In the transparent export I made of the same clip, an office chair near one of them also came through as a grey see-through shape. If your clip has a bystander, the result has a bystander, only partly there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Talking-head clip at 4 s: clean hair and shoulders with faint specks above the head; a bystander kept with a ghosted lower body" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Clip: Wikimedia Commons "Tarun speaking 01" (CC0).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Fast arms were next. "Juggling with swing poi" is someone swinging poi under purple light. I cut a 6-second stretch with the most motion and scaled it to 720 wide. Wherever a hand moves fast, the hand and whatever it's holding go soft and half transparent, basically motion blur turned into see-through edges. At 3 seconds a patch of the background next to his back, around a door frame, stayed in the output as if it were part of him. At 5 seconds, with both arms up, the raised hands are semi-transparent. The curly hair, oddly, held its outline better than the hands did. There was no fire in this stretch, so I can't say anything about flames.&lt;/p&gt;

&lt;p&gt;Then the one that really fell apart: "Folkloristic dance in Naples", a street performance, 1280×720, a row of musicians and a crowd behind barriers, camera slowly panning. Where people overlap at 2 seconds, the ones in the middle turn semi-transparent and a pair of legs just isn't there. At 11 seconds the whole group of about five people on the left is gone, removed along with the street behind them. The front row on the right, lighter clothes and clear outlines, stays solid the whole time. I also tried this clip on a gradient background image I made, and at 6 seconds the gradient shows through the lower half of the people in front. The far-away crowd was removed properly, for what it's worth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futdiptxt2ybb7b8idsq5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futdiptxt2ybb7b8idsq5.png" alt="Street clip: overlapping people semi-transparent at 2 s, left group removed at 11 s, gradient background showing through at 6 s" width="800" height="399"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Clip: Wikimedia Commons "Folkloristic dance in Naples (tammorriata)" (CC0).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So where does it look fake? From these three: the top of the head, faintly, on a clean single-person shot. Hands and anything held in them, whenever they move fast. People standing behind or between other people, especially in dark clothes against a busy background. And anyone who isn't your subject, who gets kept whether you wanted them or not. None of that is hidden, to be fair. The page's own notes say complex occlusion and fast motion can cause edge errors, and after this I believe it.&lt;/p&gt;

&lt;p&gt;What I'd actually use it for: one person, facing the camera, not much else going on in the frame. That covers a lot of talking-head clips. I didn't try Quality first on the crowd clip, which is the obvious next test, and I don't know yet whether the bigger model would keep that left group or just lose them more neatly. The model card, to be fair, pitches Compatible as the one for typical computers, longer videos and fast previews, not as the one for crowds.&lt;/p&gt;

&lt;p&gt;It's at &lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt; if you want to try your own clip before trusting it with anything busy.&lt;br&gt;
kk61ykro0gwx0t8xnlb.png)&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A resized copy of a photo still had its GPS. How I check before sending one</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Mon, 14 Sep 2026 08:11:01 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/a-resized-copy-of-a-photo-still-had-its-gps-how-i-check-before-sending-one-g3g</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/a-resized-copy-of-a-photo-still-had-its-gps-how-i-check-before-sending-one-g3g</guid>
      <description>&lt;p&gt;I resize and convert photos all day, mostly to get them under some upload limit, and somewhere along the way I'd filed "the smaller copy is a new file" in my head as "the smaller copy is clean". Then I actually looked. On a real iPhone photo, a resized copy made with the Mac's built-in &lt;code&gt;sips&lt;/code&gt; tool still had the exact coordinates of where it was taken. A copy saved with Python's Pillow didn't. Same photo, same kind of operation, opposite results.&lt;/p&gt;

&lt;p&gt;The photo I used is "Avions à l'aéroport de Madrid - 2015" from Wikimedia Commons, released under CC0 (so free to reuse), shot on an iPhone 6 in July 2015. Its latitude reads 40°29′27.76″N, and together with the longitude it points straight at the airport. I ran it and a synthetic test photo I generated myself (fake GPS, a point in the middle of the sea) through the usual suspects and read the results back with piexif and Pillow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="What survived each kind of processing, tested 2026-09-11 with Python 3.9 + Pillow 11.3, macOS sips and HeadlessChrome 149. Red = GPS still present. Real photo: CC0 iPhone 6 photo from Wikimedia Commons" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The row that got me is "macOS sips convert / resize: kept". Resized to 1200×900 and converted to PNG, the Madrid photo still carried its coordinates. Pillow's plain save dropped everything, and so did a browser canvas re-encode when I tried it on the synthetic photo. So "I converted it" tells you nothing on its own. It depends on which program did the converting, and most apps don't tell you. I haven't tested what any chat app or social site does to photos on upload, and I have no way to verify it from the outside, so I don't count on them to remove anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking and removing it, the official way
&lt;/h2&gt;

&lt;p&gt;I stuck to what Apple and Google document in their own help pages, plus ExifTool's manual for Windows. On iPhone, open the photo in Photos and swipe up or tap the info button: you get the camera, lens and shutter details, and the place it was taken. To take the location off, tap the More button, then Adjust Location, then No Location. Apple's guide also says you can revert that, so I read it as "removed in Photos" and wouldn't assume the original bytes are gone. For a one-off share, the better switch is in the share sheet: tap Options, turn off Location, tap Done, then send. And if you'd rather the camera never records it, it's Settings &amp;gt; Privacy &amp;amp; Security &amp;gt; Location Services &amp;gt; Camera &amp;gt; Never.&lt;/p&gt;

&lt;p&gt;On a Mac, Preview is the quickest check: open the photo and choose Tools &amp;gt; Show Location Info. Apple's own note is handy here: if that menu item is grey, the image has no location information. In the Photos app, it's Image &amp;gt; Location &amp;gt; Hide Location, with Revert to Original Location to undo it. Apple calls it "hide", not "remove", and I think the wording is deliberate.&lt;/p&gt;

&lt;p&gt;On Android the location switch lives in the camera app's settings, and the name changes by brand; Google's help page lists what it's called on several makers' phones. Google Photos has a per-album and per-link toggle for sharing location. The part people miss is in Google's own help: if the camera added the location, you can't remove it inside Google Photos, and the sharing toggle doesn't apply when you download a photo and send it some other way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Windows: ExifTool
&lt;/h2&gt;

&lt;p&gt;I didn't verify the Windows menus on a current version, so I'm not going to write out click paths from memory. What I can point you to is ExifTool, a free command-line tool that runs on Windows too. Everything below is as ExifTool's official manual describes it. I haven't run any of it myself, because it's not installed on the machine I tested on.&lt;/p&gt;

&lt;p&gt;The manual covers three jobs. The first is listing everything a photo carries, grouped by where it lives in the file, and even there the manual warns that the list may still not show every piece of metadata. The second is removing just the location, which is the one most people need; the manual writes it as &lt;code&gt;exiftool -gps:all= photo.jpg&lt;/code&gt;, with your own file name in place of photo.jpg. The third is wiping all metadata at once, and the manual says not to do that to RAW files, DNG excepted, so I'd keep it for ordinary JPEGs and only when you really don't need the camera details either.&lt;/p&gt;

&lt;p&gt;One thing tripped me up just reading the docs: by default ExifTool keeps your untouched original next to the edited file, with &lt;code&gt;_original&lt;/code&gt; added to the name. So after removing the GPS tags, the folder has a clean &lt;code&gt;photo.jpg&lt;/code&gt; and a &lt;code&gt;photo.jpg_original&lt;/code&gt; with the coordinates still in it. Send the wrong one and you've done nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots are a different story
&lt;/h2&gt;

&lt;p&gt;A screenshot is a new image made from whatever is on your screen, and the screen has no idea where the photo was taken. My test screenshot on macOS came out as a PNG with a colour profile, an XMP packet and a tiny EXIF block, and no GPS at all. That makes "screenshot it, send the screenshot" a surprisingly decent low-tech trick, with two catches. You lose resolution, since you only get what fits on screen. And the picture itself can still give the place away. Google's help page makes the same point: even with location hidden, people can guess from landmarks in the photo. A plane in an airline's livery parked at a gate narrows things down a lot without any metadata.&lt;/p&gt;

&lt;p&gt;My habit now is dull but it works: before anything goes out, I open the exact file I'm about to send, not the original in my library, and check it with Show Location Info or the iPhone info panel. The copy is the thing that leaves, so the copy is what gets checked.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Getting the text out of five screenshots inside one PDF</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Sat, 12 Sep 2026 08:03:35 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/getting-the-text-out-of-five-screenshots-inside-one-pdf-8g2</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/getting-the-text-out-of-five-screenshots-inside-one-pdf-8g2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpi5nba7ydrbakxpujzsf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpi5nba7ydrbakxpujzsf.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The document is a three-page weekly ops report with five screenshots pasted in: a growth dashboard, a chat channel, a spreadsheet, an alert and a kanban board. I built it myself as a test file with invented content, because it's such a common shape of document: someone screenshots five tools and drops them into a report, and later someone else needs the words.&lt;/p&gt;

&lt;p&gt;Select-all and copy doesn't get you there. Pulling the text layer out with pypdf gives 20 lines: the body paragraphs, the five captions, and a header and footer on each page. Not a single word from inside a screenshot, because to the PDF those are just pictures.&lt;/p&gt;

&lt;p&gt;For one screenshot I'd use what the operating system already has, Live Text on a Mac or Text Actions in the Windows Snipping Tool: open the page, grab the image, copy. Five screenshots over three pages is where that gets old. So I dropped the whole PDF into ImgIng's Extract PDF content, left "Recognize text inside images" ticked, kept the OCR model on "Auto · recommended" and pressed Start. No account, and the workspace says the file is read only in the browser and never uploaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came back
&lt;/h2&gt;

&lt;p&gt;All five screenshots were read in one run. Each becomes a block labelled Image OCR, placed right after its screenshot and before the caption, in page order. In the TXT export the label is &lt;code&gt;[Image OCR]&lt;/code&gt;, and in Markdown it's a bold &lt;strong&gt;Image OCR&lt;/strong&gt; line followed by a &lt;code&gt;text&lt;/code&gt; code block. The four dashboard tiles came out perfectly and lined up with Tabs: the labels, then &lt;code&gt;12,480  3.2%  $41.70  1.8%&lt;/code&gt;, then the deltas. The chat came through with all four messages, right text, right speaker, except that Leo Park's name got glued to his timestamp both times (&lt;code&gt;Leo Park14:09&lt;/code&gt;) while Maya Chen's didn't. The spreadsheet was the best part: six rows, four columns, every cell right and Tab-separated, so it pastes straight into a sheet. The alert's five lines were right too, with the title losing the spaces around its dot (&lt;code&gt;Alert·payments-api&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Two screenshots didn't go as well. The bar chart under the dashboard tiles has seven values, one per day. The OCR kept every number but grouped them by bar height into two rows, &lt;code&gt;342 355 372 360&lt;/code&gt; and &lt;code&gt;310 295 _ _ 180 _&lt;/code&gt;, so which number is Monday's you can only tell from the picture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="English Extract PDF content workspace: the dashboard's bar chart (boxed in the result pane) and its OCR text, where " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the kanban board, the three column names and three of the four cards were right. The fourth card, OPS-416, came back as just its ID. Its title, "Retry budget per client", is nowhere in the output, and nothing marks it as missing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuisrc43j07tcwconvx47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuisrc43j07tcwconvx47.png" alt="The kanban screenshot with the OPS-416 card boxed, and the Image OCR block below it ending in a bare " width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One more thing went missing, and this one I can't explain. Each page of the report has a small header ("Ops Weekly Report · Week 36, 2026") and a footer ("1 / 3" and so on). None of those six lines appear in the TXT, Markdown or HTML. My guess is that text repeated identically on every page gets treated as furniture and dropped, but that's a guess; a two-column test file I made kept its headers and footers. For a weekly report I'd strip them anyway. If your header carries the only copy of the date, keep an eye on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking without rereading everything
&lt;/h2&gt;

&lt;p&gt;Since every recognised block is labelled, I only reread those, side by side with their screenshots, and I always take chart numbers from the image. Before rereading anything, though, I make a quick overview: for each Image OCR block in the TXT export, I note which caption follows it, how many lines it has, and what its very last line says. You can do this by searching for the label and scrolling, or with a few lines of script if you do this often. On this report the overview was five short rows. The dashboard block had 7 lines and ended on the weekday row. The chat had 9 lines and ended mid-sentence on the closing message about the error rate. The sheet had 7 lines ending on its Total row, the alert had 6 ending on the runbook line, and the ops board had 5 lines ending on a bare "OPS-416".&lt;/p&gt;

&lt;p&gt;The last line turns out to be a decent tell. A normal block ends on a sentence, a totals row or a label, because that's how the screenshot itself ends. A block that ends on a bare ticket number is a card whose text got cut, and that's how OPS-416 jumped out before I'd read any of the other 33 lines. It won't catch the bar chart, which ends on a perfectly normal row of weekdays, so the numbers above it look fine until you compare them with the bars; charts I just check by eye every time.&lt;/p&gt;

&lt;p&gt;So my routine is short: one screenshot, the OS's own text grab; several screenshots, the whole file in one pass, then straight to the Image OCR blocks. The extractor is at &lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Five image compressors, one feature checklist: batch, animation, formats, limits</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:03:59 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/five-image-compressors-one-feature-checklist-batch-animation-formats-limits-22am</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/five-image-compressors-one-feature-checklist-batch-animation-formats-limits-22am</guid>
      <description>&lt;p&gt;I collect tools the way some people collect keyboards, and image compressors have been a long-running category in that collection. This is not a benchmark of ratios, because on the same JPEG the popular tools land within a few percent of each other and the differences drown in the quality slider. It is a feature-by-feature walk through five of them, written for the moment when you already know you need to compress something and want to know which one will not stop you halfway.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Five image-compression tools compared (from public product info)" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where it runs. TinyPNG, compressor.io and docsmall upload your file to their servers and send the result back. Squoosh and ImgIng at imging.ai run the encoders inside the browser, Squoosh with its own WASM ports and ImgIng with WASM builds as well, so common formats never leave the machine. This matters for screenshots of internal tools, client material, and anything with a face in it, and it also decides who pays for the compute, which is why the limits below look the way they do.&lt;/p&gt;

&lt;p&gt;Batch. TinyPNG handles multiple files, docsmall handles multiple files, ImgIng handles folders. Squoosh and compressor.io take one image at a time. If you regularly receive a zip of forty product shots, this line alone narrows the list to three.&lt;/p&gt;

&lt;p&gt;Animated images. Of the five, ImgIng is the one that compresses animated GIF, APNG and animated WebP, using inter-frame differencing so that unchanged regions between frames are not stored twice. Squoosh does not open animated files, and none of the three upload services lists animation as a headline feature, so I did not test them on it. For anyone maintaining a docs site full of screen recordings converted to GIF, this is the feature that decides the question.&lt;/p&gt;

&lt;p&gt;Formats and encoders. Squoosh has the broadest encoder menu for a single image and the side-by-side viewer is the reference experience for tuning; it is the tool I use when I want to see exactly what a setting does to one file. It does not read HEIC. ImgIng covers PNG with its own colour-reduction quantiser, JPEG at MozJPEG-level output, WebP, and AVIF through libavif on the client, the same encoder family behind Squoosh's AVIF path, so I treat their AVIF output as comparable rather than ranking one above the other. compressor.io is the one in this list that handles SVG. docsmall adds PDF compression, which none of the others do.&lt;/p&gt;

&lt;p&gt;Controls. Most tools expose a single quality slider. ImgIng exposes two levers, quality and scale, which sounds minor until you are shipping a 4000-pixel photo to a 600-pixel card and realise that resizing does more than any quality setting. Squoosh also offers resize inside its pipeline. The server tools generally decide for you.&lt;/p&gt;

&lt;p&gt;Limits. TinyPNG's free plan is 500 images a month at 5 MB each. compressor.io allows 10 MB per file. docsmall allows 5 MB per file on the free tier and puts large files behind a membership. Squoosh has no limits. ImgIng has no monthly count, no watermark and no login, and if a file will not get smaller it returns the original rather than a larger file.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;TinyPNG&lt;/th&gt;
&lt;th&gt;compressor.io&lt;/th&gt;
&lt;th&gt;docsmall&lt;/th&gt;
&lt;th&gt;Squoosh&lt;/th&gt;
&lt;th&gt;ImgIng&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs in&lt;/td&gt;
&lt;td&gt;server&lt;/td&gt;
&lt;td&gt;server&lt;/td&gt;
&lt;td&gt;server&lt;/td&gt;
&lt;td&gt;browser&lt;/td&gt;
&lt;td&gt;browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch&lt;/td&gt;
&lt;td&gt;yes&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;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animated GIF/APNG/WebP&lt;/td&gt;
&lt;td&gt;not compared&lt;/td&gt;
&lt;td&gt;not compared&lt;/td&gt;
&lt;td&gt;not compared&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;not compared&lt;/td&gt;
&lt;td&gt;not compared&lt;/td&gt;
&lt;td&gt;not compared&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG / PDF&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;SVG&lt;/td&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free limit&lt;/td&gt;
&lt;td&gt;500/mo, 5 MB&lt;/td&gt;
&lt;td&gt;10 MB&lt;/td&gt;
&lt;td&gt;5 MB&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One number I will quote with its boundary attached: on a specific reference sample reduced to 256 colours, ImgIng's PNG-8 quantiser measured 45.8 dB PSNR. That is a single sample, not a claim of beating pngquant across the board, and I would expect the two to trade wins depending on the image. On the AVIF side, a test icon came out at 913 bytes from the client-side encoder, which mostly tells you that AVIF is small, not that one tool is magic.&lt;/p&gt;

&lt;p&gt;Where each one fits, as plainly as I can put it. TinyPNG for automated pipelines where the API and its 500-image runway matter more than anything else. compressor.io for a single large file or an SVG. docsmall for Chinese-speaking teams with PDFs in the mix. Squoosh for one image you want to tune by eye with every encoder available. ImgIng for folders, animated files, private material, and any workflow where you would rather not count. None of them is wrong; they are answers to different questions.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Image jobs you can finish in a browser tab without installing anything</title>
      <dc:creator>NullPointerZen</dc:creator>
      <pubDate>Thu, 10 Sep 2026 08:02:25 +0000</pubDate>
      <link>https://dev.to/yue_shu_c621a4a637f22396f/image-jobs-you-can-finish-in-a-browser-tab-without-installing-anything-4c24</link>
      <guid>https://dev.to/yue_shu_c621a4a637f22396f/image-jobs-you-can-finish-in-a-browser-tab-without-installing-anything-4c24</guid>
      <description>&lt;p&gt;I keep a mental list of image jobs that used to mean installing something heavy, and one by one they have moved into a browser tab. Not everything — this is not a full desktop editor replacement, and I will be honest about where it stops. But for a surprising range of everyday work a client-side canvas editor like Online Design (imging.ai) now covers it, with nothing uploaded and nothing installed. Here is the list, with how each one actually works rather than just what it is called.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Online Design workspace" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first is multi-image layout — collages, posters, banners. You create a canvas in the shape you need (square, portrait, landscape, or a custom size), import each image as its own layer, then move, scale, rotate at any angle, set opacity and reorder the stack. Up to 24 layers on one canvas is more than enough for a dense poster, and because whatever sits higher in the stack draws in front, arranging the composition is really just managing that order.&lt;/p&gt;

&lt;p&gt;The second is tidying up product shots, and two tools carry it. The 18 fine adjustments, alongside the 12 filter presets, handle colour and exposure correction. A separate fast background remover works by edge-connected colour removal — not AI. On a clean white-background product shot it lifts the subject cleanly; on a busy or gradient background it is the wrong tool, and knowing that boundary up front saves you the frustration of fighting it.&lt;/p&gt;

&lt;p&gt;The third is light portrait retouching. There is a skin-tone-aware beauty tool for smoothing, brightening and adding warmth — natural touch-ups, not reconstruction. It does not slim faces, does not reshape features, and does not identify anyone; it is not a stand-in for a full editor, and it will not force every lighting condition and skin tone into a single look. Used for what it is — gentle correction — it is quick, and the result still looks like the person rather than a rendered version of them.&lt;/p&gt;

&lt;p&gt;The fourth is multi-page material sets: a deck of matching social cards, a small catalogue, a run of banners. A project holds up to 24 pages, so you build them together with consistent sizing in one place, then export the whole set at once as a per-page ZIP instead of exporting each one by hand and hoping the dimensions match.&lt;/p&gt;

&lt;p&gt;The fifth is the plain format-and-export chores. Export to PNG, WebP or JPG, and choose between exporting the current page, a per-page ZIP for a whole project, or a high-resolution multi-page HTML bundle. Because editing is non-destructive, you can save the project as a .imging file and re-export it in a different format later without redoing any of the work.&lt;/p&gt;

&lt;p&gt;The common flow underneath all five is the same three steps: create a canvas and import your material as layers, arrange and adjust each layer or the whole canvas, then save the .imging or export. Once that loop is muscle memory, most of these jobs take a couple of minutes each and stop feeling like errands.&lt;/p&gt;

&lt;p&gt;Where it stops is worth stating plainly. This is not a heavy-duty retoucher or a vector tool, complex background removal still wants a real masking workflow, and 8192px is the ceiling. But the point of the list is that the floor has moved. A lot of "I need to install something" reflexes are now just a tab, and your images never leave the machine while you work through them.&lt;/p&gt;

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