Skip to content

Commit 33bcd47

Browse files
authored
fix(media): flow_diagram pads to 16:9 at native size, never downscales (#2901)
The template was shrinking the diagram to fit a fixed 1200x675 canvas, costing resolution and making nodes smaller than needed. It now renders the diagram at full native size and pads the canvas out to a 16:9 aspect ratio around it (a 4-node flow becomes ~1700x957). A named preset applies its aspect ratio at native scale; explicit width/height grows to content instead of clipping. No content is ever downscaled. Smoke test updated to assert aspect ratio + native-size behavior.
1 parent b2db7dc commit 33bcd47

2 files changed

Lines changed: 85 additions & 49 deletions

File tree

inc/Abilities/Media/Templates/FlowDiagramTemplate.php

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,8 @@ public function render( array $data, GDRenderer $renderer, array $options = arra
139139
}
140140
}
141141

142-
// Resolve the target canvas first (default is a 16:9 card). The diagram
143-
// grid is then scaled to fit inside it and centered, so a bare render
144-
// always produces a clean, correctly-proportioned image.
145-
$canvas = $this->resolve_canvas( $options );
146-
$width = $canvas['width'];
147-
$height = $canvas['height'];
148-
149-
// Base (unscaled) grid metrics.
142+
// Grid metrics are always full size. The diagram is NEVER downscaled;
143+
// instead the canvas is grown around the native-size content.
150144
$node_w = self::NODE_WIDTH;
151145
$node_h = self::NODE_HEIGHT;
152146
$gap = $has_edge_labels ? self::GAP + 90 : self::GAP;
@@ -155,28 +149,17 @@ public function render( array $data, GDRenderer $renderer, array $options = arra
155149
$content_w = $this->grid_width( $direction, $count, $node_w, $gap, $margin );
156150
$content_h = $this->grid_height( $direction, $count, $node_h, $gap, $margin, $title_band );
157151

158-
// Scale the whole grid down uniformly if it overflows the canvas in
159-
// either axis. Never scale up past the base size (keeps small diagrams
160-
// from ballooning on a large canvas).
161-
$scale = min( 1.0, $width / max( 1, $content_w ), $height / max( 1, $content_h ) );
162-
if ( $scale < 1.0 ) {
163-
// floor() (not round()) guarantees the scaled grid never exceeds the
164-
// target canvas by a rounding pixel, so preset dimensions stay exact.
165-
$node_w = (int) floor( $node_w * $scale );
166-
$node_h = (int) floor( $node_h * $scale );
167-
$gap = (int) floor( $gap * $scale );
168-
$margin = (int) floor( $margin * $scale );
169-
$title_band = (int) floor( $title_band * $scale );
170-
$content_w = $this->grid_width( $direction, $count, $node_w, $gap, $margin );
171-
$content_h = $this->grid_height( $direction, $count, $node_h, $gap, $margin, $title_band );
172-
}
152+
// Resolve the canvas:
153+
// - Explicit width/height or preset: use it, but grow (never shrink)
154+
// so content is never clipped and never downscaled.
155+
// - Default: render the content at native size, then pad the canvas
156+
// out to a 16:9 aspect ratio around it.
157+
$canvas = $this->resolve_canvas( $options, $content_w, $content_h );
158+
$width = $canvas['width'];
159+
$height = $canvas['height'];
173160

174-
// If content is still larger than the canvas (single very wide node),
175-
// grow the canvas so nothing clips.
176-
$width = max( $width, $content_w );
177-
$height = max( $height, $content_h );
178-
$off_x = intdiv( $width - $content_w, 2 );
179-
$off_y = intdiv( $height - $content_h, 2 );
161+
$off_x = intdiv( $width - $content_w, 2 );
162+
$off_y = intdiv( $height - $content_h, 2 );
180163

181164
$renderer->create_canvas( $width, $height );
182165

@@ -301,33 +284,72 @@ public function render( array $data, GDRenderer $renderer, array $options = arra
301284
}
302285

303286
/**
304-
* Resolve the target output canvas size.
287+
* Resolve the output canvas size around native-size content.
305288
*
306-
* Precedence:
307-
* 1. Explicit options[width] + options[height]
308-
* 2. options[preset] resolved via PlatformPresets
309-
* 3. The template default preset (16:9 twitter_card, 1200x675)
289+
* The diagram is never downscaled. The canvas is sized to at least the
290+
* content, then:
291+
* - Explicit width/height or a named preset: grow that target to the
292+
* content if the content is larger (never clip, never shrink).
293+
* - Default: pad the content out to a 16:9 aspect ratio, so a bare
294+
* render produces a clean 16:9 card at the content's native resolution
295+
* (e.g. a 4-node flow becomes ~1600x900, not a shrunk 1200x675).
310296
*
311-
* @param array $options Render options (preset, width, height).
297+
* @param array $options Render options (preset, width, height, aspect).
298+
* @param int $content_w Native content width.
299+
* @param int $content_h Native content height.
312300
* @return array{width: int, height: int}
313301
*/
314-
private function resolve_canvas( array $options ): array {
302+
private function resolve_canvas( array $options, int $content_w, int $content_h ): array {
303+
// Explicit target dimensions (grow to content, never shrink).
315304
if ( ! empty( $options['width'] ) && ! empty( $options['height'] ) ) {
316305
return array(
317-
'width' => (int) $options['width'],
318-
'height' => (int) $options['height'],
306+
'width' => max( (int) $options['width'], $content_w ),
307+
'height' => max( (int) $options['height'], $content_h ),
319308
);
320309
}
321310

322-
$preset = ! empty( $options['preset'] ) ? (string) $options['preset'] : self::DEFAULT_PRESET;
323-
$dims = PlatformPresets::dimensions( $preset );
324-
if ( ! $dims ) {
325-
$dims = PlatformPresets::dimensions( self::DEFAULT_PRESET );
311+
// Named preset: use its aspect ratio, applied at native content scale.
312+
if ( ! empty( $options['preset'] ) ) {
313+
$dims = PlatformPresets::dimensions( (string) $options['preset'] );
314+
if ( $dims && ! empty( $dims['width'] ) && ! empty( $dims['height'] ) ) {
315+
return $this->pad_to_aspect( $content_w, $content_h, (int) $dims['width'], (int) $dims['height'] );
316+
}
317+
}
318+
319+
// Default: pad out to 16:9 around the native content.
320+
return $this->pad_to_aspect( $content_w, $content_h, 16, 9 );
321+
}
322+
323+
/**
324+
* Grow a content box to a target aspect ratio without shrinking it.
325+
*
326+
* Whichever axis is short is padded up so width:height matches
327+
* aspect_w:aspect_h. The content is never scaled, only surrounded.
328+
*
329+
* @param int $content_w Native content width.
330+
* @param int $content_h Native content height.
331+
* @param int $aspect_w Target aspect numerator.
332+
* @param int $aspect_h Target aspect denominator.
333+
* @return array{width: int, height: int}
334+
*/
335+
private function pad_to_aspect( int $content_w, int $content_h, int $aspect_w, int $aspect_h ): array {
336+
$content_w = max( 1, $content_w );
337+
$content_h = max( 1, $content_h );
338+
339+
// Width needed to make this height match the aspect, and vice versa.
340+
$needed_w = (int) ceil( $content_h * $aspect_w / $aspect_h );
341+
$needed_h = (int) ceil( $content_w * $aspect_h / $aspect_w );
342+
343+
if ( $needed_w >= $content_w ) {
344+
return array(
345+
'width' => $needed_w,
346+
'height' => $content_h,
347+
);
326348
}
327349

328350
return array(
329-
'width' => (int) ( $dims['width'] ?? 1200 ),
330-
'height' => (int) ( $dims['height'] ?? 675 ),
351+
'width' => $content_w,
352+
'height' => $needed_h,
331353
);
332354
}
333355

tests/flow-diagram-template-smoke.php

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,19 +135,33 @@ function get_template_directory() {
135135
$info = getimagesize( $path );
136136
$assert( 'output is a valid PNG', is_array( $info ) && $info['mime'] === 'image/png' );
137137
$assert( 'output has sane dimensions', is_array( $info ) && $info[0] > 100 && $info[1] > 100 );
138-
// Default canvas is a 16:9 twitter_card (1200x675).
139-
$assert( 'default render is 16:9 (1200x675)', is_array( $info ) && $info[0] === 1200 && $info[1] === 675 );
138+
// Default: content rendered at native size, canvas padded to 16:9.
139+
// The 4-node flow is wider than 1200, so the diagram is NOT downscaled
140+
// into a fixed card; the canvas grows around it at a 16:9 ratio.
141+
$ratio = is_array( $info ) ? $info[0] / $info[1] : 0;
142+
$assert( 'default render is ~16:9 aspect', abs( $ratio - ( 16 / 9 ) ) < 0.02 );
143+
$assert( 'content rendered at native size (not downscaled to 1200 wide)', is_array( $info ) && $info[0] > 1200 );
140144
wp_delete_file( $path );
141145
}
142146

143-
// Explicit preset override is honored.
147+
// Explicit preset uses that aspect ratio (still at native content scale).
144148
$og = $template->render( $spec, new GDRenderer(), array( 'preset' => 'open_graph' ) );
145149
if ( ! empty( $og[0] ) && file_exists( $og[0] ) ) {
146-
$oginfo = getimagesize( $og[0] );
147-
$assert( 'preset override honored (open_graph 1200x630)', is_array( $oginfo ) && $oginfo[0] === 1200 && $oginfo[1] === 630 );
150+
$oginfo = getimagesize( $og[0] );
151+
$ogratio = is_array( $oginfo ) ? $oginfo[0] / $oginfo[1] : 0;
152+
$expected = 1200 / 630;
153+
$assert( 'preset override applies its aspect ratio', abs( $ogratio - $expected ) < 0.03 );
148154
wp_delete_file( $og[0] );
149155
}
150156

157+
// Explicit width/height grows to content, never clips.
158+
$fixed = $template->render( $spec, new GDRenderer(), array( 'width' => 400, 'height' => 300 ) );
159+
if ( ! empty( $fixed[0] ) && file_exists( $fixed[0] ) ) {
160+
$finfo = getimagesize( $fixed[0] );
161+
$assert( 'explicit small canvas grows to content (no clip)', is_array( $finfo ) && $finfo[0] >= 400 && $finfo[1] >= 300 );
162+
wp_delete_file( $fixed[0] );
163+
}
164+
151165
// Empty nodes must fail gracefully (no fatal, empty result).
152166
$empty = $template->render( array( 'nodes' => array() ), new GDRenderer() );
153167
$assert( 'empty nodes returns empty array without fatal', is_array( $empty ) && count( $empty ) === 0 );

0 commit comments

Comments
 (0)