The problem
Uploading product photos to a Shopify store in bulk runs into the same wall every time: the source files are huge (some HEIC straight off an iPhone, some JPG, several megabytes each), Shopify doesn't need full resolution, and Shopify's own bulk product importer wants a CSV with an image URL per row โ not a folder of raw files you drag in one at a time.
image-compressor-sharp is a small pair of Node.js scripts built to automate that pipeline: compress a folder of product photos in bulk, upload the results somewhere with public URLs, and hand back a spreadsheet of links shaped for exactly that CSV import step.
What it does
The tool is split into two scripts that run independently.
compress_image_mp.js walks a folder recursively, mirrors its structure into an output folder, and for every image it finds:
- Converts HEIC files to PNG first (via
heic-convert), since most compression/viewing pipelines โ and Shopify's importer โ don't handle HEIC directly - Caps resolution at a configurable megapixel limit (20MP by default) โ large photos get downscaled, smaller ones are left alone
- Re-encodes everything as WebP at a configurable quality setting (85 by default)
- Skips files that already have a compressed
.webpcounterpart, so re-running the script after adding new product photos doesn't redo existing work - Logs a size comparison for every file and writes it all to
Image_Sizes.xlsxat the end
dropbox_upload.js takes over from there: it walks the compressed output folder, recreates the same folder structure inside a Dropbox path, uploads each file (overwriting if it already exists), and generates a direct-download shared link for each one. Every filename and link gets written to image_links.xlsx โ a filename-to-URL mapping that lines up with the Image Src column Shopify's product CSV import expects.
Why this shape, specifically for Shopify
Shopify's bulk product importer doesn't accept raw image files โ it wants a CSV where each row carries a Handle (matching an existing product) and an Image Src URL it fetches the image from. Organizing photos into one subfolder per product in _Original_Images, running the compressor, then the uploader, produces exactly the filename โ public-URL mapping that CSV needs. The spreadsheet doesn't have to be built by hand from a folder of files after the fact โ it falls out of the pipeline.
Why two separate scripts instead of one
Splitting compression from upload means you can run the compressor, spot-check the output in _Compressed_Images/, and only kick off the (slower, network-bound) Dropbox upload once you're happy with the results. It also means a Dropbox auth failure partway through doesn't force you to recompress everything on retry โ dropbox_upload.js re-walks whatever's already sitting in the compressed folder.
Configuration is constants, not flags
There's no CLI argument parsing here โ settings like input/output folder names, the megapixel cap, WebP quality, and the Dropbox destination folder are plain constants at the top of each file:
const inputDir = '_Original_Images';
const outputDir = '_Compressed_Images';
const MAX_MEGAPIXELS = 20;
const OUTPUT_FORMAT = 'webp';
const QUALITY = 85;
const SAVE_EXCEL = true;
const DROPBOX_BASE_FOLDER = '/2025 02 14';
That's a deliberate tradeoff for a personal tool: editing a constant and re-running node compress_image_mp.js is simpler than remembering a flag syntax for something you might run once a month.
Stack
- sharp for the actual resize/re-encode work โ it's the fastest image library available for Node, backed by libvips.
- heic-convert to handle iPhone photos, which ship as HEIC by default.
- exceljs to generate the size-comparison and shared-link spreadsheets โ a plain
.xlsxis an easy handoff format, and its columns map directly onto Shopify's CSV import fields. - dropbox SDK plus
node-fetchfor the upload and shared-link generation. - dotenv for the Dropbox access token, kept out of the repo via
.env.
Running it
git clone https://github.com/prakharaug/image-compressor-sharp.git
cd image-compressor-sharp
npm install
# organize photos as one subfolder per product inside _Original_Images/, then:
node compress_image_mp.js
# once you're happy with the output in _Compressed_Images/:
node dropbox_upload.js
The end result is a _Compressed_Images folder full of right-sized WebP files, an Image_Sizes.xlsx showing exactly how much space each file saved, and โ after the upload step โ an image_links.xlsx with a direct-download link for every photo, ready to feed into Shopify's product CSV importer.
What it's not
This is a personal-scale utility, not a service โ there's no queueing, no retry-with-backoff on upload failures beyond what the Dropbox SDK does internally, and configuration means editing source constants rather than passing flags or a config file. It also doesn't talk to Shopify's API directly; it produces the spreadsheet, and the CSV import into Shopify is still a manual step. If you're processing a handful of product folders occasionally, that's a reasonable tradeoff. If you needed this running unattended on a schedule against a large, constantly-changing catalog, you'd want to add proper CLI args, error recovery, and a real Shopify API integration first.
Source
The code is on GitHub at github.com/prakharaug/image-compressor-sharp, MIT licensed.