@@ -117,3 +117,83 @@ def test_combine_respects_reserved_modules(tmp_path):
117117 assert (mx , my ) not in reserved , (mx , my )
118118 red = _count_red_in_module (out , mx , my , modules_per_unit , qz )
119119 assert red >= 50 , f"data module ({ mx } ,{ my } ) not painted red: { red } /81"
120+
121+
122+ def test_combine_scaling_preserves_aspect_ratio (tmp_path ):
123+ """Non-square backgrounds should retain their aspect ratio after scaling.
124+
125+ Uses a two-tone background (red top half, blue bottom half) so the
126+ vertical split is visible after center-crop. For a portrait bg, the
127+ top and bottom are cropped equally; the red/blue boundary should
128+ remain near the vertical center of the data area.
129+ """
130+ words = "https://github.com"
131+ ver , _ = theqrmodule .get_qrcode (1 , "H" , words , str (tmp_path ))
132+ n = (ver - 1 ) * 4 + 21
133+
134+ # Create a portrait bg (width < height) with distinct top/bottom colors.
135+ # Red top half, blue bottom half — boundary at h/2.
136+ bg_w , bg_h = 80 , 160
137+ bg = Image .new ("RGBA" , (bg_w , bg_h ))
138+ for y in range (bg_h ):
139+ color = (255 , 0 , 0 , 255 ) if y < bg_h // 2 else (0 , 0 , 255 , 255 )
140+ for x in range (bg_w ):
141+ bg .putpixel ((x , y ), color )
142+ bg_path = tmp_path / "two_tone_portrait.png"
143+ bg .save (bg_path )
144+
145+ out_name = "out.png"
146+ rver , rlevel , out_path = amzqr .run (
147+ words ,
148+ level = "H" ,
149+ picture = str (bg_path ),
150+ colorized = True ,
151+ save_name = out_name ,
152+ save_dir = str (tmp_path ),
153+ )
154+ assert rver == ver
155+
156+ out = Image .open (out_path ).convert ("RGBA" )
157+ qz = constant .QUIET_ZONE_MODULES
158+ total_modules = n + 2 * qz
159+ assert out .width % total_modules == 0 , (out .width , total_modules )
160+ modules_per_unit = out .width // total_modules
161+ assert modules_per_unit == constant .PIXELS_PER_MODULE * 3
162+
163+ # Verify sample modules are not in the reserved set before asserting
164+ # colors, so a version change doesn't produce a misleading failure.
165+ reserved = _reserved_modules (ver , n )
166+
167+ # Sample a data module near the TOP of the QR (module (10, 10)) —
168+ # should be RED because the portrait bg's top half is red.
169+ # Use the top-left sub-pixel (i%3=0, j%3=0) to avoid the combine()
170+ # center-sub-pixel skip (i%3==1 and j%3==1).
171+ top_mx , top_my = 10 , 10
172+ assert (top_mx , top_my ) not in reserved , f"top sample module ({ top_mx } ,{ top_my } ) is reserved"
173+ top_x = (qz + top_mx ) * modules_per_unit
174+ top_y = (qz + top_my ) * modules_per_unit
175+ top_px = out .getpixel ((top_x , top_y ))
176+ assert _is_red (top_px ), (
177+ f"top module ({ top_mx } ,{ top_my } ) should be red (center-cropped portrait bg), got { top_px } "
178+ )
179+
180+ # Sample a data module near the BOTTOM of the QR (module (10, n-10)) —
181+ # should be BLUE because the portrait bg's bottom half is blue.
182+ bottom_mx , bottom_my = 10 , n - 10
183+ assert (bottom_mx , bottom_my ) not in reserved , f"bottom sample module ({ bottom_mx } ,{ bottom_my } ) is reserved"
184+ bottom_x = (qz + bottom_mx ) * modules_per_unit
185+ bottom_y = (qz + bottom_my ) * modules_per_unit
186+ bottom_px = out .getpixel ((bottom_x , bottom_y ))
187+ assert bottom_px [2 ] > 200 and bottom_px [0 ] < 50 and bottom_px [1 ] < 50 , (
188+ f"bottom module ({ bottom_mx } ,{ bottom_my } ) should be blue (center-cropped portrait bg), got { bottom_px } "
189+ )
190+
191+ # Also verify reserved modules are still skipped (not colored over).
192+ # Top-left finder area should be mostly untouched (not red/blue).
193+ finder_x0 = qz * modules_per_unit + modules_per_unit // 2
194+ finder_y0 = qz * modules_per_unit + modules_per_unit // 2
195+ fp = out .getpixel ((finder_x0 , finder_y0 ))
196+ # Finder center should be dark (black module), not red or blue.
197+ assert fp [0 ] < 50 and fp [1 ] < 50 and fp [2 ] < 50 , (
198+ f"finder should be dark, got { fp } "
199+ )
0 commit comments