Add resolution support for image formats (JPEG, TIFF, PNG, BMP) along with unit parsing#165
Add resolution support for image formats (JPEG, TIFF, PNG, BMP) along with unit parsing#165cmrd-senya wants to merge 5 commits intosdsykes:masterfrom
Conversation
… with unit parsing
| # FastImage.resolution("test/fixtures/test.png") | ||
| # => nil | ||
| # | ||
| def self.resolution(uri, options={}) |
There was a problem hiding this comment.
This returns both resolution and units. Instance methods return just one or another. It's inconsistent between each other but with class method allows to parse just once. And for instance methods it is parsed just once anyway
There was a problem hiding this comment.
Pull request overview
This PR adds resolution (pixel density) extraction to FastImage for common formats, extending the existing minimal-read parsing approach to support reading format-specific metadata for JPEG, PNG, BMP, and TIFF.
Changes:
- Adds
FastImage.resolution(uri)plus instance methods#resolutionand#resolution_units. - Implements per-format resolution parsing for JPEG (JFIF), PNG (pHYs), BMP (DIB header PelsPerMeter), and TIFF (EXIF rational tags), including EXIF parser enhancements.
- Extends fixture expectations and adds a sweep test validating resolution across the fixture set.
Reviewed changes
Copilot reviewed 9 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/fastimage/fastimage.rb | Adds class/instance resolution APIs and wires resolution parsing into the fetch/parse pipeline. |
| lib/fastimage/fastimage_parsing/image_base.rb | Adds a default resolution hook for parsers. |
| lib/fastimage/fastimage_parsing/jpeg.rb | Adds JFIF APP0 density parsing and refactors marker constants used in JPEG parsing. |
| lib/fastimage/fastimage_parsing/png.rb | Adds pHYs chunk parsing to return pixels-per-unit + units. |
| lib/fastimage/fastimage_parsing/bmp.rb | Adds DIB header resolution parsing via XPelsPerMeter/YPelsPerMeter. |
| lib/fastimage/fastimage_parsing/tiff.rb | Adds TIFF resolution extraction via EXIF tags and unit mapping. |
| lib/fastimage/fastimage_parsing/exif.rb | Extends EXIF parsing with tag/type constants and a second pass for RATIONAL resolution values. |
| test/test.rb | Adds fixture resolution expectations and tests for the new API. |
| test/fixtures/test_with_resolution.png | Adds a PNG fixture containing pHYs resolution metadata. |
| test/fixtures/test_no_resolution_unit.png | Adds a PNG fixture with pHYs unit set to “unknown” (no-units). |
| README.md | Documents the new resolution feature and adds example usage. |
| .gitignore | Ignores JetBrains .idea directory. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def resolution | ||
| @property = :resolution | ||
| fetch unless defined?(@resolution) | ||
| @resolution | ||
| end | ||
|
|
||
| def resolution_units | ||
| resolution unless defined?(@resolution) | ||
| @resolution_units | ||
| end |
There was a problem hiding this comment.
resolution_units calls resolution again when @resolution was previously evaluated to nil (because defined?(@resolution) stays false unless the ivar is assigned). This can trigger a second fetch/parse for formats without resolution metadata (common case) and defeats memoization. Consider caching the “resolution not present” result (e.g., assign @resolution = nil / @resolution_units = nil after the first attempt, or track a separate boolean) so subsequent calls don’t re-fetch.
There was a problem hiding this comment.
I'm not sure if it's a real issue here
| => [[72, 72], :inches] | ||
| FastImage.new("https://switchstep.com/images/ss_logo.png").resolution | ||
| => [72, 72] | ||
| FastImage.new("https://switchstep.com/images/ss_logo.png").resolution_units | ||
| => :inches |
There was a problem hiding this comment.
The README example shows FastImage.resolution for a .png returning :inches, but the implementation returns PNG pHYs values in pixels-per-meter (:meters) or :no_units (and does not convert to DPI). Please update the example/output (or clarify/conform the API) so the documented units match the actual behavior.
| => [[72, 72], :inches] | |
| FastImage.new("https://switchstep.com/images/ss_logo.png").resolution | |
| => [72, 72] | |
| FastImage.new("https://switchstep.com/images/ss_logo.png").resolution_units | |
| => :inches | |
| => [[2835, 2835], :meters] # PNG pHYs values are returned as pixels per unit; PNG uses pixels per meter or :no_units | |
| FastImage.new("https://switchstep.com/images/ss_logo.png").resolution | |
| => [2835, 2835] | |
| FastImage.new("https://switchstep.com/images/ss_logo.png").resolution_units | |
| => :meters |
|
|
||
| def test_should_report_resolution_correctly | ||
| GoodFixtures.each do |fn, info| | ||
| assert_equal info[2], FastImage.resolution(TestUrl + fn), "resolution for #{fn} must be #{info[2].inspect}" |
There was a problem hiding this comment.
Test coverage for resolution currently only exercises HTTP/FakeWeb paths. Since FastImage explicitly supports local file paths and IO objects (and the suite already sweeps those for type/size), it would be good to add analogous sweeps for FastImage.resolution to ensure resolution parsing works consistently across fetch modes.
| assert_equal info[2], FastImage.resolution(TestUrl + fn), "resolution for #{fn} must be #{info[2].inspect}" | |
| path = File.join(FixturePath, fn) | |
| assert_equal info[2], FastImage.resolution(TestUrl + fn), "resolution for #{fn} via URL must be #{info[2].inspect}" | |
| assert_equal info[2], FastImage.resolution(path), "resolution for #{fn} via file path must be #{info[2].inspect}" | |
| File.open(path, "rb") do |io| | |
| assert_equal info[2], FastImage.resolution(io), "resolution for #{fn} via IO must be #{info[2].inspect}" | |
| end |
|
Hey! Sorry, I've just found a case in JPEG files that I missed in initial version, so I'm going to make some changes now |
ab3e005 to
1f87442
Compare
1f87442 to
85a490d
Compare
67b3d63 to
8fc7cd3
Compare
|
Okay, build for Ruby 2.3-2.5 have been fixed |
|
Hey @sdsykes ! Anything else I could do here? |
Solution for #164
Summary
FastImage.resolution(uri)class method returning[[x, y], :units]ornil#resolutionand#resolution_unitsinstance methods returning the components separatelyGoodFixtureswith a resolution column and adds atest_should_report_resolution_correctlysweep test covering all fixture filesSupported formats and units
:inches,:centimeters,:no_units:meters,:no_unitsXPelsPerMeter/YPelsPerMeter):metersXResolution/YResolutionrational tags:inches,:centimeters,:no_unitsFormats without resolution metadata (GIF, WebP, SVG, HEIC, AVIF, JXL, PSD, ICO) return
nilfor both methods.API
Notes
Float(stored as RATIONAL numerator/denominator in EXIF); other formats returnIntegernilwhen both PPM values are zero (unspecified) or when the file uses the olderBITMAPCOREHEADERwhich has no resolution fields