3

I'm building an application using Electron primarily for Mac OS X. The user can drop an image onto the page, the page creates an <img> with the source path of the dropped image, and the user sees the image.

Problem

If the user takes a screenshot on a retina display and then drops the image onto the page, the image displays at double the size. I need to somehow know to display this image at half its natural dimensions.

Possible Solution

I believe I should be able to reliably tell if the image is retina if I check its DPI. In the Preview app on the Mac I can see that the image is 144 DPI. Essentially, if the DPI is 144 or greater, then it's retina, right?

Is there some way to read this data, given the image, using either Electron's Native Image or NodeJS?

Note: Mac OS X takes screenshots as PNGs, so there is no exif data.

Edit and Update: I believe I can tell what the image's DPI is from looking at the HEX information. I.e., fs.readFileSync('file.type').toString('hex'), then for PNGs look for 70 48 59 73, as mentioned here, or for JPGs look for FFD8FFE000104A464946000101 as mentioned here. The problem I'm having now is when trying to work with Electron's NativeImage when an image is pasted from the clipboard.

If I paste a PNG from the clipboard, and do nativeImage.toPng().toString('hex'), the following is output:

89504e470d0a1a0a0000000d49484452000001fc000001680806000000b2a54946000005c249444154789cedd5410dc03010c0b0ae448f3f8a0dc554a9b111e4976766de05005c6d9f0e0000fe67f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f8001060f80010f0014a0204ec5f3e63f20000000049454e44ae426082

Where in this string would I find the DPI information?

Edit and Update 2: I'm wondering if this could be a bug within Electron. When I copy an image that is 144 PPI from Preview and paste it into my app, then copy it out of my app and paste it into a new Preview window, it changes to 72 PPI. Is it possible that Electron is stripped out this information?

Preview DPI

Chunks

Requested by @robertklep, here are screenshots of the chunks (sorry, I override the Copy in my app so I can't actually copy text right now).

Initial Image Initial

After copied/pasted After copy

Community
  • 1
  • 1
atdrago
  • 295
  • 4
  • 16

2 Answers2

3

The output you're showing looks to be the raw PNG data (the PNG signature is 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A).

You can parse the different chunks in the data and look for the pHYs chunk, which is (I think) the chunk you need to extract resolution information from for Mac OS X-generated PNG's.

A simple parser which processes all PNG chunks:

var image = nativeImage.toPng(); // or the result of fs.readFile*()

function* parseChunks(data) {
  var offset = 8; // skip PNG header

  while (offset < data.length) {
    var dataLength  = data.readUInt32BE(offset);
    var chunkLength = dataLength + 12;
    var typeStart   = offset + 4;
    var dataStart   = offset + 8;
    var dataEnd     = offset + 8 + dataLength;
    var crcEnd      = dataEnd + 4;

    yield {
      type : data.toString('ascii', typeStart, dataStart),
      data : data.slice(dataStart, dataEnd),
      crc  : data.slice(dataEnd, crcEnd),
    };

    offset = crcEnd;
  }
}

for (let chunk of parseChunks(image)) {
  // Extract pixel information
  if (chunk.type === 'pHYs') {
    var ppuX = chunk.data.readUInt32BE(0);
    var ppuY = chunk.data.readUInt32BE(4);
    var unit = chunk.data.readUInt8(8); // should always be `1`
    console.log('PPI', Math.round(ppuX * 0.0254));
  }
}

This outputs PPI 144 on my Mac, as expected.

robertklep
  • 198,204
  • 35
  • 394
  • 381
  • Thank you fro your answer, but see my updated question. Is there a way to get this when reading an image from the clipboard? It looks like copied images don't get this data. – atdrago Nov 25 '15 at 18:37
  • Since this is the closest it seems I'll get, and since it does answer the original question, I will give you the correct answer and award you the bounty if there are no better answers before the bounty runs out. – atdrago Nov 26 '15 at 01:27
  • @atdrago can you post a list of the chunks in the pasted data? You can reuse the code I posted and just log each `chunk.type`. Or, if possible, can you write a pasted image to file and post that somewhere so I can take a look? – robertklep Nov 26 '15 at 07:21
  • 1
    @atdrago I believe that the Mac OS X clipboard uses the TIFF format for storing image data, which means that the PNG gets converted to TIFF and once pasted to Electron gets converted back to PNG again. During any of those conversions, the metadata (like the `pHYs` chunk) probably gets lost and you end up with an image that appears double in size. I don't think there's a way around that :-( – robertklep Nov 26 '15 at 09:02
  • Sadly, I think you're right. It doesn't seem to have anything to do with Electron. You can easily reproduce it in Preview by taking a screenshot, opening in Preview, Command + C, Command + N. The new image will have lost its DPI data. – atdrago Nov 26 '15 at 09:06
  • Do you know if it's possible to retrieve DPI/PPI from the pasted TIFF? – atdrago Nov 26 '15 at 09:16
  • @atdrago sorry, no. I'm not even sure if you can access the pasteboard directly from Electron? – robertklep Nov 26 '15 at 09:22
  • @atdrago as far as I can see, that's still a pretty high-level API: `readImage()` will return a `NativeImage`, which only supports PNG or JPEG, so the conversion from "clipboard format" to PNG/JPEG is handled automatically; it seems you can't access the raw data. – robertklep Nov 26 '15 at 20:58
0

I found that you can access raw PNG clipboard data for screenshot via clipboard.readBuffer("public.png") now. See https://ocadaruma.hatenablog.com/entry/2023/01/29/091354 for the details how it works.

Haruk Okada
  • 83
  • 2
  • 6