@@ -28,7 +28,6 @@ import { CANVAS_DIMENSIONS, VALID_CANVAS_RESOLUTIONS, type CanvasResolution } fr
2828export type OutputResolutionIssueKind =
2929 | "hdr-incompatible"
3030 | "aspect-mismatch"
31- | "downsampling"
3231 | "non-integer-scale" ;
3332
3433export interface OutputResolutionCompatibility {
@@ -69,24 +68,35 @@ export function suggestMatchingPreset(
6968 compositionHeight : number ,
7069 chosen : CanvasResolution ,
7170) : CanvasResolution | undefined {
72- const aspectMatches : CanvasResolution [ ] = VALID_CANVAS_RESOLUTIONS . filter ( ( preset ) => {
73- const { width, height } = CANVAS_DIMENSIONS [ preset ] ;
74- // Integer-safe aspect compare (cross-multiplication), matching the
75- // producer's mismatch check exactly.
76- return width * compositionHeight === height * compositionWidth ;
77- } ) ;
71+ const aspectMatches : CanvasResolution [ ] = VALID_CANVAS_RESOLUTIONS . filter ( ( preset ) =>
72+ fitsAspect ( CANVAS_DIMENSIONS [ preset ] , compositionWidth , compositionHeight ) ,
73+ ) ;
7874 if ( aspectMatches . length === 0 ) return undefined ;
7975
8076 // Prefer the aspect-matching preset in the same resolution tier as the chosen
81- // one so we don't silently downgrade 4K → HD (or vice versa). Tier is keyed
82- // off the `-4k` suffix rather than long-side pixels: a 4K *square* (2160×2160)
77+ // one so we don't silently change 4K → HD (or vice versa). Tier is keyed
78+ // off the suffix ( `-4k`, `-720p`, …) rather than long-side pixels: a 4K *square* (2160×2160)
8379 // has a shorter long side than 4K landscape (3840), so a pixel-based compare
8480 // would fail to recognise `square-4k` as the 4K peer and downgrade to `square`.
85- const chosenIs4k = chosen . endsWith ( "-4k" ) ;
86- const sameTier = aspectMatches . find ( ( preset ) => preset . endsWith ( "-4k" ) === chosenIs4k ) ;
81+ const sameTier = aspectMatches . find ( ( preset ) => tierOf ( preset ) === tierOf ( chosen ) ) ;
8782 return sameTier ?? aspectMatches [ 0 ] ;
8883}
8984
85+ /** `landscape-4k` → `-4k`, `portrait` → `` (1080p). */
86+ const tierOf = ( preset : CanvasResolution ) =>
87+ preset . includes ( "-" ) ? preset . slice ( preset . indexOf ( "-" ) ) : "" ;
88+
89+ /** Integer-safe aspect compare. A smaller preset may be under a pixel off on its long side (854×480 at 16:9). */
90+ function fitsAspect (
91+ target : { width : number ; height : number } ,
92+ compositionWidth : number ,
93+ compositionHeight : number ,
94+ ) : boolean {
95+ const drift = Math . abs ( target . width * compositionHeight - target . height * compositionWidth ) ;
96+ if ( drift === 0 ) return true ;
97+ return target . width < compositionWidth && drift < Math . min ( compositionWidth , compositionHeight ) ;
98+ }
99+
90100function describeOrientation ( width : number , height : number ) : string {
91101 if ( width > height ) return "landscape" ;
92102 if ( width < height ) return "portrait" ;
@@ -154,26 +164,14 @@ export function checkOutputResolutionCompatibility(input: {
154164 }
155165
156166 const target = CANVAS_DIMENSIONS [ outputResolution ] ;
157- // Aspect-ratio compare via cross-multiplication so the equality is integer-
158- // safe. Float division (`target.width / compositionWidth`) loses precision
159- // for non-power-of-2 ratios (e.g. cinema 4K 4096×2160 = 1.8963…) and a
160- // future preset could trip a false-mismatch on otherwise valid input.
161- if ( target . width * compositionHeight !== target . height * compositionWidth ) {
167+ if ( ! fitsAspect ( target , compositionWidth , compositionHeight ) ) {
162168 return buildAspectMismatch ( compositionWidth , compositionHeight , outputResolution , target ) ;
163169 }
164170
165- // Aspect ratios match → widthRatio === heightRatio. Compute once.
171+ // A smaller preset renders at a device scale factor under 1: Chrome lays the
172+ // page out at composition size and rasterizes it straight at the preset's.
166173 const widthRatio = target . width / compositionWidth ;
167- if ( widthRatio < 1 ) {
168- return {
169- ok : false ,
170- kind : "downsampling" ,
171- message :
172- `outputResolution ${ outputResolution } (${ target . width } ×${ target . height } ) ` +
173- `is smaller than the composition (${ compositionWidth } ×${ compositionHeight } ). ` +
174- `Downsampling via --resolution is not supported.` ,
175- } ;
176- }
174+ if ( widthRatio < 1 ) return OK ;
177175
178176 if ( ! Number . isInteger ( widthRatio ) ) {
179177 return {
0 commit comments