<?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: Alex | Tshirts I Want</title>
    <description>The latest articles on DEV Community by Alex | Tshirts I Want (@alex_nguyen).</description>
    <link>https://dev.to/alex_nguyen</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%2F4024907%2Fc821fab2-94ad-4671-baf6-547988deb9c1.png</url>
      <title>DEV Community: Alex | Tshirts I Want</title>
      <link>https://dev.to/alex_nguyen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alex_nguyen"/>
    <language>en</language>
    <item>
      <title>Thumbnail vs Original — why most image downloaders fail on Etsy and Amazon</title>
      <dc:creator>Alex | Tshirts I Want</dc:creator>
      <pubDate>Wed, 05 Aug 2026 09:12:43 +0000</pubDate>
      <link>https://dev.to/alex_nguyen/thumbnail-vs-original-why-most-image-downloaders-fail-on-etsy-and-amazon-ok1</link>
      <guid>https://dev.to/alex_nguyen/thumbnail-vs-original-why-most-image-downloaders-fail-on-etsy-and-amazon-ok1</guid>
      <description>&lt;p&gt;I build graphic apparel designs at &lt;a href="https://tshirtsiwant.com/" rel="noopener noreferrer"&gt;Tshirts I Want&lt;/a&gt;, which means I spend a lot of time studying product photos on Etsy and Amazon.&lt;/p&gt;

&lt;p&gt;For a while I used an off-the-shelf image downloader. Then I opened one of the saved files and it was &lt;strong&gt;570 pixels wide&lt;/strong&gt;. The listing had shown me a crisp, zoomable photo — but what landed on my drive was a thumbnail.&lt;/p&gt;

&lt;p&gt;That turned out not to be a bug in that extension. It is what almost every image downloader does, and the reason is more interesting than it first looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: the URL you can see is not the URL you want
&lt;/h2&gt;

&lt;p&gt;The naive approach to downloading an image is to read &lt;code&gt;img.src&lt;/code&gt; and fetch it. On a modern e-commerce site that gives you the wrong file, for three separate reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;srcset&lt;/code&gt; means there are many URLs, not one.&lt;/strong&gt; The browser picks a candidate based on viewport and DPR. &lt;code&gt;img.src&lt;/code&gt; is just the fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. CDNs encode size in the URL.&lt;/strong&gt; &lt;code&gt;?w=400&amp;amp;dpr=2&lt;/code&gt; or a path segment. The original exists at the same address without those parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Big retailers hide the real file behind DOM attributes.&lt;/strong&gt; The &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; you see is deliberately small; the high-resolution URL sits in a data attribute that the zoom widget reads on demand.&lt;/p&gt;

&lt;p&gt;Point 3 is the one that bites hardest, and each site does it differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Amazon: the size is baked into the filename
&lt;/h2&gt;

&lt;p&gt;Amazon image URLs look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://m.media-amazon.com/images/I/71abcd1234._AC_SL1500_.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;._AC_SL1500_&lt;/code&gt; chunk is a stack of transformation flags. Strip it and you get the original upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://m.media-amazon.com/images/I/71abcd1234.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;media-amazon|images-amazon|ssl-images-amazon&lt;/span&gt;&lt;span class="se"&gt;)\.&lt;/span&gt;&lt;span class="sr"&gt;com$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\/&lt;/span&gt;&lt;span class="sr"&gt;images&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;I&lt;/span&gt;&lt;span class="se"&gt;\/[^/]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;?)\.[^/&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(\.(?:&lt;/span&gt;&lt;span class="sr"&gt;jpg|jpeg|png|gif|webp&lt;/span&gt;&lt;span class="se"&gt;))&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$1$2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// also drop the query string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex has to be careful: image IDs can contain &lt;code&gt;+&lt;/code&gt;, and there may be several flag groups. It matches the &lt;em&gt;last&lt;/em&gt; flag block before the extension rather than assuming a fixed shape.&lt;/p&gt;

&lt;p&gt;Amazon also exposes the real thing directly in the DOM, which is more reliable than rewriting a URL when it is available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;data-old-hires&lt;/code&gt; — a single hi-res URL&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-a-dynamic-image&lt;/code&gt; — a JSON map of &lt;code&gt;{ url: [width, height] }&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the lookup order is: read &lt;code&gt;data-old-hires&lt;/code&gt;, else parse &lt;code&gt;data-a-dynamic-image&lt;/code&gt; and take the largest area, else fall back to rewriting the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Etsy: one token in the path
&lt;/h2&gt;

&lt;p&gt;Etsy is simpler. The size lives in a filename token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;il_570xN.1234567890_abcd.jpg   →   il_fullxfull.1234567890_abcd.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/etsystatic&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;com$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/_&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+x&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;N|&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\.&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_fullxfull.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;(?:N|\d+)&lt;/code&gt; matters — Etsy uses both &lt;code&gt;il_570xN&lt;/code&gt; and &lt;code&gt;il_1588x1588&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: don't "clean" a signed URL
&lt;/h2&gt;

&lt;p&gt;The obvious generalisation is to strip every size-looking query parameter. That breaks a whole class of sites.&lt;/p&gt;

&lt;p&gt;Plenty of CDNs sign their URLs. The signature is computed &lt;strong&gt;over the full query string&lt;/strong&gt;, so deleting &lt;code&gt;w=400&lt;/code&gt; invalidates the token and you get a 403 instead of a bigger image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signedKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sig&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expires&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expiry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hmac&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;policy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;key-pair-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signedKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// hands off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detect a signature, return the URL untouched. A slightly smaller image beats a broken download.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two rendering problems worth knowing about
&lt;/h2&gt;

&lt;p&gt;Extracting the URL was only half the work. Injecting UI into arbitrary websites has its own hazards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page CSS will wreck your buttons.&lt;/strong&gt; My round 28px buttons rendered as ovals on some sites. Site stylesheets are full of rules like &lt;code&gt;button { width: 100% }&lt;/code&gt;, and &lt;code&gt;!important&lt;/code&gt; only gets you so far — there is always another property you did not think to lock down.&lt;/p&gt;

&lt;p&gt;The fix is to stop fighting and take the UI out of reach, in a shadow root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;hostEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;hostEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cssText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all: initial;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hostEl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;shadowRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hostEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachShadow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stylesheet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stylesheet has to be listed in &lt;code&gt;web_accessible_resources&lt;/code&gt; for the content script to load it this way. Page CSS now cannot reach the buttons at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product grids hide the image under a link.&lt;/strong&gt; On many listing pages the hover target is a transparent &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; stretched over the card, so &lt;code&gt;event.target&lt;/code&gt; is never the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; and no icons appear.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;document.elementsFromPoint()&lt;/code&gt; solves it — it returns the whole stack under the cursor, so you can look past the overlay:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elementsFromPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IMG&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isVideoContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;isVideoContext&lt;/code&gt; check exists because YouTube renders its poster frame as a CSS background image, and without it the extension offered to download the "image" of a video player.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zipping in the browser without a library
&lt;/h2&gt;

&lt;p&gt;Batch download produces a single archive. Pulling in JSZip for this felt heavy, and the ZIP format's stored (uncompressed) mode is genuinely small to implement: a local header per entry, a central directory, an end-of-central-directory record, and CRC-32 over each file.&lt;/p&gt;

&lt;p&gt;Images are already compressed, so &lt;code&gt;STORE&lt;/code&gt; costs almost nothing in size and keeps the writer to a couple of hundred lines with no dependency.&lt;/p&gt;

&lt;p&gt;One MV3 detail: &lt;code&gt;URL.createObjectURL&lt;/code&gt; is not available in a service worker, so the archive is built in the popup and handed to &lt;code&gt;chrome.downloads.download()&lt;/code&gt; from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the extension does today
&lt;/h2&gt;

&lt;p&gt;It ended up as &lt;strong&gt;Image Downloader &amp;amp; Collector&lt;/strong&gt;, free, on Chrome, Edge, Firefox, Opera and Brave:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hover any image, or right-click it, to download the &lt;strong&gt;original&lt;/strong&gt; or save it to a folder&lt;/li&gt;
&lt;li&gt;A switch to hide the hover icons entirely — the right-click menu keeps working, so nothing is lost&lt;/li&gt;
&lt;li&gt;Folders you can file images into while browsing, without downloading anything yet&lt;/li&gt;
&lt;li&gt;Batch export a collection as one ZIP, folders preserved, with a &lt;code&gt;sources.csv&lt;/code&gt; recording where every image came from&lt;/li&gt;
&lt;li&gt;Everything local: &lt;code&gt;chrome.storage.local&lt;/code&gt;, no account, no server, no tracking
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Image Downloader &amp;amp; Collector - Batch Save"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"downloads"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contextMenus"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"host_permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;all_urls&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source-tracking part came from a user who asked for it after the first release — they wanted to trace a saved reference back to the listing it came from months later. Their images already had the data attached; the extension had been recording it since v1.0.0 and simply never showed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome Web Store / Edge / Firefox add-ons&lt;/strong&gt; — search "Image Downloader &amp;amp; Collector"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source and build setup:&lt;/strong&gt; &lt;a href="https://github.com/alex-nguyen-tiw/image-downloader-collector" rel="noopener noreferrer"&gt;github.com/alex-nguyen-tiw/image-downloader-collector&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Built by &lt;a href="https://tshirtsiwant.com/" rel="noopener noreferrer"&gt;Tshirts I Want&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you know another site that hides its originals behind an unusual attribute, tell me in the comments — site-specific rules are cheap to add and that is exactly the kind of thing that is hard to discover alone.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>webextension</category>
    </item>
    <item>
      <title>How I Built a Free Image Downloader &amp; Collector Extension for Designers &amp; Graphic Apparel Creators</title>
      <dc:creator>Alex | Tshirts I Want</dc:creator>
      <pubDate>Sat, 25 Jul 2026 16:33:13 +0000</pubDate>
      <link>https://dev.to/alex_nguyen/how-i-built-a-free-image-downloader-collector-extension-for-designers-graphic-apparel-creators-39li</link>
      <guid>https://dev.to/alex_nguyen/how-i-built-a-free-image-downloader-collector-extension-for-designers-graphic-apparel-creators-39li</guid>
      <description>&lt;p&gt;As a developer and creator involved in graphic apparel design at &lt;a href="https://tshirtsiwant.com/" rel="noopener noreferrer"&gt;Tshirts I Want&lt;/a&gt;, I constantly find myself browsing visual inspiration across platforms like Pinterest, Behance, and Unsplash. &lt;/p&gt;

&lt;p&gt;Whether researching graphic t-shirt artwork, vintage typography, or visual design mockups, the repetitive process of right-clicking and manually saving images one by one felt incredibly slow. &lt;/p&gt;

&lt;p&gt;Existing solutions were either bloatware, hidden behind subscriptions, or required running heavy desktop apps. So, I decided to build a lightweight, open-source solution: &lt;strong&gt;Image Downloader &amp;amp; Collector&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  🚀 What Does It Do?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Image Downloader &amp;amp; Collector&lt;/strong&gt; is a browser extension built with &lt;strong&gt;Manifest V3&lt;/strong&gt; designed to make visual asset collection effortless for designers, creators, and shoppers.&lt;/p&gt;

&lt;p&gt;Key features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  ⚡ &lt;strong&gt;Mouse Hover Quick Action Bar&lt;/strong&gt;: Hover over any image on any webpage to instantly reveal &lt;strong&gt;Quick Download&lt;/strong&gt; and &lt;strong&gt;Save to Collection&lt;/strong&gt; action buttons.&lt;/li&gt;
&lt;li&gt;  🔖 &lt;strong&gt;Custom Folder Bookmarking&lt;/strong&gt;: Pin images directly into custom collection folders (&lt;em&gt;e.g., T-Shirt Ideas, Wallpapers, Graphic Artwork&lt;/em&gt;) without downloading right away.&lt;/li&gt;
&lt;li&gt;  📦 &lt;strong&gt;1-Click ZIP Batch Downloader&lt;/strong&gt;: Access your dashboard to select and download an entire collection folder as a single ZIP archive.&lt;/li&gt;
&lt;li&gt;  🔍 &lt;strong&gt;High-Res Source Detection&lt;/strong&gt;: Automatically parses &lt;code&gt;srcset&lt;/code&gt;, &lt;code&gt;data-src&lt;/code&gt;, and un-scaled CDN parameters to fetch the highest-resolution original image available.&lt;/li&gt;
&lt;li&gt;  🔒 &lt;strong&gt;Privacy-First&lt;/strong&gt;: 100% local processing with &lt;code&gt;chrome.storage.local&lt;/code&gt;. Zero user tracking.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🛠️ Tech Stack &amp;amp; Architecture
&lt;/h3&gt;

&lt;p&gt;Built with pure Vanilla JavaScript, HTML5, and CSS3 following modern Manifest V3 guidelines:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
json
{
  "manifest_version": 3,
  "name": "Image Downloader &amp;amp; Collector - Batch Save",
  "permissions": ["storage", "downloads"],
  "host_permissions": ["&amp;lt;all_urls&amp;gt;"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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