<?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: yue xing</title>
    <description>The latest articles on DEV Community by yue xing (@token111).</description>
    <link>https://dev.to/token111</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%2F4087986%2F86b82555-4629-45b8-9c3a-6743e1932cb9.png</url>
      <title>DEV Community: yue xing</title>
      <link>https://dev.to/token111</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/token111"/>
    <language>en</language>
    <item>
      <title>My GIF frames said 60 ms, not the 66 ms I set. That's when I started reading the format</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Mon, 21 Sep 2026 08:06:58 +0000</pubDate>
      <link>https://dev.to/token111/my-gif-frames-said-60-ms-not-the-66-ms-i-set-thats-when-i-started-reading-the-format-2gon</link>
      <guid>https://dev.to/token111/my-gif-frames-said-60-ms-not-the-66-ms-i-set-thats-when-i-started-reading-the-format-2gon</guid>
      <description>&lt;p&gt;I set every frame of my test animation to 66 ms. When I read the file back, every frame said 60 ms.&lt;/p&gt;

&lt;p&gt;That is not a rounding bug in my code. GIF stores frame delay in hundredths of a second, so 66 ms has no representation in the file at all — the nearest it can hold is 6 units. Frame intervals in a GIF are always multiples of 10 ms. That was the point where I stopped blaming my screen recorder for the file size and started reading the format instead.&lt;/p&gt;

&lt;p&gt;I'm a CS senior and I needed a short demo clip for my thesis mid-term review. 4.5 seconds of a back-office UI, exported as a GIF, came out at 1.5 MB — too big for the group chat, too big to drop into the document. Lowering the resolution and the frame rate bought me about a third after an evening of fiddling.&lt;/p&gt;

&lt;p&gt;I couldn't publish the real recording, so I drew a fictional one frame by frame with Pillow: 960×600, 75 frames, 60 ms each, 4.5 seconds — a table filling in row by row, a cursor moving to a filter, a confirmation dialog. I drew it at 2× and scaled it down, the way a retina capture would be exported; without that step the frames are flat color blocks and the GIF comes out unrealistically small. On disk: 1,585,112 bytes. Self-made and fictional, not a screenshot of any real product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every frame carries its own color table
&lt;/h2&gt;

&lt;p&gt;A frame is one still image in the animation; a color table (palette) is the list of colors that frame may use, and each pixel stores an index into that list rather than a color. GIF caps the list at 256 entries, which made sense in 1987 when displays couldn't show more anyway.&lt;/p&gt;

&lt;p&gt;The expensive part is the second rule: the table isn't shared across the file, so every frame may carry its own. In my sample, 74 of the 75 frames each carry a 256-entry palette. At 3 bytes per entry that's roughly 57 KB spent on "which colors is this frame allowed to use" before a single pixel of picture is counted.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rectangle per frame, and that's it
&lt;/h2&gt;

&lt;p&gt;GIF does have interframe optimization. It's called partial refresh: a frame repaints one rectangular region and inherits everything outside it from the previous frame. That region is usually called the dirty rect. The catch is in the singular — one rectangle per frame.&lt;/p&gt;

&lt;p&gt;My recording has three things moving at once: the pointer on the left, a clock ticking in the table header on the right, a blinking caret in the search box. Three corners of the screen, one rectangle to cover all of them, so the rectangle swells to most of the canvas. The numbers: 68 of the 75 frames did use partial refresh, and the average dirty rect still covered 59% of the canvas. On a full-screen gradient sample, 0 of 30 frames could use it at all — 100% repainted every frame.&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="Dirty-rect share across four self-made samples: screen recording 59%, gradient 100%, already-compressed 74%, dashboard demo 90.5%" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A video encoder splits the picture into blocks and asks where each block moved from. GIF can only say "repaint this rectangle". Encoding the same 75 frames locally with ffmpeg at H.264 default settings gives 82,628 bytes, which makes the GIF 19.2× larger — one sample, one setting, and a sample that is mostly static, so expect that ratio to drop hard on fast-moving footage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the savings actually are
&lt;/h2&gt;

&lt;p&gt;For the format comparison I used the animation workshop on ImgIng (&lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt;), mostly because all four animated formats export from the same page with everything else untouched, which keeps the variables clean. It also runs in the browser — I kept the Network panel open through every run and never saw a file upload request.&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%2Fuif0qnkt31rqmxmh35i1.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%2Fuif0qnkt31rqmxmh35i1.png" alt="One 4.5-second recording stored six ways: original GIF 1,585,112 B, re-encoded GIF 983,087, animated WebP 1,045,924, APNG 1,392,726, animated AVIF 157,490, local H.264 82,628" width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I expected WebP to be the answer. It wasn't, at least not here: 1,045,924 bytes, 34% off the original, about the same as re-encoding the GIF. APNG is lossless — its frames are pixel-identical to the source — so it stays at 1.33 MB. Animated AVIF is the one that moves the needle: 157,490 bytes, 9.94% of the original. On a gradient sample the ranking flips again, with WebP down 76% and GIF down only 14%. Why WebP does so well on gradients and so little on a screen recording is the part I still haven't worked out.&lt;/p&gt;

&lt;p&gt;One more result worth knowing. I took a GIF I had already squeezed once — scaled to 70%, every second frame dropped, palette cut to 64 colors, 357,648 bytes — and re-encoded it as GIF: 356,964 bytes. 684 bytes saved, frames pixel-identical. Re-encoded as AVIF instead, it still dropped another 62%. "Already optimized, can't compress further" is only true inside the same format.&lt;/p&gt;

&lt;p&gt;I'm not going to tell you to switch everything to AVIF. I verified decode and playback in three desktop browser engines and nothing else — no messaging apps, no old phones, no in-app webviews — and the tool's own result card notes that its animated AVIF carries no transparency. So I decide by where the file is going: if I control the player, AVIF; if someone else does, WebP; and if it has to stay a GIF, drop frames, the cheapest change at 28.8% off, remembering that it also makes the animation play twice as fast.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Screenshots of a paper's figure gave me about half the pixels the PDF stores</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Sun, 20 Sep 2026 08:06:53 +0000</pubDate>
      <link>https://dev.to/token111/screenshots-of-a-papers-figure-gave-me-about-half-the-pixels-the-pdf-stores-5gjm</link>
      <guid>https://dev.to/token111/screenshots-of-a-papers-figure-gave-me-about-half-the-pixels-the-pdf-stores-5gjm</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%2Fuuh7oksgfzhwblvhfrjh.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%2Fuuh7oksgfzhwblvhfrjh.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;br&gt;
My default way of grabbing a figure from a paper has always been a screenshot of the PDF viewer, and the figure always looks a bit soft once it's on a slide or zoomed in a notebook. I assumed that was just what figures in papers look like. It isn't. I tried to measure how much I was losing on a real paper, and the answer for the figure I checked was roughly half the width.&lt;/p&gt;

&lt;p&gt;The paper is RAG-Safety-Bench by Adithiyan Rajan Indira Saravanan and Kathleen C. Fraser (arXiv:2609.11758), 24 pages, published under CC BY 4.0, which is why I can show its figures here. I looked at the bar chart on page 6. Inside the PDF it's stored as an image of 1564×1080 pixels. On the page it's only printed about 272 points wide (a point is 1/72 inch), so the viewer draws it squeezed into a small box. Rendering the whole page at 216 DPI and cutting the figure out gave me 818×565. A screenshot at normal reading zoom lands in the same range: the chart as displayed, not the chart as stored. Zooming way in first gets more pixels, but those are the viewer's resampled copy, and honestly I never bother doing that per figure.&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="Page-6 figure of arXiv:2609.11758 (Indira Saravanan &amp;amp; Fraser, CC BY 4.0). Left: extracted at 1564×1080. Right: page rendered at 216 DPI and cropped, 818×565. Both at actual pixel size" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking every figure, not just one
&lt;/h2&gt;

&lt;p&gt;One figure could be a fluke, so I used PyMuPDF to go through the whole paper and write down three numbers for every image it stores: its stored size in pixels, how wide it's printed on the page in points, and roughly how many pixels a 216 DPI render would give it, which is just the printed width divided by 72 and multiplied by 216. Each image is counted once, on the first page it appears. Before trusting the estimate I checked it against the page-6 crop above: 816 predicted vs 818 measured, the 2 px difference being my crop box.&lt;/p&gt;

&lt;p&gt;There are 10 images. The page-1 figure is stored at 1476×786 but printed 218 pt wide, so a 216 DPI render gives it about 655 px, 44% of what's stored. The page-6 chart gets about 816 px of its 1564, 52%. Page 8 has two: 1779×1080 printed 363 pt wide gets 61%, and 1473×1280 printed 218 pt wide is back at 44%. The last six, on page 18 and pages 20 to 24, are all around 1960 px wide and all printed across the full 454 pt text width, and even those only get about 1361 px, 69% of their stored width.&lt;/p&gt;

&lt;p&gt;Then I compared each file I extracted against the pixels stored in the PDF: convert both to RGB, subtract one from the other, and check that the difference is empty. All 10 of 10 matched. Pairing files with images by order works here because the extractor numbered them in the order they first appear, which I only confirmed by that 10/10.&lt;/p&gt;

&lt;p&gt;Why the authors saved everything at 1.4 to 2.3 times the printed size, I don't really know. My guess is a high export DPI in their plotting setup, but that's a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the stored version
&lt;/h2&gt;

&lt;p&gt;The extracted files came from ImgIng's Extract PDF images, with the whole 24-page PDF dropped in at once. It found 10 images, on pages 1, 6, 8 (two), 18 and 20 to 24, which matches my count, and saved them as one ZIP. Inside, the names run from "-image-001.png" to "-010.png" after the paper's ID, with no page numbers, so I kept the card list, or my own notes of the page numbers, next to the folder. The files are PNGs whose bytes differ from what's in the PDF, but every pixel is the same, which is what the comparison above checks.&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%2Fnupvexr76lvicsua76l6.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%2Fnupvexr76lvicsua76l6.png" alt="Extract PDF images on the full paper: 10 results, 2.12 MB; the figure on page 1 is pixelated in this screenshot" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This only works for figures that are bitmaps inside the PDF. Plenty of LaTeX papers include plots as vector PDF or EPS, and then there is no image to extract; when I tried a test page I'd drawn with nothing but shapes on it, the extractor reported 0 results. For those the fix goes the other way: render the page at a high DPI (I'd use 300) and crop, and since vector shapes stay sharp at any resolution, that's fine. I haven't yet tried a real paper with vector figures, so I can't say how common each case is. Also, if a publisher downsampled the figures before building the PDF, extraction can only give back what the PDF holds.&lt;/p&gt;

&lt;p&gt;Licence is the part I used to skip. This paper is CC BY 4.0, so reusing its figures is allowed as long as the authors and source are credited, which is why the figure caption above names them. Lots of arXiv papers don't use CC BY; the licence line on the abstract page is where I look before putting a figure in anything that other people will see. The extractor I used is at &lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt; if you want to try it on a paper you're allowed to reuse.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>My scanned PDF had to be under 2 MB. The hard part was deciding which 2 MB</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Sat, 19 Sep 2026 08:06:35 +0000</pubDate>
      <link>https://dev.to/token111/my-scanned-pdf-had-to-be-under-2-mb-the-hard-part-was-deciding-which-2-mb-44bn</link>
      <guid>https://dev.to/token111/my-scanned-pdf-had-to-be-under-2-mb-the-hard-part-was-deciding-which-2-mb-44bn</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%2Fn3o2tsd361ugujrclho6.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%2Fn3o2tsd361ugujrclho6.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
An application portal says "PDF only, max 2 MB". My first question was how to shrink the file. The question that actually mattered came later: which 2 MB?&lt;/p&gt;

&lt;p&gt;I'm a CS senior, and portals with this kind of limit show up everywhere in my life right now: competitions, summer programs, internship forms. I didn't want to experiment on real documents with my personal details in them, so I generated a fake one: a fictional four-page "cooperation intent registration form", each page rendered as a 300 dpi color image with a beige background, noise and a slight tilt so it looks like a phone scan. The file is 8,948,995 bytes. The text on the pages is Chinese, which doesn't matter for anything below.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, figure out what kind of PDF you have
&lt;/h2&gt;

&lt;p&gt;A scanned PDF is just pictures of pages. There is no text layer, meaning there is no real, selectable text underneath: you can't highlight a word or find it with Ctrl+F. A PDF exported from Word or Google Docs does have a text layer. The quickest test is to search for a word you know is on the page. If search finds nothing and you can't select anything, you're holding a scan, and nearly all of its weight is image data. That's good news, because images are the part a compressor can actually shrink.&lt;/p&gt;

&lt;p&gt;DPI is dots per inch, how many pixels are packed into each inch of paper. An A4 page at 300 dpi is 2480×3508 pixels, far more than anyone needs to read a form on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four presets, one sensible answer
&lt;/h2&gt;

&lt;p&gt;I used the PDF compressor on ImgIng (&lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt;), which runs in the browser. With DevTools open on the Network tab, I saw no request sending the file anywhere while it compressed and saved, which I cared about for anything carrying ID numbers. It offers four presets. Here is what my fake scan turned into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screen, 72 dpi: 347 KB. Small, and the letter edges go soft and smudgy.&lt;/li&gt;
&lt;li&gt;eBook, 150 dpi: 1,927,707 bytes, shown as 1.84 MB, 78.5% smaller. Text stays crisp.&lt;/li&gt;
&lt;li&gt;Print, 300 dpi: 8.46 MB. Only 0.8% smaller, since the scan was already 300 dpi and there was nothing to downsample.&lt;/li&gt;
&lt;li&gt;Lossless structure: pixels untouched, size basically unchanged.&lt;/li&gt;
&lt;/ul&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="ImgIng PDF compressor with the eBook 150 dpi preset outlined in red and the result card reading " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screen preset is tempting because it's the smallest number. Zoom in and it stops looking tempting. I compared crops of the same paragraph from each result, plus macOS's built-in Quartz filter "Reduce File Size" (the one you can pick when exporting from Preview). To be exact, I ran that same system filter file directly and did not click through Preview's export dialog to double-check. It produced 2,905,274 bytes, which is still over the limit.&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%2Fhoyvbakhthokjys4e4cj.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%2Fhoyvbakhthokjys4e4cj.png" alt="Detail crops of the same scanned page: original 8.53 MB, Screen 72 dpi 347 KB with soft edges, eBook 150 dpi 1.84 MB, and macOS Reduce File Size 2.77 MB (red box, still over 2 MB). Self-made fictional form; sizes use 1 MB = 1024 KB" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2 MB question
&lt;/h2&gt;

&lt;p&gt;The compressor shows 1.84 MB, but that counts 1 MB as 1,048,576 bytes. By the decimal definition, the same file is 1.93 MB. So "2 MB" on a portal could mean 2,000,000 bytes or 2,097,152 bytes, and the page almost never says which. I can't tell you what any specific portal checks, so I compare the raw byte count against the stricter number. 1,927,707 is under both. A result between 2,000,000 and 2,097,152 bytes is the dangerous zone, and I'd treat it as too big.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the PDF is mostly text
&lt;/h2&gt;

&lt;p&gt;I also tried a ten-page text-only PDF of 137,782 bytes. All four presets gave the exact same 131,156 bytes, 4.8% smaller. Fonts made up 85% of that file, and this tool doesn't subset fonts (strip out the glyphs that are never used), so there wasn't much left to squeeze. That's one sample, so I'm not claiming every text PDF behaves like this. If yours is still over the limit, I'd go back to the source document rather than keep trying presets. I haven't tested those routes, so I can't put numbers on them.&lt;/p&gt;

&lt;p&gt;My checklist now is short: search the PDF to tell scan from text, pick 150 dpi for scans, check the byte count against 2,000,000, then zoom to 200% and read the smallest line before uploading.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The screenshot got blurry because of how it was shrunk, not because of the compressor</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Fri, 18 Sep 2026 08:07:05 +0000</pubDate>
      <link>https://dev.to/token111/the-screenshot-got-blurry-because-of-how-it-was-shrunk-not-because-of-the-compressor-2j0e</link>
      <guid>https://dev.to/token111/the-screenshot-got-blurry-because-of-how-it-was-shrunk-not-because-of-the-compressor-2j0e</guid>
      <description>&lt;p&gt;Assignment portals love a file size cap. Mine is 2 MB per attachment, and the habit I picked up was simple: drag the screenshot into whatever viewer is open, halve the dimensions, save as JPG, done. It passes every time. What finally made me look closer was someone asking me to resend a screenshot because the terminal output in it was unreadable. Zooming in, the red command-line text had smeared together and the edges of the small labels were fuzzy. My first guess was that the compressor was bad. It was not. The blur came from the resize, which happens before any compressor gets involved.&lt;/p&gt;

&lt;p&gt;I rebuilt the test with a sample I could share. It is a fake "course assignment portal" page I rendered myself, all invented content, 1280x800, 94.0 KB as PNG.&lt;/p&gt;

&lt;p&gt;Path one, the habit: scale to 50%, save JPG at quality 80. That gives 20.1 KB, which is 21% of the original, and a PSNR of 27.0 against the source. PSNR here just means comparing the two images pixel by pixel and reporting how far apart they are, higher being closer to the original; anything above 40 is usually indistinguishable.&lt;/p&gt;

&lt;p&gt;Path two: leave the pixel dimensions completely alone and only reduce the number of colors. That gives 37.6 KB, 60% smaller, at PSNR 60.7, and the glyphs are essentially pixel-identical.&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="Three ways of compressing the same 1280x800 UI screenshot: the 94.0 KB original, 37.6 KB after quantizing to 208 colors, and 20.1 KB after scaling to 50% and saving JPG, with the boxed region magnified 3x underneath. Sample: a self-built UI screenshot with invented content; quantization by the imging web app, scaling and JPG by local Pillow" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two separate losses stack up in path one. The resize resamples two neighbouring pixels into one, and in a UI screenshot a stroke is often only one or two pixels wide, so that detail is simply gone before encoding starts. Then JPEG quantizes in small blocks and leaves low-frequency residue exactly where dark text meets a light background, which is the grey haze you see around the letters. Photographs barely notice either step because they are continuous gradients. Screenshots carry almost all of their information in those hard edges, so they take the full hit.&lt;/p&gt;

&lt;p&gt;Color quantization works on screenshots for the opposite reason: a UI uses very few distinct colors. White background, grey rules, two or three text colors, a button. Quantizing builds a limited palette and stores an index per pixel, so nothing moves and letter shapes survive. I ran the screenshot through the image compression tool on imging at its default setting and got a 208-color indexed PNG at 37.6 KB, PSNR 60.7 and SSIM 1.000. Locally, Pillow down to 256 colors landed at 37.3 KB, so the win comes from the image type, not from one clever implementation. Going to WebP at quality 80 gives 31.6 KB, but that is back on a lossy path.&lt;/p&gt;

&lt;p&gt;What I now do, by image type:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;What to use&lt;/th&gt;
&lt;th&gt;Measured on my samples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Screenshots, flat icons, line art&lt;/td&gt;
&lt;td&gt;Keep the size, quantize to an indexed PNG&lt;/td&gt;
&lt;td&gt;94.0 KB to 37.6 KB, 60% off, PSNR 60.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photographs&lt;/td&gt;
&lt;td&gt;WebP or JPEG, pick quality for a target size&lt;/td&gt;
&lt;td&gt;At about 150 KB: JPEG q23 PSNR 33.7, WebP q65 37.6, AVIF q59 38.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anything with transparency&lt;/td&gt;
&lt;td&gt;Export to WebP or AVIF&lt;/td&gt;
&lt;td&gt;35.2 KB badge to 20.2 KB WebP, 255 alpha levels kept; AVIF 2.4 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The photo sample is a CC0 image from Wikimedia Commons shot on an iPhone 6, not one of mine, scaled to 2048x1536. The transparent badge with a drop shadow is something I drew for the test. Saving that badge as JPEG destroys it outright, since JPEG has no alpha channel at all and the transparent area becomes a solid block; composited over a light background it scored PSNR 1.2. I have not checked how AVIF behaves on older devices or browsers, so for anything I hand to someone else I still pick WebP.&lt;/p&gt;

&lt;p&gt;On tooling I have low standards: no signup, nothing to install, and let me choose the output format. imging runs in the browser tab, and for image compression and format conversion the file stays local rather than being uploaded, though the first use of some formats downloads a decoder, which is a download and not an upload. Two limits worth knowing. It will not shrink everything: a gradient background I threw at it came out larger, so it fell back and returned the original file with the interface saying the source was already the smallest. And photos at the default setting barely move, 638.7 KB down to 578.0 KB, about 10%, so you have to lower the quality yourself.&lt;/p&gt;

&lt;p&gt;The check that takes five seconds: before attaching, zoom the image to 300% and look at the smallest line of text. If it is still crisp and you never touched the dimensions, send it. &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>Same numbers, different rulebook: why a re-saved photo loses its color</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Thu, 17 Sep 2026 08:23:55 +0000</pubDate>
      <link>https://dev.to/token111/same-numbers-different-rulebook-why-a-re-saved-photo-loses-its-color-a46</link>
      <guid>https://dev.to/token111/same-numbers-different-rulebook-why-a-re-saved-photo-loses-its-color-a46</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%2Fcs8gx7qxllw136rlwxuy.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%2Fcs8gx7qxllw136rlwxuy.png" alt=" " width="800" height="327"&gt;&lt;/a&gt;&lt;br&gt;
A photo gets exported, resized, or converted to WebP, and afterwards the reds look tired and the sky looks a bit chalky. Nothing is blurry, nothing is blocky, it just lost some color. I went down this rabbit hole while reading about image pipelines, and the explanation turned out to be simpler than I expected: the pixel numbers didn't change, the rulebook for reading them went missing. One scope note up front. Different screens can show different ranges of color, and that alone can make a photo look different from one device to another. I didn't test screens at all, so everything below is only about what happens inside the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same numbers, different rulebook
&lt;/h2&gt;

&lt;p&gt;Each pixel is stored as three numbers for red, green and blue. Those numbers don't mean a color by themselves. Think of a recipe that says "2 cups of flour" without saying whose cups: a US cup and a metric cup are close, but not the same, and the cake comes out different. In an image file, the note that says which cup you're using is the color profile (ICC profile). It tells software which color standard the numbers should be read in. sRGB is the default almost everywhere. Display P3 and Adobe RGB cover a wider range, and Apple's developer documentation mentions that iPhone cameras can capture P3 starting with the iPhone 7. Because the range is wider, a saturated red in P3 is written with smaller numbers than the same red in sRGB. If the profile gets dropped, software falls back to reading those smaller numbers as sRGB, and the red comes out duller.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much color actually goes away
&lt;/h2&gt;

&lt;p&gt;I wanted a number instead of a feeling. I took a CC0 photo from Wikimedia Commons (an airport apron shot on an iPhone 6), scaled it to 2048×1536, and used Pillow to convert it to Display P3 and embed the profile. Then I made a copy with exactly the same pixels and no profile, and compared both as they'd be shown when read as sRGB.&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="Same pixels: left read with the Display P3 profile, right after the profile is dropped and read as sRGB. Sample: CC0 photo from Wikimedia Commons, shot on an iPhone 6" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On this sample, average chroma dropped by 16.6%. The average color difference (CIELAB ΔE76) across the whole image was only 1.83, which sounds small until you look at where it lands. For the most saturated 5% of pixels, ΔE was about 5.4, so the tail fin and the sky take the hit, while the white fuselage, the grey tarmac and the shadows barely move. I repeated it with Adobe RGB and it was worse: 20.1% less chroma on average, around 7.2 ΔE in the most saturated areas. One caveat I can't get around: this photo's colors all sit inside sRGB. A real wide-gamut photo with colors outside sRGB could behave differently, and I have no data for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it tends to happen after re-saving
&lt;/h2&gt;

&lt;p&gt;The profile is extra data attached to the file, not part of the pixels, so every tool that re-saves the image decides whether to copy it over. Starting from the P3 JPEG and using default settings, Pillow 11.3 dropped the profile when saving JPEG or WebP, and also after resize or thumbnail followed by a JPEG save, but kept it for PNG. ffmpeg 7.1 kept it for JPEG and PNG and dropped it for WebP. macOS sips kept it when converting to PNG, scaling, or saving at quality 60. None of this is a bug as far as I can tell; in Pillow, passing the original profile through the &lt;code&gt;icc_profile&lt;/code&gt; argument keeps it. It's just not the default, and I still don't know why PNG and JPEG behave differently there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do to avoid it
&lt;/h2&gt;

&lt;p&gt;Start from the original file whenever possible instead of a copy that has already been exported a few times. Keep the number of re-saves low, since each tool in the chain is another chance to lose the profile. And if the image is headed for the web, convert the colors to sRGB during processing rather than just stripping the profile. Converting rewrites the pixel values themselves, so even if some later step throws the profile away, reading the file as sRGB still gives the right colors.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>When an upload form says “unsupported file”, the iPhone photo is usually HEIC</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Wed, 16 Sep 2026 08:09:56 +0000</pubDate>
      <link>https://dev.to/token111/when-an-upload-form-says-unsupported-file-the-iphone-photo-is-usually-heic-nna</link>
      <guid>https://dev.to/token111/when-an-upload-form-says-unsupported-file-the-iphone-photo-is-usually-heic-nna</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%2Fzzfh781cqcabc0xlhkgn.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%2Fzzfh781cqcabc0xlhkgn.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;br&gt;
It's a pattern that shows up every application season: a form asks for a photo, someone picks a picture straight off their iPhone, and the site answers "unsupported file type". Or the upload bar finishes and the preview is a broken-image icon. The photo opens fine on the phone, so it's not obvious what went wrong. In most of these cases the file is HEIC, not JPEG, and converting it to JPG fixes the upload. The part that trips people up afterwards is that the converted JPG can end up bigger than the original.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell it's HEIC
&lt;/h2&gt;

&lt;p&gt;HEIC is the default photo format on iPhones since iOS 11. It stores the same picture in fewer bytes than JPEG, using HEVC compression. Look at the file extension: &lt;code&gt;.heic&lt;/code&gt; or &lt;code&gt;.HEIC&lt;/code&gt;. Windows hides extensions by default (File Explorer → View → File name extensions); on a Mac, Get Info shows the kind as "HEIF Image". Apple's documentation also says photos may be converted to JPEG when transferred to a Mac or PC, unless Settings → Apps → Photos → Transfer to Mac or PC is set to "Keep Originals". That's why two people with the same phone can have different results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why other machines and browsers can't show it
&lt;/h2&gt;

&lt;p&gt;A viewer needs a decoder for the format. Apple ships one in iOS and macOS. On Windows, Microsoft says you need both the "HEIF Image Extensions" and the "HEVC Video Extensions" from the Store, and the HEVC one may be paid.&lt;/p&gt;

&lt;p&gt;For browsers, I tested a sample rather than guessing. I didn't have a shareable iPhone original, so I took a CC0 photo from Wikimedia Commons that was shot on an iPhone 6 and encoded it to HEIC with macOS's built-in &lt;code&gt;sips&lt;/code&gt;. So it is not a straight-from-camera file, though it has the same 512×512 tile-grid layout and rotation property iPhone photos use. In the browser engines bundled with Playwright on macOS 26.5:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chromium 149: &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; fails, &lt;code&gt;createImageBitmap&lt;/code&gt; throws&lt;/li&gt;
&lt;li&gt;Firefox 151: same failure&lt;/li&gt;
&lt;li&gt;WebKit 26.5: displays, 2448×3264, correct orientation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebKit works because it leans on the macOS system decoder. I didn't test real Safari, Windows, or phones. But if a form tries to preview or measure the image in a Chromium-based browser, it has nothing to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting to JPG
&lt;/h2&gt;

&lt;p&gt;On a Mac, Preview's File → Export to JPEG works, and &lt;code&gt;sips -s format jpeg x.heic --out out/&lt;/code&gt; does several files at once. One detail: the sips output keeps the pixels landscape (3264×2448) and writes EXIF Orientation 6, relying on the viewer to rotate it.&lt;/p&gt;

&lt;p&gt;Without a Mac, a browser-based converter avoids installing Store extensions. I used imging with the same sample. The first time you open a HEIC it can't decode it natively, so there's a "Load decoder" button. That downloads a WASM decoder built on libheif; the photo itself stays in the browser.&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="imging's " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things to watch. Before loading, the bottom bar says "Server processing required"; after loading it changes to "Instant processing · Runs in your browser; the source image is never uploaded". And WebP is preselected, which most upload forms reject, so click JPG before "Convert &amp;amp; Download". It takes one file at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check size and orientation before uploading
&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%2Fc9bpxkyhszcw6y0gkdl3.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%2Fc9bpxkyhszcw6y0gkdl3.png" alt="Conversion result: 745.1 KB HEIC became a 1.01 MB JPG, flagged as 38% larger than the source (red boxes). Same CC0 sample" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The portrait sample was 745.1 KB. At the default quality of 88 the JPG came out at 1.01 MB, 38% larger. That makes sense, since HEIC compresses better than JPEG. If the form caps uploads at 1 MB, drop quality to 80: the same photo became 742.2 KB. A 4032×3024 version went from 840.1 KB to 1.32 MB at quality 88 (+61%) and 982.9 KB at 80 (+17%).&lt;/p&gt;

&lt;p&gt;For orientation, imging's output was 2448×3264 with Orientation set to 1, so the pixels are already upright. With tools that only set the EXIF flag, a system that ignores EXIF could show a portrait photo sideways. I haven't tested any upload systems for that, so I just open the file and look before uploading.&lt;/p&gt;

&lt;h2&gt;
  
  
  So it doesn't happen again
&lt;/h2&gt;

&lt;p&gt;Settings → Camera → Formats → Most Compatible. Apple says new photos will then be saved as JPEG (and videos as H.264). Existing photos stay HEIC, so you'll still need to convert those.&lt;/p&gt;

&lt;p&gt;The converter I used 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>My background-removed clip passed the first-frame check and lost a person by the 10-second mark</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Tue, 15 Sep 2026 08:09:04 +0000</pubDate>
      <link>https://dev.to/token111/my-background-removed-clip-passed-the-first-frame-check-and-lost-a-person-by-the-10-second-mark-443p</link>
      <guid>https://dev.to/token111/my-background-removed-clip-passed-the-first-frame-check-and-lost-a-person-by-the-10-second-mark-443p</guid>
      <description>&lt;p&gt;![ ](&lt;a href="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/eMy" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/eMy&lt;/a&gt; habit with any video tool has been to look at the first frame of the output, scrub to the middle, and call it done. If the start is right, the model has "found" the person and the rest will follow, or so I assumed. I've been reading up on video matting lately, specifically RVM, which carries a recurrent state from one frame to the next, and that assumption seemed even safer for a model with memory. So I tested one case I expected to break at the start, and one I expected to be hard all the way through. Neither behaved the way I predicted.&lt;/p&gt;

&lt;p&gt;The first test was a clip that starts with nobody in it. I don't have footage like that, so I built it: two seconds of an empty street (a crop of road surface from a CC0 Commons clip, scaled up and held still), then a hard cut straight into "Tarun speaking 01", a CC0 clip of a man talking at an event. I spliced it with ffmpeg, so it's an empty shot cutting to a person already standing there, not someone walking into frame. I ran it through the imging video matting page on the Compatible model with a pure green background, and I fully expected the first frames after the cut to be broken, with the model still "warming up".&lt;/p&gt;

&lt;p&gt;They weren't. The 120 empty frames came out as solid green, never more than 2 levels off pure #00FF00. On the very first frame after the cut the whole man was there. I compared it with a separate run of the same clip without the empty intro, where the person is there from frame 0. Measured on the edge region, the two differed by 0.028 on average. That's smaller than the 0.042 difference I got between them later in the clip where nothing special is happening, which is mostly noise from estimating the matte back out of a green H.264 frame. Within what I can measure, the blank opening changed nothing. One clip, one model, so I'm not saying more than that.&lt;/p&gt;

&lt;p&gt;The second test was a crowd. "Folkloristic dance in Naples (tammorriata)" is 12.84 seconds of street musicians with a crowd behind them and a slowly panning handheld camera, CC0 again, same model, same green. On the first frame the lead singer on the left is there, mostly solid. At 6 seconds he's there too, a bit see-through, which I'd have forgiven. By my old habit of first frame plus a look at the middle, this clip passes. The end of the clip doesn't. He isn't faded there, he's gone, and the left half of the frame, where he's still standing and singing in the source, has no foreground at all.&lt;/p&gt;

&lt;p&gt;So now I sample instead of scrubbing. &lt;code&gt;ffmpeg -i out.mp4 -vf fps=1 frames/%03d.png&lt;/code&gt; gives one frame for every second, 13 of them for this clip, and I put source and output next to each other in a grid. One thing I didn't know: with fps=1, ffmpeg doesn't hand you the frame at exactly 0 s, 1 s, 2 s. When I checked the timestamps with its showinfo filter, it had picked frames roughly half a second into each second. For eyeballing that doesn't matter, but my first labels on the grid were off by half a second.&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="The Naples clip sampled once per second, source above output; tiles where under 2% of the left half is kept are outlined in red" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Footage: Wikimedia Commons "Folkloristic dance in Naples (tammorriata)" (CC0), Compatible model, green background.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The grid shows a lot more than the two frames I used to check. He starts fading within the first three seconds, is down to a faint shape around 3 s, comes back around 5 to 6 s, and from about 10 s to the end of the clip he's not there at all. Over the same 13 seconds, the front row on the right stays solid in every tile. There's no scene cut anywhere in this clip, only the camera slowly panning.&lt;/p&gt;

&lt;p&gt;What I don't understand yet is why he doesn't come back at the end the way he did at 5 seconds. My guess is that the recurrent state, which is what keeps edges steady from frame to frame, can also keep a wrong decision going: once he's treated as background for a while, he stays background, and the state only gets wiped clean on a scene cut, which this clip never has. That's a guess from reading about how RVM works, not something I've verified. I also only ran the small model, so I can't say whether the bigger ones do better on this clip.&lt;/p&gt;

&lt;p&gt;What I changed in how I check results is short. The first frame tells me very little, and neither does a random frame in the middle. I sample one frame per second over the whole clip and look at the grid, and for anything important I look at the last few seconds on purpose, since that's where this one broke. A one-per-second grid can still miss something that goes wrong and recovers within a second, and I haven't figured out a cheap way to catch that without looking at every frame.&lt;/p&gt;

&lt;p&gt;The matting page I used is at &lt;a href="https://imging.ai/" rel="noopener noreferrer"&gt;https://imging.ai/&lt;/a&gt;.&lt;br&gt;
e448513dtbbhlyoxfiu.png)&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A photo's location is stored in the file, which is why a screenshot doesn't have it</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Mon, 14 Sep 2026 08:18:09 +0000</pubDate>
      <link>https://dev.to/token111/a-photos-location-is-stored-in-the-file-which-is-why-a-screenshot-doesnt-have-it-3mck</link>
      <guid>https://dev.to/token111/a-photos-location-is-stored-in-the-file-which-is-why-a-screenshot-doesnt-have-it-3mck</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%2Fhavgkmo9p9bg5q8f4lmo.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%2Fhavgkmo9p9bg5q8f4lmo.png" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;br&gt;
For a long time my mental model of photo location was a database one. The phone keeps a list of photos, each photo has a "place" field, and the map in the gallery app reads that field. Under that model, sending someone the photo sends the picture, and the place stays behind in the phone's database. It's a reasonable guess, and it's wrong in the way that matters most: the location is written into the image file itself, so it travels with every copy of that file, and only disappears when some program writes a new file without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually lives
&lt;/h2&gt;

&lt;p&gt;A JPEG from a phone is not just compressed pixels. Near the start of the file there is a metadata block called EXIF, and inside it are several small tables of tags. One table describes the camera and exposure. Another, pointed to by tag 0x8825, is the GPS block: latitude, longitude, altitude, sometimes a timestamp. The pixels come after all that in the same file. So "the photo" and "the photo's location" are literally neighbours in one sequence of bytes, and anything that copies the file byte for byte (email attachment, USB stick, cloud folder sync) copies both.&lt;/p&gt;

&lt;p&gt;To see it for myself I used a real photo I'm allowed to share: "Avions à l'aéroport de Madrid - 2015" from Wikimedia Commons, which is released under CC0 and was taken on an iPhone 6. Then I made two derived files. One is a screenshot of that photo displayed at 1000×750 in a headless browser. The other is a copy resized to 1200 px with macOS's built-in sips tool, the kind of "make it smaller before sending" step a lot of people do. For each of the three files I opened it with Pillow in Python, read the camera model from the main EXIF table, then followed tag 0x8825 into the GPS block and read the latitude.&lt;/p&gt;

&lt;p&gt;The original is 3264×2448, says iPhone 6, and has a latitude of 40 degrees, 29 minutes, 27.76 seconds north, which is 40°29′27.76″N. The screenshot is 1000×750 and has neither a camera model nor a latitude. The resized copy is 1200×900, still says iPhone 6, and still has 40°29′27.76″N. It's about 37% of the original's width and says exactly the same thing, down to the hundredth of a second of arc. That was the result I didn't expect. I'd assumed resizing counted as "making a new photo".&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="Original, a screenshot of it, and a copy exported after a sips resize: GPS is 40°29′27.76″N, none, and 40°29′27.76″N. Photo: Wikimedia Commons, CC0 (iPhone 6); tested 2026-09-11" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the screenshot is different
&lt;/h2&gt;

&lt;p&gt;The screenshot looks almost the same as the original, yet it has no GPS and not even a camera model. Once I thought about how it's made, that stopped being surprising. A screenshot isn't a copy of the photo file. The screenshot tool takes the pixels currently being displayed and encodes them into a brand-new PNG, and the only metadata in that PNG is whatever the screenshot tool decides to write. The display never had the GPS block in the first place; it only ever received pixels. I also checked a screenshot from the macOS system screenshot tool (of my screen, not of this photo), not only the browser one: the PNG it produced had a colour profile, some XMP and a very small EXIF block, and no GPS in any of them.&lt;/p&gt;

&lt;p&gt;A resized copy works on the same principle but with a different outcome. The resizing program reads the old file, makes new pixels, and writes a new file, and at that point it chooses what metadata to carry over. sips chose to carry the GPS over. Pillow's default save, in the tests I ran on the same photo, writes no EXIF at all, so its copy came out with no location. Neither behaviour is "correct"; they're just different programs making different choices, and the file name or format tells you nothing about which one happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I still don't know
&lt;/h2&gt;

&lt;p&gt;Phone gallery apps let you hide or change a photo's location, and I wanted to know whether that edits the file or just the app's own records. I don't know. Apple's documentation for the Photos app talks about hiding the location and reverting to the original, which suggests the original value is kept somewhere, but it doesn't say where, and I'd rather not guess. Also, the screenshot shows an airport apron with airline logos on the planes, which is its own kind of location information that no metadata tool will remove.&lt;/p&gt;

&lt;p&gt;So the practical version of all this for me is small. Location is a property of a specific file, not of "the photo" in the abstract. Before I send or upload something, I check that exact file, the one I'm about to attach, by reading its GPS block the same way as above, instead of trusting that some earlier step must have stripped it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>OCR on my scanned lecture notes dropped ŷ and swapped the minus signs</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Sat, 12 Sep 2026 08:05:56 +0000</pubDate>
      <link>https://dev.to/token111/ocr-on-my-scanned-lecture-notes-dropped-y-and-swapped-the-minus-signs-1mna</link>
      <guid>https://dev.to/token111/ocr-on-my-scanned-lecture-notes-dropped-y-and-swapped-the-minus-signs-1mna</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%2Fs5hvosjkfrwnkjomxvbl.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%2Fs5hvosjkfrwnkjomxvbl.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;br&gt;
Most of my course notes are phone photos or scans of a whiteboard saved as PDFs, and before I trust OCR with a whole semester of them I wanted to know what it gets wrong. So I made a two-page test PDF that looks like scanned lecture notes: images only, no text layer, the course name as a running header, page numbers "- 2 -" and "- 3 -" at the bottom, one formula "e = y − ŷ" with a real minus sign, and a scatter plot with a caption. The content is made up, which is the point: I know exactly what every character should be.&lt;/p&gt;

&lt;p&gt;I ran it through ImgIng's Extract PDF content with the OCR model left on Auto, which picks the Professional model on a desktop. I only ran the default and I'm not comparing models here. What follows is everything I had to fix, in the order I'll check it next time.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The formula lost a symbol completely
&lt;/h3&gt;

&lt;p&gt;The source line is "2.2 Residual = observed − predicted, written e = y − ŷ." and it came out as "2.2 Residual = observed – predicted, written e = y – ." The ŷ didn't turn into a plain y or into some wrong letter. It's just gone, leaving a minus sign pointing at a full stop. Before blaming OCR I zoomed into the page image in case my font had no glyph for ŷ, and it renders fine. I haven't figured out why the model drops it instead of guessing something. If I hadn't written the formula myself, "y – ." would have been easy to read straight past.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Minus signs came back as en dashes
&lt;/h3&gt;

&lt;p&gt;Both minus signs (U+2212) came back as en dashes (U+2013), the one in "observed − predicted" as well. They look nearly identical on screen, and that's the problem: searching my notes for − finds nothing, and if the text ends up in LaTeX or code it's the wrong character.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The header repeats, the page numbers are gone
&lt;/h3&gt;

&lt;p&gt;"Statistical Learning · Lecture Notes" appears at the top of both pages with the spaces around the dot missing. That's faithful to the scan, but in one merged notes file it's clutter. Page numbers went the other way: neither "- 2 -" nor "- 3 -" is anywhere in the output, even though you can see "- 2 -" on the page image in the result pane. I want those back in notes, because they're how I find where something was in the original.&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 on a two-page synthetic lecture-notes scan: the formula line " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The plot is only a caption, and everything sits in a code block
&lt;/h3&gt;

&lt;p&gt;The scatter plot has no text in it, so there's nothing to read, and the caption "Figure 2-1: scatter plot and fitted line" survives. Fine, but notes that say "see Figure 2-1" need the picture saved separately. Also, each page is one Image OCR block, and in the Markdown export that block is a plain-text code fence. Good for knowing what was machine-read, bad for notes, since headings and lists inside a code fence don't render. I copy the text out of the fence before editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a simple check catches, and what it doesn't
&lt;/h2&gt;

&lt;p&gt;I don't want to find these by eye across a whole semester of notes, so I wrote a short Python check that reads the exported text, splits it into pages at each "Page N" line, and looks for four things. First, any line that ends in an operator with nothing after it, maybe followed by a full stop, because that's the shape a dropped symbol leaves behind. Second, every non-ASCII character on every line, printed with its code point and Unicode name, so an en dash can't pass for a minus. Third, a page with no line that is just a number, optionally between dashes. Fourth, any line from page 1 that shows up on every page, as a guess at the running header.&lt;/p&gt;

&lt;p&gt;On this file it caught the dangling minus in "written e = y – .", named the en dash as U+2013 EN DASH, and flagged both pages as having no page number. It also listed the middle dot U+00B7 in the header, which is harmless but at least tells me which dot I'm dealing with. The repeated-line check found the header, and it also flagged "Lecture 2 Linear Regression", which is a real title that happens to be on both pages. It can't tell a running header from a repeated heading, and I haven't worked out a rule that could without knowing where on the page each line sat, so I still read that list myself.&lt;/p&gt;

&lt;p&gt;It's a short list for two pages. But the ŷ, the dashes and the missing page numbers are three I'd never have caught by skimming, because the text around them reads perfectly well. The tool I used 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>Compressing images for my first portfolio site: which tool fit which job</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:07:15 +0000</pubDate>
      <link>https://dev.to/token111/compressing-images-for-my-first-portfolio-site-which-tool-fit-which-job-5g6d</link>
      <guid>https://dev.to/token111/compressing-images-for-my-first-portfolio-site-which-tool-fit-which-job-5g6d</guid>
      <description>&lt;p&gt;My first portfolio site loaded in nine seconds on a phone, and the whole reason was images. Twenty-two photos straight off the camera, a handful of GIF screen recordings, and a logo exported as a 2 MB PNG. Lighthouse was blunt about it. So I spent a weekend learning how image compression actually works, and I ended up using four different tools for different parts of the job. This is the map I wish I had at the start.&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;The first thing I learned is that "compress" hides two separate operations. One is lowering quality, which throws away detail the eye is unlikely to notice. The other is scaling down, which throws away pixels the layout never displays. A 4000-pixel photo shown in a 700-pixel column is wasting most of itself before quality even enters the picture, and scaling did more for my page than any slider. The second thing I learned is that tools split into ones that upload your file to a server and ones that do the work inside your browser, and that split explains most of their differences.&lt;/p&gt;

&lt;p&gt;For understanding what the sliders do, Squoosh from Google was the right classroom. It runs in the browser, is free with no account, and shows the original and the compressed version side by side with a draggable divider. I spent an hour dragging that divider on one photo at different quality values and different formats, and that hour taught me more than any article. It handles one image at a time and does not open animated files, but as a place to learn what JPEG, WebP and AVIF look like at various settings, it is where I would send anyone.&lt;/p&gt;

&lt;p&gt;For the twenty-two photos as a batch, I switched to ImgIng at imging.ai. It also runs in the browser, so the photos stayed on my laptop, and it lets you drop the whole folder, set a quality value and a scale value together, and download the folder back. No login, no watermark, no monthly count. The scale lever is what I had been missing: I set a maximum width for the gallery and every photo shrank to fit before it was compressed. It also handled the GIF screen recordings, which most tools either refuse or leave untouched, by compressing across frames. If a file would not get smaller it left the original alone, which I appreciated after Squoosh had once given me a WebP bigger than the source at a high setting.&lt;/p&gt;

&lt;p&gt;For a couple of one-off large files, I used the server-based tools. compressor.io took a 9 MB scan that other free tiers refused, since its free ceiling is 10 MB per file, and it is the one in my list that handles SVG. TinyPNG is the name every tutorial mentions, and its free plan of 500 images a month at 5 MB each is more than a portfolio needs; the reason I did not use it for the gallery was simply that I did not want to upload personal photos when a local option existed. docsmall, which I found through a Chinese forum, does batch and PDF on a 5 MB free ceiling with a membership for larger files, and would be the pick if I were compressing a PDF résumé alongside the images.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Tool I used&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Learning what settings do to one image&lt;/td&gt;
&lt;td&gt;Squoosh&lt;/td&gt;
&lt;td&gt;side-by-side viewer, all encoders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A folder of photos and GIFs&lt;/td&gt;
&lt;td&gt;ImgIng&lt;/td&gt;
&lt;td&gt;batch, scale + quality, animation, stays local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One 9 MB scan or an SVG&lt;/td&gt;
&lt;td&gt;compressor.io&lt;/td&gt;
&lt;td&gt;10 MB free ceiling, SVG support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated uploads later on&lt;/td&gt;
&lt;td&gt;TinyPNG&lt;/td&gt;
&lt;td&gt;mature API, 500 free per month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A PDF in the same pile&lt;/td&gt;
&lt;td&gt;docsmall&lt;/td&gt;
&lt;td&gt;PDF plus batch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few beginner mistakes worth flagging. I once compressed an already-compressed JPEG three times and wondered why it looked muddy; compress from the original export each time. I also assumed AVIF was always the answer because the files were tiny, then discovered an older browser on a friend's phone did not display them, so I now keep a JPEG fallback. And "runs in the browser" is a per-format promise, not a blanket one: common formats stay local, but check the tool's own notes before assuming anything sensitive never left your machine.&lt;/p&gt;

&lt;p&gt;The site now loads in under two seconds on the same phone. The tools mattered less than knowing which job each one was built for, and that turned out to be the actual lesson of the weekend.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I made our club's poster without opening a design app</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Thu, 10 Sep 2026 08:04:58 +0000</pubDate>
      <link>https://dev.to/token111/i-made-our-clubs-poster-without-opening-a-design-app-54hg</link>
      <guid>https://dev.to/token111/i-made-our-clubs-poster-without-opening-a-design-app-54hg</guid>
      <description>&lt;p&gt;I am not a designer. When our student club needed a poster for an event, my instinct was to download some heavy design app and I dreaded the afternoon of learning it before I could draw a single box. Instead I made the whole thing in a browser tab and it took under an hour. I am writing down what I did, partly so the next person on the committee can just follow it instead of starting from the same panic I did.&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 tool was Online Design (imging.ai), which runs in the browser — nothing I dragged in got uploaded anywhere, which mattered because the photos were of actual members. The first step was creating the canvas. It offers square, portrait and landscape presets, or a custom size; I picked portrait since it was going on a noticeboard and would double as a phone-screen share in the group chat.&lt;/p&gt;

&lt;p&gt;Then I added content as layers. Each image you bring in becomes its own layer you can move, scale, rotate to any angle and reorder, so I dropped in a background photo, put the club logo on top, and layered the event photos into a loose grid. You can stack up to 24 layers, which sounds like a lot until you are nudging things around and realise how fast they add up. Getting the stacking order right is most of the work: whatever sits higher in the list draws in front of everything below it.&lt;/p&gt;

&lt;p&gt;The raw photos looked flat, so I turned to the adjustments — there are 12 filter presets and 18 finer controls. I put a warm preset across the whole canvas to tie the mismatched photos together, then bumped brightness and contrast on the one dim photo by adjusting just that layer. Being able to correct one layer without touching the others was the thing I did not expect to need and then leaned on constantly, because the photos had come from four different phones.&lt;/p&gt;

&lt;p&gt;One group photo had someone lit harshly, so I tried the beauty tool. It is skin-tone aware and does gentle smoothing, brightening and warmth, and I want to be clear about what it is: it did not slim anyone or change their features, it does not recognise faces, and it is not a full photo editor. It just softened the harsh lighting a little. I kept it light on purpose, because overdoing it looks obviously fake, and it will not make every skin tone read the same way regardless of the light it was shot in.&lt;/p&gt;

&lt;p&gt;The last step was export. I saved the finished poster as a PNG for print quality and a smaller WebP for the group chat. There is also a per-page ZIP if you build several pages, but I only needed the one. Because it is non-destructive, I saved the project as a .imging file too, so when the date changed the following week I reopened it and edited that layer instead of rebuilding the poster from scratch.&lt;/p&gt;

&lt;p&gt;So the whole path, if you are copying it: create a canvas at the size you need, import your images and text as layers and arrange them, adjust colour on individual layers or the whole canvas and do any light touch-up, then export as PNG or WebP. Four moves, and none of them needed a manual open in another tab.&lt;/p&gt;

&lt;p&gt;What surprised me was how little I actually had to learn. I expected tools and panels and a vocabulary; what I really needed was to understand layers and the idea that adjustments stack on top rather than getting burned into the pixels. If you have been putting off a poster because opening a design app feels like a project of its own, it genuinely is not one anymore.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>After converting a PDF to a webpage, can you still select and search the text</title>
      <dc:creator>yue xing</dc:creator>
      <pubDate>Wed, 09 Sep 2026 08:06:30 +0000</pubDate>
      <link>https://dev.to/token111/after-converting-a-pdf-to-a-webpage-can-you-still-select-and-search-the-text-1n1a</link>
      <guid>https://dev.to/token111/after-converting-a-pdf-to-a-webpage-can-you-still-select-and-search-the-text-1n1a</guid>
      <description>&lt;p&gt;Converting a PDF to a webpage, my biggest worry was the text turning into an image — then you can't select, copy or search it, and converting is pointless for note-taking. Testing it out, a good PDF-to-HTML &lt;strong&gt;stays selectable and searchable&lt;/strong&gt;, and understanding how tells you when it won't.&lt;/p&gt;

&lt;p&gt;The result first: the converted webpage lets you select text with the mouse, hit it with Ctrl+F, and copy-paste. It does this with a "two layers stacked" trick — what you &lt;strong&gt;see&lt;/strong&gt; is graphics rebuilt from PDF coordinates (vector outlines of the text, so it looks like the original), and over that sits an &lt;strong&gt;invisible, transparent selectable-text layer&lt;/strong&gt;. Your selecting, searching, copying hits that transparent text; your eye sees the graphics beneath. Aligned and stacked, you get "looks like the original, yet selectable and searchable".&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="PDF to HTML operation page" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A nice detail: it handles even &lt;strong&gt;Type 3 glyphs&lt;/strong&gt;. Type 3 is a PDF font defined by drawing instructions; plenty of tools just treat it as an image and lose selectability. Here it's rebuilt from its original drawing instructions (CharProcs) into reusable graphic symbols, then the transparent text layer over it — so even these special glyphs stay selectable. On a formula- and symbol-heavy PDF the difference is stark: selectable versus view-only is a huge gap for notes.&lt;/p&gt;

&lt;p&gt;But a boundary worth knowing: &lt;strong&gt;whether the font can be packaged into the webpage depends on the font's own permission&lt;/strong&gt;. It only inlines fonts marked "embedding allowed"; ones whose license forbids embedding aren't force-packed. So a rare PDF using a restricted font may display slightly differently on another computer (but selecting and searching still work, since those ride on the transparent text layer, not the font). That's normal respect for font licensing, not a bug.&lt;/p&gt;

&lt;p&gt;And the big one for us students: &lt;strong&gt;scanned PDFs are the exception&lt;/strong&gt;. If your PDF is scanned or photographed from paper, each page is already an image with no text data inside — converting it to a webpage keeps it an image, unselectable and unsearchable, not the tool's fault, the source simply has no text. To make a scan selectable you need OCR first, a separate step. Easy test: if you can select text in the original PDF, you can after converting; if you can't (one drag selects a whole page), it's an image and stays one. It also doesn't claim "perfect / pixel-exact" — complex graphics and restricted fonts have limits, flagged on the page. So for &lt;strong&gt;printed, text-based PDFs&lt;/strong&gt; (papers, lecture notes, reports), convert away — selectable, searchable, copy straight into your notes. Tool is ImgIng (imging.ai).&lt;/p&gt;

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