using * object-fit: cover. Falls back to 50/50/1 (dead center, no zoom — today's * default behavior) when the row predates this feature or the values are * otherwise missing, so old photos render exactly as they always have. */ function photo_crop_style(array $row): string { $x = isset($row['photo_focal_x']) && $row['photo_focal_x'] !== null ? (float)$row['photo_focal_x'] : 50.0; $y = isset($row['photo_focal_y']) && $row['photo_focal_y'] !== null ? (float)$row['photo_focal_y'] : 50.0; $zoom = isset($row['photo_zoom']) && $row['photo_zoom'] !== null ? (float)$row['photo_zoom'] : 1.0; $x = max(0, min(100, $x)); $y = max(0, min(100, $y)); $zoom = max(1, min(3, $zoom)); // transform-origin is deliberately left at its CSS default (50% 50% — // the box's own center), not tied to $x/$y: object-position places the // unzoomed crop, and the scale transform then magnifies around whatever // is currently centered, matching the crop editor exactly. return sprintf( 'object-position:%.2f%% %.2f%%;transform:scale(%.3f);', $x, $y, $zoom ); }