Show HN: OnlyJPG – Client-Side PNG/HEIC/AVIF/PDF/etc to JPG
onlyjpg.comTL;DR: private, in-browser converter that turns pretty much any image file format into standard JPEGs. Everything runs locally. No uploads.
This started as a five-minute job and forty hours later...
I wanted to convert a HEIC without uploading it anywhere, so I wrestled Emscripten/WebAssembly to run Google's Jpegli inside a Web Worker. Now there's a small UI and it handles a bunch of formats.
Just about the only thing it can't decode is JXL - but there's still some JPEG XL magic in there: XYB perceptual color quantization is enabled by default via Jpegli.
The upside of all this over-engineering is privacy and compatibility: images are processed entirely on your machine and never touch a server; the output is a regular JPEG that works everywhere.
I could have used a CLI, sure — but where's the fun in that?
Would love feedback on edge cases and defaults.
Tested on Firefox, Chrome, and Safari.
Cheers!
Note: please don't turn your screenshots and digital art into JPG. JPG uses compression based on natural lighting. It works well for photos, but it's the wrong solution where run-length encoding will do much better (e.g. in screenshots). Black text (or cartoon art) on white backround always looks lousy when converted to JPG.
What should we use instead?
For final export? AVIF, JpegXL, maybe even WebP (lossless mode).
PNG kinda sucks for high resolution stuff because decoding is extremely slow. The way PNG does lossless compression also only really works with flat graphic design, anything with gradients or texture blows up the file size.
Generation Loss: JPEG, WebP, JPEG XL, AVIF [1]
Generation loss: FLIF vs WebP vs BPG vs JPEG vs MozJPEG [2]
[1] - https://www.youtube.com/watch?v=FtSWpw7zNkI
[2] - https://www.youtube.com/watch?v=YKmhZJ8H1Fc
>> What should we use instead?
> AVIF, JpegXL, maybe even WebP (lossless mode).
Definitely not lossless AVIF. It is less efficient than lossless WebP. WebP is supported everywhere, but is not much more efficient than optimized lossless PNG. Lossless JPEG XL has the best lossless compression but can't be used for web without fallbacks.
So, for offline archival: JPEG XL.
For web use without fallbacks: lossless WebP.
For web use with lossless WebP fallback: JPEG XL.
>> What should we use instead?
> AVIF, JpegXL, maybe even WebP (lossless mode).
Definitely not lossless AVIF. It is less efficient than lossless WebP. WebP is supported everywhere, but is not much more efficient than optimized lossless PNG. Lossless JpegXL has the best lossless compression but can't be used for web without fallbacks.
So, for offline archival: JpegXL.
For web use without fallbacks: lossless WebP.
For web use with lossless WebP fallback: JpegXL.
PNG
I was unsatisfied with the discussion of transparency. You mention to "jump to png", but I just scrolled to the heading and it tells me why jpg is better, in some cases, than png, not how to satisfy turning a png with transparency into a competent jpeg analog.
Of course, if you meant "use the png format" instead of "look at the header", that's likewise unhelpful.
Not sure if there's actually a suggestion in here. I entered the site thinking "if you want a jpg, this is the app". Any question I had about other formats was moot because this is the app for jpgs. But then you discussed the other formats so it seemed like you had some workarounds or ways to use jpgs in place of the other formats, and that left me unsatisfied whereas your saying nothing probably wouldn't have? Again, not sure if there's anything actionable in there, just letting you know my experience with the site.
You're totally right.
My weak attempt at being humorous fell completely flat.
I should have taken a page from the HN ethos and left sarcasm and kidding out of the copy completely.
Working on it now!
This is really cool, but I found one thing in testing it out: it seems to do really poorly with WebP images.
I've run some WebP images from reddit through your site and they all end up looking terrible. If I run them through Squoosh.app (MozJPEG, 75%), I get the expected minimal degradation in quality and a 30%ish reduction in size. If I run the same image through your site (which seems hard-coded to Q90 for quality), I get a minimal reduction in size (if any; to be expected given Q90) and it looks incredibly pixelated.
If I give it a jpeg, it works great: a large reduction in size and I can't even notice a difference (which is kinda what I expect for Q90).
EDIT: If I toggle on the "legacy mode" YCbCr (instead of XYB), the WebP files work great. If I get the jpeg from reddit instead of the webp, XYB is fine.
Try an image like https://www.reddit.com/r/PeterExplainsTheJoke/comments/1o8wo... (you can right click and download the image to get the webp or "download linked file" to get the jpeg). If I put the jpeg into your site with XYB, there's very slight degradation and a 67% savings. If I put the webp in, it looks like it was resized to half or a quarter of the size and then enlarged back up (and it gained 57%). With webp and YCbCr, it gained 63% over the webp, but the image quality is good.
So there's something with the WebP images and XYB that seems like it isn't working.
EDIT 2: I just grabbed an AVIF I have around to test and the XYB seems to have the same problem there as well.
Can I ask which browser you're using?
I was unable to reproduce the exact same issue you described here on Firefox 128 on Linux... however, you're definitely onto something!
It's a combination of two subtle issues:
1) A known bug in the jpegli library. As you suspected, XYB mode combined with chroma subsampling (like the old 4:2:0 default) can cause the exact "downscaled" look you saw. [0]
2) A sizing bug in my decoder pipeline. I was mistakenly using an image's internal codedWidth instead of its final displayWidth. For some WebP/AVIF files, this created a smaller image on a larger canvas, which magnified the XYB bug.
I've just pushed a fix that addresses both issues. I've also updated the defaults to what I believe is a much better starting point: Quality 80, XYB, and 4:4:4 subsampling. This combination avoids the bug while still leveraging the best perceptual model, so it should prevent those surprising quality drops.
Thanks again for the incredibly detailed report-it was super helpful!
There's an unbelievable amount of edge cases with all of these different image formats. It's been a great learning experience for sure!
[0] https://github.com/google/jpegli/issues/122
There is also https://squoosh.app/.
Absolutely, I'm a huge fan of Squoosh! It's a phenomenal tool and a big inspiration for this project.
The main difference is that OnlyJPG is laser-focused on one thing: creating the best possible, universally compatible JPG using Google's Jpegli encoder.
Surprisingly, Squoosh doesn't use Jpegli yet.
And it's a brilliant example of a PWA.
IIRC, you can also use it via npx, i.e, in the terminal!
Nice, bookmarked.
But if you're serious about reducing carbon waste, don't make visitors download 7.5MB to load the webpage.
You are so right.
I am serious about reducing carbon waste. I was amazed at how quickly that CO2e estimate jumps just by savings a few megabytes here and there.
At the very least, it's got me (and hopefully others!) thinking about the issue and I'll be doing everything I can to reduce the load (and the load time!) in the near future!
Thanks for the kick in the butt!
Be careful, you may run into surprises with the XYB stuff in jpegli. Most should support it, since it uses an ICC color profile, but if the decoder does not handle it I'm not entirely sure what happens. I ran into a handful of strange issues a few years ago when testing this out, though it was all relatively old software.
I ran into this exact issue during testing! I noticed that XYB-encoded images sometimes had a green or desaturated cast, especially on color-managed systems like macOS.
The way jpegli handles this is pretty clever and is designed for maximum compatibility. When you enable XYB mode, it doesn't create a non-standard file. Instead, it:
1) Performs its perceptual magic in the XYB color space internally.
2) Encodes the resulting image data into the standard Y'CbCr channels that every JPEG decoder understands.
3) Attaches a custom ICC color profile to the file.
This gives you the best of both worlds.
On modern, color-managed software (like current browsers, macOS Preview, etc.), the decoder reads the ICC profile and uses it to transform the colors back to perfect sRGB. This is how I fixed the green tint I was seeing—by ensuring the correct ICC profile was always embedded.
On old or non-color-managed software, the decoder simply ignores the ICC profile. Since the image data is already in standard Y'CbCr channels, it just displays it as if it were a regular legacy JPEG. The colors might be slightly off, but the image will still render correctly without crashing or showing major artifacts.
It's a great advancement in color spaces and it's the main reason for creating this pet project, really... Jpegli and XYB seem to be relatively unknown, despite gargantuan coding efforts by the Google Zurich team.
I'm just trying to shed some light on it, mostly!
cjpegli --xyb jpegs seem to have colors significantly (not just slightly) off if the color profile does not get loaded, and there are strong green and/or magenta tints. To test this, exiftool -all= file.jpg seems to do the trick. But even with this tint, images seem to make structural sense and can be understood, so compatibility might be fine anyway.
How a user of such a web-site may be sure that it really deosn't upload photos somewhere, even it claims not to do so?
Load the page, disconnect from the internet, convert your image(s).
Or just use ImageMagick like a normal person.
Did you say normal person? It's more like
*or just Google "convert to jpg" like a normal person, scroll through fRee cOnVerTeR results from companies that have nothing to do with image conversion (for SEO of course), click whichever is the least clowny name (with free and fast in the description), upload image, hit the convert button OOPS THAT WAS AN AD, go back, upload again, hit the right convert button, watch image appear in an animation framed by ads, click the download button (the one below the HIGH QUALITY button that's disabled with a lock.png begging for upgrades)
Pick your poison:
A) $magick *.png -quality 85 out.jpg
B) drag, drop, save
I built B for the days I forget A :P
Maybe this is just some niche use-case, but I tested it with a 268x98 png screenshot, and it made the image bigger and worse: https://files.catbox.moe/7so3z6.png
JPEG is for photos.
For a white screen with black text, PNG is also compressed and less lossy.
People should not be using PNG for images. If they are using PNG properly, converting to JPEG is a mistake.
Fair point.
Tiny, high-contrast UI screenshots are a worst-case for JPG—size can grow and edges get mushy.
PNG is the right choice here.
Great work
Have you seen vert.sh? It does on-device conversion of audio, images, and documents and is open source too.
Cheers!
I have never seen that site before, but damn that's an impressive set of decodability there for sure ... whoa!
My brain hurts just thinking about adding that many more different filetypes!
Can it handle batch processing, though?
OnlyJPG is designed to allow the user to drag and drop 100 images onto the screen and (assuming you have auto-download enabled) you should be prompted for a zip file containing your JPGs when you're done.
Can't wait for them to come out with composite file types.
Where I could make one HTTP request and get a json blob and the associated image at the same time.
Then separate them on the client side and add them to the Dom.
You can already do this?
I'm pretty sure you just replied to some AI-powered HN Karma harvesting bot there :/
They're everywhere these days...
Very cool site, I especially loved the write-up, bravo!
Cheers, mondainx!
After toiling away on this by my lonesome for the past few weekends, that means the world to me :)
Seriously, thank you!
Does not appear to be open-source, but was built from at least 90% open-source software.
And if you happen to have wasm or web workers disabled, you get an unhelpful "Processing 0/0 files…" message that just hangs forever.
Fair points.
It's built mostly on OSS and I’ll publish the remaining code with a proper license on GitHub this week.
The "0/0 files" thing ... that's something I hadn't ever even considered!
I'll replace the spinner with a plain message and a non-WASM note.
Thank you for your input!
Awesome. Thank you!
Thank you all so much for the feedback, the bug reports, the critiques, and the encouragement.
This is a personal project, and the engagement here is both exciting and incredibly valuable.
I've already pushed a fix for the WebP/AVIF quality issue based on the excellent report from mdasen—thank you!
I wanted to address a few other major themes you all brought up:
1) "JPG isn't for everything" (especially screenshots/art):
You are 100% right.
The goal of OnlyJPG isn't to argue that JPG is the best format for every use case (it definitely isn't!).
The philosophy is: "When you are in a situation where you need a JPG for compatibility, you should be able to make the best possible one."
I've seen firsthand how often people need to convert screenshots or diagrams to JPG for a CMS, an email client, or a social media site that mangles PNGs. The new default of 4:4:4 subsampling actually helps a lot with text and sharp lines, but I'll be looking into adding a dedicated "Graphics" mode that's even better tuned for this.
2) UX & Copy (especially the transparency section):
The feedback that the copy was confusing is also spot on (thanks catapart!). It's hard to see those things when you're close to the project. My goal was to explain why you might choose JPG over other formats, but it came across as a confusing sales pitch. I'm going to rewrite those sections to be clearer and more direct about what the app actually does (e.g., "How OnlyJPG handles transparency from a PNG").
3) Performance, Trust, and Open Source:
I have issues with the large initial page load, the question of trust, and the fact that the project isn't open-source. You're right on all counts.
Performance: The ~7.5MB load is too high. Optimizing the WASM bundles and vendor libraries is now at the top of my to-do list.
Trust: The best way to verify the "100% in-browser" claim is, as craftkiller noted, to load the page and then disconnect your internet. If it still works, you know nothing is being uploaded.
Open Source: This is the big one. To be transparent, I'm a 40-year-old self-taught developer, and this is one of my first real apps. I was never introduced to GitHub and haven't gotten into the rhythm of open-sourcing my work. But the passionate feedback here has convinced me it's the right next step. I commit to open-sourcing OnlyJPG on GitHub. Your expertise and willingness to help have shown me the immense value of building in public.
This has been an incredible (and slightly overwhelming!) learning experience. Thanks again for taking the time to test, critique, and advise.
ffmpeg runs in the browser now, and it converts images as well as video.
Interesting! Is anybody running with this yet or is it just possible?
I have been running this for at least a year, it works great. I have it resizing video, and save to raw pixel data that I use to extract pixel color info for use in my web application. Anything ffmpeg does (except streaming because it doesn't have network access) can be done in the browser.
https://github.com/ffmpegwasm/ffmpeg.wasm
[dead]