Skip to content

Conversation

@PrathamMehta101
Copy link

@PrathamMehta101 PrathamMehta101 commented Dec 25, 2025

Fix: Maintain Center Alignment During Zoom & Reset (Issue #656)

This PR fixes the behavior where images would drift off-center after zooming or switching images.

What was happening

  • Zoom + pan state was being reused across images
  • When zooming back out, transforms were not being fully reset
  • Navigating to another image opened it already shifted/zoomed

What this PR changes

  • Always initialize images with a clean transform (center + default zoom)
  • Reset transform when resetSignal or imagePath changes
  • Prevent lingering pan values from affecting subsequent images

Result

  • Images always open centered
  • Zooming back out snaps cleanly back to center
  • Navigation between images feels natural and consistent

This brings the viewer behavior closer to typical image viewers and significantly improves UX.

Closes #656.

Summary by CodeRabbit

  • Documentation

    • Updated API schema documentation with refined parameter definitions and improved metadata validation constraints.
  • Enhancements

    • Improved image viewer with enhanced zoom controls, expanded scaling capabilities, and optimized image rendering for better visual quality.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 25, 2025

📝 Walkthrough

Walkthrough

This pull request updates the OpenAPI schema documentation for query parameters and metadata properties, and refactors the ImageViewer component to improve zoom-on-scroll behavior by wrapping the TransformWrapper component, adding wheel step configuration, increasing maximum scale, and adjusting image fitting properties from contain to cover.

Changes

Cohort / File(s) Summary
OpenAPI Schema Updates
docs/backend/backend_python/openapi.json
Modified input_type query parameter to use allOf wrapper around InputType reference with explicit title and default value; removed additionalProperties: true constraint from ImageInCluster.Metadata object schema
ImageViewer Component Refactoring
frontend/src/components/Media/ImageViewer.tsx
Wrapped TransformWrapper in container div; added key prop for state reset on image change; introduced wheel.step configuration (0.15); increased maxScale from 8 to 10; removed minScale; changed image objectFit from contain to cover; restructured TransformComponent usage with adjusted styling

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Possibly related PRs

Suggested labels

enhancement, UI, frontend

Suggested reviewers

  • rahulharpal1603

Poem

🐰 The viewer zooms with smoother grace,
With wheels that step with measured pace,
A key resets the pan and scale,
Ten times larger—no need to wail!
The image now fits just right in place,
A better zoom within the space.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses center alignment maintenance but does not implement the core objectives from issue #656: dynamic fitScale computation, axis-dependent zoom anchoring, or automatic re-centering logic are not present in the changes. Implement dynamic minimum zoom based on image/container dimensions, axis-dependent zoom anchoring (Case 1/2/3), and automatic re-centering with panning offset reset when zooming back to minimum.
Out of Scope Changes check ⚠️ Warning Changes to OpenAPI schema (additionalProperties removal, allOf wrapping) and image viewer styling/structure appear unrelated to the zoom/pan behavior objectives specified in issue #656. Clarify why OpenAPI schema modifications are necessary for this zoom fix, or consider separating unrelated changes into a distinct PR for better clarity and traceability.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the fix for maintaining center alignment during zoom and reset operations, directly matching the main objective of the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@github-actions github-actions bot added bug Something isn't working frontend labels Dec 25, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/backend/backend_python/openapi.json (1)

2204-2213: Address inconsistent metadata schema between ImageInCluster and ImageData.

The metadata property in ImageInCluster is currently defined as a bare object type (or null) without any properties, while ImageData.metadata references MetadataModel which has explicit properties. This creates:

  1. Schema inconsistency: Same logical field using incompatible schemas across models
  2. Potential breaking change: The bare object type is more restrictive and may reject metadata properties that the backend intends to return

Update ImageInCluster.metadata to reference MetadataModel for consistency with ImageData.metadata:

"metadata": {
  "anyOf": [
    {
      "$ref": "#/components/schemas/MetadataModel"
    },
    {
      "type": "null"
    }
  ],
  "title": "Metadata"
}
🧹 Nitpick comments (2)
frontend/src/components/Media/ImageViewer.tsx (1)

30-32: Potential redundant reset with key-based approach.

The useEffect that calls resetTransform() on resetSignal changes may be redundant since the component now remounts entirely when imagePath changes (due to the key prop on line 38). This could lead to:

  1. Double resets when both imagePath and resetSignal change simultaneously
  2. Confusion about which mechanism is responsible for resetting

Clarify whether resetSignal is used for scenarios other than image path changes, or consolidate the reset logic.

docs/backend/backend_python/openapi.json (1)

1-2927: OpenAPI changes are unrelated to PR objectives.

This PR is titled "Fix: Maintain Center Alignment During Zoom & Reset (Issue #656)" and the objectives focus entirely on ImageViewer zoom behavior. However, this OpenAPI schema file contains backend API documentation changes that are:

  1. Not mentioned in the PR title or objectives
  2. Unrelated to the zoom fix functionality
  3. Mixed with the frontend zoom changes

Consider separating unrelated changes into different PRs to:

  • Improve reviewability and traceability
  • Avoid coupling unrelated changes
  • Make it easier to revert specific changes if needed
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81286fa and a598ceb.

📒 Files selected for processing (2)
  • docs/backend/backend_python/openapi.json
  • frontend/src/components/Media/ImageViewer.tsx
🔇 Additional comments (2)
frontend/src/components/Media/ImageViewer.tsx (1)

44-44: Verify centerOnInit behavior with key-based resets.

The centerOnInit prop centers the content on initial mount, but since you're using key={imagePath} to remount the component on every image change, verify that this doesn't cause unintended behavior such as:

  1. Brief flickers or layout shifts during the remount
  2. Conflicts with the imperative resetTransform() calls from the useEffect hook (line 31)
  3. Race conditions if imagePath changes while a reset is in progress

Consider testing rapid image navigation to ensure smooth transitions without visual artifacts.

docs/backend/backend_python/openapi.json (1)

1120-1128: LGTM: Proper OpenAPI schema composition.

Wrapping the InputType reference in allOf is the correct OpenAPI 3.1 pattern for adding metadata (description, default, title) to a referenced schema. The default value of "path" is reasonable and matches the description.

Comment on lines +36 to 46
<TransformWrapper
ref={transformRef}
key={imagePath}
initialScale={1}
maxScale={10}
wheel={{
step: 0.15,
}}
centerOnInit
limitToBounds={false}
>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find and read the ImageViewer.tsx file to verify the implementation
find . -name "ImageViewer.tsx" -type f

Repository: AOSSIE-Org/PictoPy

Length of output: 109


🏁 Script executed:

# Read the ImageViewer.tsx file to verify the implementation details
cat -n ./frontend/src/components/Media/ImageViewer.tsx

Repository: AOSSIE-Org/PictoPy

Length of output: 3028


🏁 Script executed:

# Search for issue #656 or related requirements documentation
rg -i "issue.*656|#656" --type-list | head -20
rg -i "dynamic.*scale|fit.*scale|minscale|zoom.*behavior" -A 3 -B 3

Repository: AOSSIE-Org/PictoPy

Length of output: 50781


🏁 Script executed:

# Search for GitHub issues or requirements documentation about zoom behavior
find . -name "*.md" -o -name "*.txt" | xargs grep -l "656\|zoom.*scale\|fit.*screen" 2>/dev/null | head -10

Repository: AOSSIE-Org/PictoPy

Length of output: 44


🌐 Web query:

react-zoom-pan-pinch library features minScale maxScale dynamic zoom capabilities

💡 Result:

Short answer: react-zoom-pan-pinch supports min/max scale and dynamic programmatic zoom. You can pass minScale and maxScale props to constrain zoom, and control zoom at runtime via the TransformWrapper render-props API (methods like zoomIn, zoomOut, setTransform, zoomToElement/zoomToPoint, zoomToCenter, etc.), so you can change scale/position dynamically or call zoom actions from buttons/handlers. The min/max bounds will clamp programmatic zoom operations (so increase maxScale if a programmatic zoom is being limited). [1][2][3]

Sources

  • Library repo / README (features + control API). [1]
  • npm package docs showing minScale / maxScale and other props. [2]
  • Example/issue explaining minScale/maxScale limiting zoomToElement and solution (increase/remove maxScale). [3]

🌐 Web query:

issue 656 PictoPy zoom image viewer requirements

💡 Result:

I can fetch and summarize the exact requirements from PictoPy issue #656 — do you want me to open the project's GitHub issue and pull the full text now?


🏁 Script executed:

# Search for GitHub issues or PR references in the codebase
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "*.tsx" -o -name "*.ts" \) -exec grep -l "#656\|issue.*656" {} \; 2>/dev/null | head -5

Repository: AOSSIE-Org/PictoPy

Length of output: 44


Critical: Implementation doesn't match stated requirements and library capabilities.

The code is missing essential zoom behavior features that react-zoom-pan-pinch supports:

  1. Missing minScale: No minScale prop is set, allowing infinite zoom-out and creating the "tiny/floating images" problem.
  2. Hardcoded maxScale: maxScale={10} is hardcoded instead of being computed dynamically based on image and viewport dimensions.
  3. No dynamic fitScale computation: The code should compute a dynamic minimum scale—fit-to-screen when image exceeds viewport, or original resolution otherwise—but uses a hardcoded initialScale={1}.
  4. Missing axis-dependent zoom anchoring: No implementation of zoom behavior based on overflow state (no overflow, one axis overflowing, both axes overflowing).
  5. No automatic re-centering: Missing logic to snap the image to center and reset panning offsets when zooming back to minimum scale.
  6. objectFit: 'cover' crops images: Using 'cover' crops images instead of fitting them (use 'contain' for fit-to-screen behavior).

The key={imagePath} approach resets the entire transform state when switching images, which is a workaround rather than implementing the zoom behavior system described.

🤖 Prompt for AI Agents
In frontend/src/components/Media/ImageViewer.tsx around lines 36–46, the
TransformWrapper usage lacks required zoom behavior: add a computed minScale
(fitScale) instead of omitting minScale, compute dynamic maxScale (don’t
hardcode 10) from image and viewport dimensions (e.g., based on image native
resolution and DPR or a multiple of fitScale), set initialScale to the computed
fitScale (fit-to-screen when image is larger, otherwise 1), replace
objectFit:'cover' with 'contain', remove the key={imagePath} reset workaround
and instead preserve TransformWrapper state across images, implement
axis-dependent zoom anchoring (decide anchor based on whether width/height
overflow the viewport and pass appropriate center/anchor values on zoom), and
add logic to auto-recenter/reset pan offsets when scale equals minScale so the
image snaps to center; compute image and viewport sizes on load/resize and
update these props and state accordingly.

<div style={{ width: '100%', height: '100%' }}>
<TransformWrapper
ref={transformRef}
key={imagePath}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Using key prop to reset state is a workaround, not a proper fix.

While key={imagePath} does reset the transform state when changing images (addressing one symptom), this approach:

  1. Completely reinitializes the TransformWrapper component, which may cause performance issues or brief flickers
  2. Doesn't address the underlying zoom behavior requirements from issue BUG: Zoom-on-Scroll Behavior Does Not Match Expected Media Viewer UX #656
  3. Prevents intentional state preservation scenarios (e.g., maintaining zoom level when navigating between similar images)

The proper fix would compute appropriate initial zoom/pan values based on the new image dimensions rather than forcing a full component remount.

🤖 Prompt for AI Agents
In frontend/src/components/Media/ImageViewer.tsx around line 38, using
key={imagePath} to force-remount TransformWrapper is a workaround that resets
state and causes flicker; instead, compute and apply appropriate initial zoom
and pan when the image changes: detect when imagePath (or its
naturalWidth/naturalHeight) updates, calculate a fit-to-container scale and
centered translation (or reuse previous zoom when images are similar), then call
TransformWrapper/TransformComponent API or controlled props to set
initialTransform/position/scale without remounting; remove the key-based remount
and ensure updates happen only when dimensions change to preserve intentional
state and avoid performance issues.

Comment on lines +71 to +74
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'cover',
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: objectFit 'cover' will crop images instead of fitting them.

Changing from objectFit: 'contain' to objectFit: 'cover' contradicts the PR objective of "fit-to-screen" behavior. The cover value will crop the image to fill the container, potentially cutting off parts of the image. This is especially problematic for portrait images in landscape containers or vice versa.

The PR objectives specify that the minimum zoom should be "fit-to-screen when image > viewport," which aligns with contain behavior, not cover.

🔎 Recommended fix
 style={{
   maxWidth: '100%',
   maxHeight: '100%',
-  objectFit: 'cover',
+  objectFit: 'contain',
   zIndex: 50,
   transform: `rotate(${rotation}deg)`,
 }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'cover',
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
🤖 Prompt for AI Agents
In frontend/src/components/Media/ImageViewer.tsx around lines 71 to 74, the
style sets objectFit: 'cover' which crops images and violates the PR's
fit-to-screen requirement; change objectFit back to 'contain' so images are
scaled to fit within the viewport without cropping, ensuring the minimum zoom
behaves as "fit-to-screen" for images larger than the viewport.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Zoom-on-Scroll Behavior Does Not Match Expected Media Viewer UX

1 participant