photopea / UPNG.js

Fast and advanced PNG (APNG) decoder and encoder (lossy / lossless)
MIT License
2.1k stars 259 forks source link

UPNG.toRGBA8() output appears to be incorrect for certain PNGs #44

Closed Hopding closed 4 years ago

Hopding commented 4 years ago

Hello @photopea and friends!

I'm looking to use upng-js as the PNG decoding library for pdf-lib. However, I ran into an issue when testing the output of UPNG.toRGBA8() on the basi0g01 image from the interlaced PNG test suite linked to in the README.

When I run basi0g01 through UPNG.toRGBA8() and draw the output to an HTML canvas, I see the following:

Screen Shot 2020-02-17 at 6 50 47 PM

The correct rendering is shown below:

Screen Shot 2020-02-17 at 6 50 39 PM

I've attached the basi0g01 image for your convenience: basi0g01.png

Here's a simple self-contained HTML file you can view in your browser to reproduce the issue:

<html>
  <head>
    <meta charset="utf-8" />
    <script src="https://unpkg.com/pako@1.0.7/dist/pako.min.js"></script>
    <script src="https://unpkg.com/upng-js@2.1.0/UPNG.js"></script>
  </head>

  <body>
    <canvas id="canvas" style="width: 100%; height: 100%;"></iframe>
  </body>

  <script>
    (async () => {
      const upng = UPNG.decode(getImageData());
      const frames = UPNG.toRGBA8(upng);

      const rgbaClamped = new Uint8ClampedArray(frames[0]);
      const imageData = new ImageData(rgbaClamped, upng.width, upng.height);

      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');
      ctx.putImageData(imageData, 0, 0);
    })();

    // Inline data for `basi0g01.png`
    function getImageData() {
      return Uint8Array.from([
        0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
        0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20,
        0x01, 0x00, 0x00, 0x00, 0x01, 0x2c, 0x06, 0x77, 0xcf, 0x00, 0x00, 0x00,
        0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x01, 0x86, 0xa0, 0x31, 0xe8, 0x96,
        0x5f, 0x00, 0x00, 0x00, 0x90, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x2d,
        0x8d, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x45, 0xdf, 0xc6, 0x82, 0xc4, 0x15,
        0x18, 0x7a, 0x00, 0xa4, 0x2e, 0x19, 0x7a, 0xb8, 0x1e, 0x83, 0xb1, 0x27,
        0xe0, 0x0c, 0x56, 0x39, 0x00, 0x13, 0x63, 0xa5, 0x80, 0xd8, 0x58, 0x2c,
        0x65, 0xc9, 0x10, 0x35, 0x7c, 0x4b, 0x78, 0xb0, 0xbf, 0xbf, 0xdf, 0x4f,
        0x70, 0x16, 0x8c, 0x19, 0xe7, 0xac, 0xb9, 0x70, 0xa3, 0xf2, 0xd1, 0xde,
        0xd9, 0x69, 0x5c, 0xe5, 0xbf, 0x59, 0x63, 0xdf, 0xd9, 0x2a, 0xaf, 0x4c,
        0x9f, 0xd9, 0x27, 0xea, 0x44, 0x9e, 0x64, 0x87, 0xdf, 0x5b, 0x9c, 0x36,
        0xe7, 0x99, 0xb9, 0x1b, 0xdf, 0x08, 0x2b, 0x4d, 0x4b, 0xd4, 0x01, 0x4f,
        0xe4, 0x01, 0x4b, 0x01, 0xab, 0x7a, 0x17, 0xae, 0xe6, 0x94, 0xd2, 0x8d,
        0x32, 0x8a, 0x2d, 0x63, 0x83, 0x7a, 0x70, 0x45, 0x1e, 0x16, 0x48, 0x70,
        0x2d, 0x9a, 0x9f, 0xf4, 0xa1, 0x1d, 0x2f, 0x7a, 0x51, 0xaa, 0x21, 0xe5,
        0xa1, 0x8c, 0x7f, 0xfd, 0x00, 0x94, 0xe3, 0x51, 0x1d, 0x66, 0x18, 0x22,
        0xf2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,
        0x82
      ]);
    }
  </script>
</html>

Note that I obtained the inline image by running the following NodeJS script:

const img = fs.readFileSync('basi0g01.png')
console.log(
  Array
    .from(x)
    .toString(16)
    .split(/(\d+,\d+,\d+,\d+,\d+,\d+,\d+,\d+,\d+,\d+,\d+,\d+,)/g)
    .map(line => line.split(',').filter(Boolean).map(n => Number(n).toString(16))
    .map(n => `0x${n.padStart(2, '0')}`).join(', '))
    .filter(Boolean)
    .join(',\n        '),
)

Lest you suspect that the issue is simply due to a bug in my script code, I should mention that I first encountered the issue when reading the image data directly from the PNG file. I just thought it would be helpful to create a self-contained HTML file to make reproducing the issue as easy as possible.

Finally, I found that by exporting the PNG image in Mac's Preview app (essentially rewriting it as a different PNG type) I was able to successfully decode and draw it on the canvas. Here's the rewritten image I created: basi0g01_rewritten.png. And here's an alternative version of the getImageData function you can swap out in the example I shared to use the rewritten version:

// Inline data for `basi0g01_rewritten.png`
function getImageData() {
  return Uint8Array.from([
    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
    0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20,
    0x08, 0x00, 0x00, 0x00, 0x01, 0x21, 0x16, 0x15, 0xbe, 0x00, 0x00, 0x00,
    0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x01, 0x86, 0xa0, 0x31, 0xe8, 0x96,
    0x5f, 0x00, 0x00, 0x00, 0xd0, 0x69, 0x43, 0x43, 0x50, 0x49, 0x43, 0x43,
    0x20, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x00, 0x00, 0x18, 0x95,
    0x63, 0x60, 0x60, 0xac, 0x48, 0x2c, 0x28, 0xc8, 0x61, 0x12, 0x60, 0x60,
    0xc8, 0xcd, 0x2b, 0x29, 0x72, 0x0f, 0x72, 0x8c, 0x8c, 0x88, 0x8c, 0x52,
    0x60, 0xbf, 0xca, 0xc0, 0xce, 0xc0, 0xc8, 0x00, 0x06, 0x89, 0xc9, 0xc5,
    0x05, 0x8e, 0x01, 0x01, 0x3e, 0x0c, 0x38, 0xc1, 0xb7, 0x6b, 0x10, 0xb5,
    0x97, 0x75, 0x41, 0x66, 0xe1, 0x56, 0x87, 0x15, 0xb0, 0xa4, 0xa4, 0x16,
    0x27, 0x03, 0xe9, 0x2d, 0x40, 0x5c, 0x9a, 0x5c, 0x50, 0x54, 0xc2, 0xc0,
    0xc0, 0xa8, 0x03, 0x64, 0xab, 0x97, 0x97, 0x14, 0x80, 0xd8, 0x21, 0x40,
    0xb6, 0x48, 0x76, 0x48, 0x90, 0x33, 0x90, 0x9d, 0x01, 0x64, 0xf3, 0x41,
    0xd5, 0x83, 0x80, 0xb4, 0x73, 0x62, 0x4e, 0x66, 0x52, 0x51, 0x62, 0x49,
    0x6a, 0x8a, 0x82, 0x7b, 0x51, 0x62, 0xa5, 0x82, 0x73, 0x7e, 0x4e, 0x7e,
    0x51, 0x71, 0x41, 0x62, 0x72, 0x2a, 0x89, 0xae, 0x20, 0x02, 0x94, 0xa4,
    0x56, 0x94, 0x80, 0x68, 0xe7, 0xfc, 0x82, 0xca, 0xa2, 0xcc, 0xf4, 0x8c,
    0x12, 0x05, 0x47, 0xa0, 0x6f, 0x53, 0x81, 0x76, 0xe6, 0x16, 0x94, 0x96,
    0xa4, 0x16, 0xe9, 0x28, 0x78, 0xe6, 0x25, 0xeb, 0x31, 0x30, 0x80, 0xc2,
    0x0f, 0xa2, 0xe3, 0x73, 0x20, 0x38, 0x5c, 0x18, 0xc5, 0xce, 0x24, 0x97,
    0x16, 0x95, 0x41, 0x8d, 0x61, 0x04, 0x09, 0x01, 0x00, 0x10, 0xc0, 0x34,
    0x91, 0x8a, 0x29, 0xae, 0x33, 0x00, 0x00, 0x00, 0x44, 0x65, 0x58, 0x49,
    0x66, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x01,
    0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x87,
    0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x26, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x02, 0xa0, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00,
    0x01, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00,
    0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x66, 0xf6, 0xf7,
    0x5d, 0x00, 0x00, 0x01, 0x59, 0x69, 0x54, 0x58, 0x74, 0x58, 0x4d, 0x4c,
    0x3a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x78,
    0x6d, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x78, 0x3a, 0x78, 0x6d,
    0x70, 0x6d, 0x65, 0x74, 0x61, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a,
    0x78, 0x3d, 0x22, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x3a, 0x6e, 0x73, 0x3a,
    0x6d, 0x65, 0x74, 0x61, 0x2f, 0x22, 0x20, 0x78, 0x3a, 0x78, 0x6d, 0x70,
    0x74, 0x6b, 0x3d, 0x22, 0x58, 0x4d, 0x50, 0x20, 0x43, 0x6f, 0x72, 0x65,
    0x20, 0x35, 0x2e, 0x34, 0x2e, 0x30, 0x22, 0x3e, 0x0a, 0x20, 0x20, 0x20,
    0x3c, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, 0x46, 0x20, 0x78, 0x6d, 0x6c,
    0x6e, 0x73, 0x3a, 0x72, 0x64, 0x66, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70,
    0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72,
    0x67, 0x2f, 0x31, 0x39, 0x39, 0x39, 0x2f, 0x30, 0x32, 0x2f, 0x32, 0x32,
    0x2d, 0x72, 0x64, 0x66, 0x2d, 0x73, 0x79, 0x6e, 0x74, 0x61, 0x78, 0x2d,
    0x6e, 0x73, 0x23, 0x22, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
    0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70,
    0x74, 0x69, 0x6f, 0x6e, 0x20, 0x72, 0x64, 0x66, 0x3a, 0x61, 0x62, 0x6f,
    0x75, 0x74, 0x3d, 0x22, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
    0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a,
    0x74, 0x69, 0x66, 0x66, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f,
    0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f,
    0x6d, 0x2f, 0x74, 0x69, 0x66, 0x66, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x22,
    0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c,
    0x74, 0x69, 0x66, 0x66, 0x3a, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61,
    0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x31, 0x3c, 0x2f, 0x74, 0x69, 0x66, 0x66,
    0x3a, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e,
    0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x72, 0x64,
    0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f,
    0x6e, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a,
    0x52, 0x44, 0x46, 0x3e, 0x0a, 0x3c, 0x2f, 0x78, 0x3a, 0x78, 0x6d, 0x70,
    0x6d, 0x65, 0x74, 0x61, 0x3e, 0x0a, 0x4c, 0xc2, 0x27, 0x59, 0x00, 0x00,
    0x01, 0x01, 0x49, 0x44, 0x41, 0x54, 0x38, 0x11, 0x95, 0x93, 0xdb, 0x12,
    0xc2, 0x20, 0x0c, 0x44, 0xc1, 0xf1, 0xff, 0x7f, 0x19, 0xf7, 0x92, 0x0d,
    0x50, 0xc7, 0x07, 0xe9, 0x58, 0x48, 0x72, 0x48, 0x36, 0x50, 0xe7, 0x1a,
    0x18, 0x0b, 0xcf, 0x58, 0x5c, 0xf2, 0x37, 0xe5, 0xd3, 0xba, 0x7c, 0x8c,
    0xf2, 0x81, 0xe9, 0xe0, 0x1c, 0xe3, 0x45, 0x52, 0x56, 0xe1, 0xbd, 0x4f,
    0x21, 0x44, 0xc7, 0x24, 0xc3, 0x79, 0xe7, 0xd2, 0x06, 0xd8, 0xde, 0x4e,
    0x3f, 0x18, 0xda, 0x18, 0x5f, 0x79, 0xec, 0x56, 0x1a, 0x22, 0x85, 0xcd,
    0x39, 0xde, 0x80, 0xcb, 0xb2, 0x37, 0x92, 0x1b, 0x02, 0xa1, 0x01, 0x8a,
    0x69, 0xb5, 0x45, 0xb3, 0xdd, 0x78, 0x93, 0x48, 0x0e, 0x3b, 0x53, 0x3f,
    0xc8, 0x4e, 0x6a, 0x4f, 0xda, 0xe9, 0x78, 0xb4, 0x47, 0x98, 0xbb, 0x4a,
    0x58, 0xd9, 0x75, 0x02, 0x3b, 0x8e, 0x92, 0x38, 0x42, 0x10, 0x16, 0x45,
    0x14, 0x84, 0x8e, 0xbf, 0xd5, 0x74, 0xd9, 0xa8, 0xab, 0x53, 0xdc, 0x6a,
    0xab, 0xb9, 0xca, 0x83, 0xc9, 0x84, 0x79, 0xdd, 0x89, 0x88, 0xda, 0xaf,
    0x82, 0x24, 0xca, 0xf6, 0xad, 0x81, 0x88, 0x8d, 0xfa, 0x5c, 0x3e, 0xdb,
    0xa7, 0xb8, 0x6b, 0xb4, 0xb0, 0xcb, 0xdb, 0x46, 0xce, 0xb8, 0x1d, 0xf7,
    0x82, 0x25, 0x4a, 0x96, 0x2b, 0xcd, 0xab, 0xa2, 0xc5, 0x54, 0x67, 0xd4,
    0x76, 0xcb, 0x89, 0xd6, 0x06, 0xbe, 0x93, 0xdb, 0x63, 0x91, 0xc1, 0x2b,
    0x49, 0x4c, 0x22, 0xd5, 0x05, 0x0b, 0xa4, 0xc8, 0x19, 0x6e, 0x60, 0x17,
    0xb8, 0xc3, 0xd5, 0xc5, 0xef, 0xf0, 0x03, 0xc8, 0xee, 0x53, 0xca, 0xd1,
    0x45, 0xc2, 0x92, 0x86, 0xd7, 0xd2, 0x87, 0xdd, 0xc0, 0x19, 0xae, 0x2b,
    0x92, 0xab, 0x80, 0x3b, 0xec, 0x2b, 0x3d, 0x32, 0x3c, 0xc3, 0x67, 0x06,
    0xff, 0xdf, 0x77, 0x1b, 0xff, 0xaf, 0x3e, 0xe2, 0x3a, 0x3f, 0x78, 0x53,
    0xff, 0x69, 0x96, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
    0x42, 0x60, 0x82
  ]);
}

Any idea what's going on here? Am I doing something incorrectly? Or is this a bug in the UPNG.toRGBA8() method?

photopea commented 4 years ago

Hi, I just tried to open your PNG image in Photopea and it seems to work well. Photopea uses UPNG.toRGBA8(). Try to use UPNG.js library from this repository, as I dont know who is the author and maintainer of your "npm" version.

Hopding commented 4 years ago

@photopea The NPM version I am using is advertised in the README file, so I assumed it was an officially supported distribution. The NPM package also lists users photopea and scimonster as contributors (the same contributors as this repo lists). However, I just noticed that the last NPM release was 2 years ago, whereas the last commit to this repo was just 3-4 months ago.

I tried copying the latest version of the UPNG.js file in this repo directly into a script tag and it works as expected: upng.html.txt (note the .txt extension is just to get around GitHub's file extension whitelist).

Screen Shot 2020-02-18 at 8 34 10 AM

Would you please confirm whether or not the upng-js NPM package is officially supported or not?

I should also mention that unpkg is just a CDN for NPM packages. All NPM packages are available on it without any additional configuration by the author. But regardless, the problem is not an unpkg issue. I ran into it when just installing the package via npm.

photopea commented 4 years ago

@Scimonster Could you please update the NPM version of UPNG.js?

I am not a user of NPM (but I made an account there through a browser some time ago). I publish my work only here on this GitHub account, and other users put it on NPM.

If nobody updates it on NPM, I will remove the NPM-related instructions from README.

Hopding commented 4 years ago

That makes sense. Thanks for the clarification!

I forked this repo (https://github.com/Hopding/upng) and published my own version to NPM: @pdf-lib/upng. I'll be using my fork as a dependency in pdf-lib.

@photopea Thanks for the work you've done on this project! I really appreciate that you've open sourced this code. It's the best PNG decoding library I've been able to find for JavaScript. All the other libraries I've come across either don't support certain types of PNGs or rely on NodeJS APIs and are a hassle to use in the browser.

friksa commented 4 years ago

I only found this lib because of npm. Maybe @Hopding can share the steps to publish it to NPM? Otherwise, maybe you can give me rights to the project and I can figure it out and publish it to NPM. Thanks for a great project!

photopea commented 4 years ago

@Hopding thanks! The most complex part of UPNG.js is the lossy PNG encoder, which can minify PNG files, similar to tinypng.com https://blog.photopea.com/png-minifier-inside-photopea.html

Hopding commented 4 years ago

@friksa You can take a look at my fork to see how I went about publishing the code to NPM. Here's a diff of the changes I made: https://github.com/photopea/UPNG.js/compare/master...Hopding:master. Let me know if you have any questions.

And, of course, feel free to just use the @pdf-lib/upng module I published, if you like.