New ".Dpi" property for image resources

I’d like to have a way to get the DPI value(s) for a (local) image resource.

In my render-image.html render hook I’ve something like this:

{{- with .Page.Resources.GetMatch .Destination -}}
  <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" />
{{- end -}}

And I’d like to have a .Dpi (or: .DpiX and .DpiY) property here.

(Before creating a feature proposal on GitHub, I thought I’d ask here first.)

My use case:

I often use screenshots in my Hugo pages:

![My Screenshot](my-screenshot.png)

When I record a, say 100x100 pixels, screenshot on a high DPI/4K display, the screenshot tool properly records the DPI values as 192/192 (which is double of the standard 96 DPI on a 1K display).

The downside is that browsers now display the image at 200x200 pixels (because that’s the “native” pixel size of the image) - but I’d rather display it at 100x100 pixels.

If I could get the DPI values in Hugo, I could automatically calculate a scaling factor (by dividing the DPI size by 96).

Here’s an example of such a screenshot:

view-raw-logs

This might be solvable with existing features.

Option 1:

I would expect to have DPI information in the EXIF tags of the original file. Hugo strips some (or all), when processing images, so you might want to find out, which EXIF tag has the value you are seeking and then configure them to be included instead of deleted and then work with the .Exif method of the resource you loaded. That would be doable with a general render hook that checks if the image is “double sized” or not and put out the markup accordingly.

Option 2:

If your image is x by y pixels, that is your double dpi image. Create an image that is resized to 50% of that and then use a srcset to accomplish this with HTML. You could create a “markdown render hook for images” that takes care of the whole calculation side of it if ALL markdown images are double sized or with a shortcode if that happens only every now and then.

I use GNOME Screenshot, and I can’t find this information anywhere (tried ImageMagick’s
identify and exiftool). I’m not sure how it would determine this value because a screen capture can span monitors of varying resolutions.

If your app writes some EXIF data, make sure you write JPEGs instead of PNGs. Hugo currently reads EXIF data from JPEG and TIFF. And I think that’s the enhancement here… read EXIF from PNG (similar to https://github.com/gohugoio/hugo/issues/10855).

I use GNOME Screenshot, and I can’t find this information anywhere (tried ImageMagick’s
identify and exiftool). I’m not sure how it would determine this value because a screen capture can span monitors of varying resolutions.

The DPI values are stored (somewhere) in the JPEG header. Same goes for PNG. It seems this information could also be stored in EXIF but this isn’t a must (also I’m not sure if EXIF exists for PNG).

My screenshot tool (Greenshot) uses the regular headers to store DPI information. I’m on Windows so I can view/edit them with XnView or just view them with Paint (via File → Image Properties):

I’ve tested both JPEG and PNG and in all cases both XnView and Paint displayed the correct values. (I would attach the images as zip here but I’m not allowed to.)

So I’m really looking for the DPI values (and not for EXIF - which apparently didn’t contain the information, at least not in my cases).

I’ve created a test page (with the various formats) to show what I mean:

At the moment, the DPI detection works off the file name (i.e. file names that end with @2x are considered 192 DPI).

For now, let’s limit the discussion to PNG, in that this is the most common screen capture output format.

Miscellaneous notes from the PNG spec

PNGs may or may not contain a pHYs chunk:

The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. It contains:
   Pixels per unit, X axis: 4 bytes (unsigned integer)
   Pixels per unit, Y axis: 4 bytes (unsigned integer)
   Unit specifier:          1 byte
The following values are legal for the unit specifier:
   0: unit is unknown
   1: unit is the meter

The pHYs chunk is an ancillary chunk:

All ancillary chunks are optional, in the sense that encoders need not write them and decoders can ignore them. However, encoders are encouraged to write the standard ancillary chunks when the information is available, and decoders are encouraged to interpret these chunks when appropriate and feasible.

Comments

Pixels per unit can be different along each axis. If they are the same, then converting that to something like 120dpi makes sense, except for the rounding errors (you need to decide what you want to round to). So we would need to report both values, probably raw, and let users do the math. If the unit specifier is unknown, I guess we’d return nil for both values.

Go’s PNG decoder ignores the ancillary chunks as permitted by the spec, so we would have to either (a) add another dependency (a package that decodes the pHYs chunk if present) or (b) roll our own.

The .PixelsPerMeterX and .PixelsPerMeterY methods could return nil (the pHYs chunk is optional), so you’d have to code defensively to use them, making the API very different than accessing width or height (that’s not a good thing).

I come back to EXIF data as suggested by @davidsneighbour. PNG supports EXIF data, but Hugo cannot currently read it. I think a new EXIF decoder that supports PNG and WebP is far more likely to happen.

I come back to EXIF data as suggested by @davidsneighbour. PNG supports EXIF data, but Hugo cannot currently read it. I think a new EXIF decoder that supports PNG and WebP is far more likely to happen.

The question is whether (screenshot) tools record the DPI values in EXIF.

I tried Hugo’s .Exif function with a JPEG screenshot but could not see the DPI values in the EXIF data (I think I’ve tried {{ .Exif | jsonify }}). But maybe I’m using .Exif wrong.

The .PixelsPerMeterX and .PixelsPerMeterY methods could return nil (the pHYs chunk is optional), so you’d have to code defensively to use them, making the API very different than accessing width or height (that’s not a good thing).

You also need to code defensively when using .Width and .Height because they’re only available for raster images but not .svg images.

Go’s PNG decoder ignores the ancillary chunks as permitted by the spec, so we would have to either (a) add another dependency (a package that decodes the pHYs chunk if present) or (b) roll our own.

I absolutely sympathize. If the implementation gets too complicated, then I totally understand if you don’t “want” to do it. :slight_smile:

True, related to https://github.com/gohugoio/hugo/issues/11779. But now you need to be defensive for every raster image format too… that’s not good. Site and theme authors are (to some extent) already used to coding defensively for EXIF extraction, so that’s another reason to see if that approach is feasible.

There are a few issues here:

  1. Does the (your) screen cap app write EXIF data for the target image format?
  2. Does the (your) screen cap app write EXIF data correctly?
  3. Is the EXIF data consistent between formats?
  4. Can Hugo currently read this data?

With respect to #4, Hugo can read these three EXIF fields from a JPEG if you allow access in site config:

[imaging]
[imaging.exif]
includeFields = '.*'  # or be more selective

See https://gohugo.io/content-management/image-processing/#exif-data

That’s exactly what I’ve tried. I got some EXIF data but none that looked remotely like a DPI value.

I’m guessing, it’s not.

I tested GIMP screen caps as well. Export to PNG generates the expected EXIF tags, but their export to JPEG does not (it stuffs the data in the JFIF APP0 marker segment). Given prior experience with GIMP, I don’t find this inconsistency to be surprising.

Feel free to upload an original PNG capture (don’t copy/paste); I’d like to take a look at it.

You can find them here: https://manski.net/articles/hugo/image-dpi-test

The original screenshot is the PNG, 192 DPI. All other versions were converted with XnView (however, there is a checkbox in this tool that says it’ll update the DPI values in EXIF).

OK thanks. I only care about the PNG for the moment.

So if you don’t do that you lose the info I guess. Everything’s optional everywhere… great.

With view-raw-logs@1x.png I can see the optional pHYs chunk (3779 pixels per meter in both x and y = 95.9866 dpi), but your app doesn’t write to EXIF.

There are a couple of related Go issues:

If would be handy if image.Config included the resolution values, but I doubt that will ever happen.


bep wrote an experimental (incomplete) package to read EXIF, XMP, and IPTC:
https://github.com/bep/imagemeta

The alternatives (including what we’re using today) do not have the desired feature set, and are (for the most part), abandoned.

If this package gets legs, perhaps we could access format specific metadata. For example:

.Exif.ImageDescription   # applicable to all formats
.XMP.Title               # applicable to all formats

.PNG.PixelsPerMeterX     # applicable to PNG
.PNG.PixelsPerMeterY     # applicable to PNG

.JPEG.DensityX           # applicable to JPEG
.JPEG.DensityY           # applicable to JPEG
.JPEG.DensityUnits       # applicable to JPEG

Or something…

@jmooring Thanks for the detailed analysis :slight_smile:

Unfortunately, this all doesn’t look very promising. So, I would abandon the topic (for now) and stick with my @2x trick. :slight_smile:

1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.