Skip to content

fix: VipsDriver#resize_image_to resizes to the requested dimensions - #205

Merged
pftg merged 1 commit into
masterfrom
fix/vips-resize-scale
Aug 22, 2026
Merged

fix: VipsDriver#resize_image_to resizes to the requested dimensions#205
pftg merged 1 commit into
masterfrom
fix/vips-resize-scale

Conversation

@pftg

@pftg pftg commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Bug

VipsDriver#resize_image_to called image.resize(new_width.to_f / new_height). Vips::Image#resize takes a uniform scale factor, not target dimensions — the code was computing the target aspect ratio and passing that as the scale.

Reproduction (test/fixtures/images/a.png, 80x80):

image.resize(new_width.to_f / new_height)   # 40x30 request → resize(40.0/30) = resize(1.333)
# => [107, 107]  (should be [40, 30])

Production call chain

Screenshoter#process_screenshot (screenshoter.rb:54) → #resize_if_needed (screenshoter.rb:114-122), triggered only on macOS + Selenium + a retina display (selenium_with_retina_screen?). For a 2560x1600 screenshot with an expected window width of 1280:

new_height = 1280 * 1600 / 2560          # = 800  (correct, aspect-preserving halve)
driver.resize_image_to(image, 1280, 800) # buggy factor: 1280.0/800 = 1.6
# => [4096, 2560]  (ENLARGED instead of halved to [1280, 800])

The crop step that follows then operates on a misaligned, wrongly-sized image, so retina baselines get saved at the wrong dimensions.

Fix

def resize_image_to(image, new_width, new_height)
  image.resize(new_width.to_f / image.width, vscale: new_height.to_f / image.height)
end

Vips::Image#resize accepts an independent vscale (vertical scale factor) option alongside the required horizontal scale argument — confirmed against the installed ruby-vips 2.3.0 / libvips resize operation:

$ vips resize
usage:
   resize in out scale [--option-name option-value ...]
optional arguments:
   vscale       - Vertical scale image by this factor, input gdouble

This resizes to the exact requested [new_width, new_height] in one call, with no aspect-preserving surprises — the minimal change that keeps the method's existing shape (still image.resize(...), still returns a Vips::Image). Considered Vips::Image#thumbnail_image(width, height:, size: :force) as an alternative; resize with vscale: was chosen as the smaller diff since the method already called resize.

Tests

  1. REDtest/support/driver_contract_tests.rb: added [contract] resize_image_to resizes a non-square source to the exact requested non-square dimensions, run via the shared DriverContractTests module against both drivers (6 instances: 5 ChunkyPNGDriverTest nested classes + 1 VipsDriverTest). Before the fix: 1 failure (Vips) — Expected: [40, 30] Actual: [4, 8] on a 3x6 portrait.png source; ChunkyPNG's resample_bilinear(new_width, new_height) already passed.
  2. GREEN — same contract test passes on both drivers after the fix (35 runs/0 failures on ChunkyPNG, 35 runs/0 failures on Vips, including the new tests).
  3. Production-path guardtest/unit/screenshoter_test.rb: added #resize_if_needed halves a non-square retina screenshot to the expected window size via VipsDriver, driving a synthetic 2560x1600 Vips::Image through the real resize_if_needed formula (Screenshot.window_size stubbed to [1280, 1024]) and asserting [1280, 800]. Verified this reds against the pre-fix code with the exact reported numbers (Expected: [1280, 800] Actual: [4096, 2560]), then confirmed green after the fix.
  4. Ride-along (Sourcery, test: driver contract tests formalizing the current driver seam #204) — strengthened [contract] load_images returns [old_image, new_image] without swapping slots to use a.png (80x80) and a_cropped.png (80x60) instead of same-dimension fixtures, so a swap is actually detectable.

Gate-checks

  • Vips resize bug: reverted the fix locally, re-ran the new #resize_if_needed screenshoter test → red (Actual: [4096, 2560]), matching the bug report's numbers exactly. Reapplied the fix → green.
  • load_images swap: temporarily swapped ChunkyPNGDriver#_load_images's return order → the strengthened contract test reds in all 5 nested test classes (Expected: [80, 80] Actual: [80, 60]). Reverted → green.

Verification

  • mise x ruby@4.0.6 -- bundle exec rake test:unit — 280 runs, 825 assertions, 0 failures, 0 errors, 0 skips (baseline was 267 runs/0 failures before this change; +13 new: 2 new contract tests × 6 driver-test instances + 1 screenshoter test).
  • mise x ruby@4.0.6 -- bundle exec rake test — 313 runs, 870 assertions, 0 failures, 0 errors, 6 skips (baseline 300 runs/0 failures/6 skips).
  • mise x ruby@4.0.6 -- bundle exec standardrb — 97 files inspected, no offenses.
  • CHANGELOG.md not touched.

🤖 Generated with Claude Code

Summary by Sourcery

Correct VipsDriver resizing to use independent horizontal and vertical scale factors while strengthening regression coverage for resizing and image ordering.

Bug Fixes:

  • Fix Vips image resizing so requested width and height are produced exactly, including non-square and retina screenshots.

Tests:

  • Add contract and production-path tests covering exact Vips resize dimensions and retina screenshot scaling.
  • Strengthen image-loading contract coverage to detect swapped old and new image slots.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed image resizing to preserve both the requested width and height, including for non-square images.
    • Corrected handling of differently sized screenshots so images remain in their proper slots.
    • Improved retina image resizing to produce accurate dimensions based on the window size.
  • Tests

    • Added coverage for exact resizing and non-square retina image scenarios.

@sourcery-ai

sourcery-ai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR fixes the Vips driver’s resize logic to use proper horizontal and vertical scale factors so images are resized to the requested dimensions, and adds focused tests to enforce the contract across drivers and along the production retina-screenshot path, plus a strengthened load_images contract.

Sequence diagram for retina screenshot resizing

sequenceDiagram
    participant Screenshoter
    participant VipsDriver
    participant VipsImage
    Screenshoter->>Screenshoter: process_screenshot
    Screenshoter->>Screenshoter: resize_if_needed
    Screenshoter->>VipsDriver: resize_image_to(image, 1280, 800)
    VipsDriver->>VipsImage: resize(1280.0 / image.width, vscale: 800.0 / image.height)
    VipsImage-->>VipsDriver: image sized 1280x800
    VipsDriver-->>Screenshoter: resized image
Loading

File-Level Changes

Change Details Files
Fix VipsDriver#resize_image_to to scale independently in X and Y so the output matches requested dimensions.
  • Change resize_image_to to compute horizontal scale as new_width.to_f / image.width.
  • Pass vscale: new_height.to_f / image.height to Vips::Image#resize instead of using aspect-ratio-based scale.
  • Keep method signature and return type unchanged, still returning a Vips::Image.
lib/capybara/screenshot/diff/drivers/vips_driver.rb
Add driver contract tests that validate resizing behavior and image-slot ordering for all drivers.
  • Add a contract test ensuring resize_image_to produces the exact requested non-square dimensions for a non-square source image.
  • Add a contract test ensuring load_images returns [old_image, new_image] without swapping the two image slots, using different-sized fixtures to make swaps observable.
test/support/driver_contract_tests.rb
Add a production-path unit test for retina screenshot resizing via VipsDriver to guard against regressions.
  • Add a ScreenshoterTest that constructs a synthetic non-square retina Vips::Image at 2x window size.
  • Stub Screenshot.window_size and assert that resize_if_needed halves the image to the expected window width and computed height via the Vips driver.
test/unit/screenshoter_test.rb

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: df714d49-438f-48a9-99d4-d51445f49283

📥 Commits

Reviewing files that changed from the base of the PR and between 9614078 and 56f2cc7.

📒 Files selected for processing (3)
  • lib/capybara/screenshot/diff/drivers/vips_driver.rb
  • test/support/driver_contract_tests.rb
  • test/unit/screenshoter_test.rb

📝 Walkthrough

Walkthrough

The Vips driver now uses independent horizontal and vertical scaling factors. Tests cover exact resizing of non-square images, retina screenshot resizing, and image slot identity.

Changes

Vips image resizing

Layer / File(s) Summary
Independent resize factors and validation
lib/capybara/screenshot/diff/drivers/vips_driver.rb, test/support/driver_contract_tests.rb, test/unit/screenshoter_test.rb
resize_image_to passes separate scaling factors for width and height. Contract tests verify exact target dimensions and image slot identity. Screenshoter tests verify non-square retina resizing.

Estimated code review effort: 2 (Simple) | ~10 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/vips-resize-scale

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Approved.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

VipsDriver#resize_image_to passed image.resize() the aspect ratio
(new_width / new_height) instead of a scale factor, which is what
Vips::Image#resize expects. An 80x80 source resized to [40, 30]
returned [107, 107] instead of [40, 30].

On the production path (Screenshoter#resize_if_needed, macOS +
Selenium + retina), a 2560x1600 screenshot with expected width 1280
computed the buggy factor 1280/800 = 1.6, enlarging to [4096, 2560]
instead of halving to [1280, 800].

Fix uses libvips resize's documented vscale option to set width and
height scale factors independently: image.resize(scale, vscale:).

Adds a strict driver-contract test (resize_image_to on a non-square
source to a non-square target returns exactly [new_width, new_height])
that reds on Vips and passes on ChunkyPNG, plus a focused
Screenshoter test exercising resize_if_needed's formula through
VipsDriver on a synthetic 2560x1600 retina image, and strengthens the
load_images ordering contract test to use fixtures with different
dimensions so a swapped return goes red.
@pftg
pftg force-pushed the fix/vips-resize-scale branch from f28cb05 to 56f2cc7 Compare August 22, 2026 14:57
@pftg
pftg merged commit 87fc4c2 into master Aug 22, 2026
5 of 6 checks passed
@pftg
pftg deleted the fix/vips-resize-scale branch August 22, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant