[{"data":1,"prerenderedAt":41},["ShallowReactive",2],{"$f63hmd6DCvoqN9JnY4YOoLRUVbIjFj_XafzxbSOWUHeI":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"tools":13,"draft":18,"html":19,"toc":20,"minutes":40},"converting-png-jpg-webp","PNG, JPG and WebP Conversions, and the Browser Quirks Behind Them","Why transparency turns white in a JPG, why JPG to PNG makes files bigger, and why Safari quietly hands back a PNG when you ask it for a WebP.","2026-09-29",[9,10,11,12],"image formats","webp","canvas","safari",[14,15,16,17],"png-to-jpg","jpg-to-png","webp-to-jpg","convert-to-webp",false,"\u003Cp>You convert a logo from PNG to JPG for a form, and its see-through background comes back as a solid box. Or you convert your site’s images to WebP on an iPhone, and the “WebP” files turn out to be PNGs with the wrong name. ToolOnTap has four format converters, and all of them run on the same few lines of canvas code. Most of what’s interesting about them is how browsers behave at the edges, so that’s what this article is about.\u003C\u002Fp>\n\u003Ch2 id=\"one-pipeline-four-converters\">One pipeline, four converters\u003C\u002Fh2>\n\u003Cp>\u003Ca href=\"\u002Fpng-to-jpg\">PNG to JPG\u003C\u002Fa>, \u003Ca href=\"\u002Fjpg-to-png\">JPG to PNG\u003C\u002Fa>, \u003Ca href=\"\u002Fwebp-to-jpg\">WebP to JPG\u003C\u002Fa> and \u003Ca href=\"\u002Fconvert-to-webp\">Convert to WebP\u003C\u002Fa> are the same tool with a different output format. Each image goes through three steps in the browser:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Decode\u003C\u002Fstrong> the file into pixels with \u003Ccode>createImageBitmap\u003C\u002Fcode>, which every current browser can do for JPG, PNG, WebP and GIF.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Draw\u003C\u002Fstrong> the pixels onto a canvas the size of the image.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Encode\u003C\u002Fstrong> the canvas in the new format with \u003Ccode>canvas.toBlob(callback, type, quality)\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>The browser’s own encoders do the real work, so no image library is downloaded for these pages. What I choose is the format and the quality:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>Output\u003C\u002Fth>\n\u003Cth>Quality\u003C\u002Fth>\n\u003Cth>Kind of compression\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>JPG\u003C\u002Ftd>\n\u003Ctd>0.92\u003C\u002Ftd>\n\u003Ctd>Lossy\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>WebP\u003C\u002Ftd>\n\u003Ctd>0.8\u003C\u002Ftd>\n\u003Ctd>Lossy (the quality the tool passes)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>PNG\u003C\u002Ftd>\n\u003Ctd>not used\u003C\u002Ftd>\n\u003Ctd>Lossless\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>0.92 is the default in my \u003Ccode>canvasToBlob\u003C\u002Fcode> helper. For WebP the converter passes 0.8, which is a common setting for images on the web. PNG ignores the quality argument, because there’s nothing to trade: every pixel is stored exactly.\u003C\u002Fp>\n\u003Ch2 id=\"where-the-transparency-goes\">Where the transparency goes\u003C\u002Fh2>\n\u003Cp>PNG, WebP and GIF can store transparency. JPG can’t: every pixel in a JPG has a color. So when a transparent image becomes a JPG, something has to fill the see-through areas.\u003C\u002Fp>\n\u003Cp>If you leave it to the browser, the HTML standard tells it to put the image on a solid \u003Cstrong>black\u003C\u002Fstrong> background before encoding a format without transparency. A logo with black text on a transparent background then comes out as black on black. So before drawing, the tool paints the canvas white whenever the output is JPG:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#57606a;--shiki-dark:#8b949e\">\u002F\u002F Paint white first when the format can't hold transparency\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">export\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> function\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> fillForFormat\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">ctx\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> CanvasRenderingContext2D\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">type\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> ImageFormat\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">width\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">height\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  if\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (type \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">!==\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'image\u002Fjpeg'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  ctx.fillStyle \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> '#fff'\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  ctx.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">fillRect\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, width, height)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>White is a guess, but a safe one. Most logos are made for white pages and white documents, and a white box looks like a margin while a black one looks like a mistake.\u003C\u002Fp>\n\u003Cp>Going the other way, transparency stays. PNG to WebP keeps transparent backgrounds transparent, because WebP supports them. JPG to PNG keeps the background the photo already had. The PNG can hold transparency, but there’s none in a JPG to carry across, and working out which pixels are “background” is a different job altogether.\u003C\u002Fp>\n\u003Ch2 id=\"lossless-doesn-t-mean-better\">Lossless doesn’t mean better\u003C\u002Fh2>\n\u003Cp>This is the part that confuses people most. PNG is lossless, so converting a JPG to PNG sounds like an upgrade. It isn’t one. The PNG stores the JPG’s pixels exactly, including every blur and block the JPG compression already made. Nothing comes back. What you get is a much bigger file, often several times the size of the JPG, because lossless compression is poor at photos.\u003C\u002Fp>\n\u003Cp>There are good reasons to do it anyway. Some sites and icon tools only accept PNG. And if you’ll edit the image many times, saving each version as PNG stops new JPG damage from building up with every save.\u003C\u002Fp>\n\u003Cp>The opposite conversion has its own surprise. PNG to JPG makes photos much smaller, but it can make \u003Cstrong>graphics\u003C\u002Fstrong> bigger. A screenshot or a flat-color chart compresses very well as PNG, while JPG adds soft halos around sharp edges and spends bytes on them. The tool shows the size change next to each file, so if a PNG grew as a JPG, keep the PNG.\u003C\u002Fp>\n\u003Cp>WebP to JPG often grows too, for a simpler reason. WebP compresses more efficiently than JPG, so the same picture needs more bytes in the older format.\u003C\u002Fp>\n\u003Ch2 id=\"safari-can-show-webp-but-can-t-make-it\">Safari can show WebP but can’t make it\u003C\u002Fh2>\n\u003Cp>Here’s the quirk that shaped Convert to WebP. When you ask a canvas for a format the browser can’t encode, the standard says it should hand back a PNG instead. There’s no error and no warning. The callback receives a perfectly good PNG blob.\u003C\u002Fp>\n\u003Cp>Safari displays WebP images, but its canvas can’t encode them. On an iPhone or iPad, Chrome, Firefox and Edge are built on Safari’s engine too, so they behave the same way. Without a check, the tool would have saved PNG files named \u003Ccode>.webp\u003C\u002Fcode>. They’d often be bigger than the originals, and the website owner would never know.\u003C\u002Fp>\n\u003Cp>So the converter checks what it got back:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">async\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> function\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> convert\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">file\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> Blob\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">type\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> ImageFormat\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'image\u002Fjpeg'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> Promise\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">ProcessResult\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> result\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> await\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> renderImage\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(file, type, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">s\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =>\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> s, type \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">===\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'image\u002Fwebp'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> ?\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 0.8\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> :\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> undefined\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  if\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (result.blob.type \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">!==\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> type) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">throw\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> UnsupportedOutputError\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(type)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  return\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { blob: result.blob, note: result.scaled \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">?\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> scaledNote\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(result) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> undefined\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The blob’s \u003Ccode>type\u003C\u002Fcode> says which encoder actually ran. If it doesn’t match, nothing is saved. The file is shown with a message saying that this browser can’t create WebP images, and that Chrome, Edge or Firefox on a computer or an Android phone can.\u003C\u002Fp>\n\u003Cp>The check looks at what the browser actually produced, so it needs no list of browser names. If Safari ever gains a WebP encoder, the tool will start working there without a change.\u003C\u002Fp>\n\u003Ch2 id=\"smaller-details\">Smaller details\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Animated GIF and WebP.\u003C\u002Fstrong> Only the first frame is converted. JPG can’t animate, and the canvas only ever holds one frame.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Very large images.\u003C\u002Fstrong> Results are capped at 16,777,216 pixels (4096 × 4096), the canvas size iOS Safari can handle, so a bigger image is scaled down and the file gets a note saying so.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Quality loss happens once.\u003C\u002Fstrong> Each conversion decodes and encodes one time. Converting JPG to WebP and back to JPG costs quality twice, so keep your originals.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"try-it\">Try it\u003C\u002Fh2>\n\u003Cp>Pick the converter for the job: \u003Ca href=\"\u002Fpng-to-jpg\">PNG to JPG\u003C\u002Fa> for smaller photos, \u003Ca href=\"\u002Fjpg-to-png\">JPG to PNG\u003C\u002Fa> for forms and editors that want PNG, \u003Ca href=\"\u002Fwebp-to-jpg\">WebP to JPG\u003C\u002Fa> for images saved from websites, and \u003Ca href=\"\u002Fconvert-to-webp\">Convert to WebP\u003C\u002Fa> for your own site’s images. They all convert on your device, one file or a whole batch with a ZIP download. More tools are on the \u003Ca href=\"\u002Fimage-tools\">image tools\u003C\u002Fa> page.\u003C\u002Fp>\n",[21,25,28,31,34,37],{"id":22,"text":23,"level":24},"one-pipeline-four-converters","One pipeline, four converters",2,{"id":26,"text":27,"level":24},"where-the-transparency-goes","Where the transparency goes",{"id":29,"text":30,"level":24},"lossless-doesn-t-mean-better","Lossless doesn’t mean better",{"id":32,"text":33,"level":24},"safari-can-show-webp-but-can-t-make-it","Safari can show WebP but can’t make it",{"id":35,"text":36,"level":24},"smaller-details","Smaller details",{"id":38,"text":39,"level":24},"try-it","Try it",4,1792325805293]