Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/api/stack-image-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Stack image template

Use `datamachine/render-image-template` with `template_id: "stack"`, or:

```sh
wp datamachine image render --template_id=stack --preset=print_pocket_flyer --data='{"background":"#182028","blocks":[{"type":"text","text":"Get involved","font":"heading","size":80,"fit":true},{"type":"qr","url":"https://example.org/join","size":0.45,"gap_before":24},{"type":"list","items":["Scan the code","Choose a project","Invite a friend"],"style":"numbered","size":36,"gap_before":24}]}'
```

All platform presets work. Pixel dimensions scale from a 1080px-wide base.
Top-level fields are `background` (hex or brand color token, default
`background_dark`), `padding` (64), `align` (`center` or `left`), and ordered
`blocks`. Colors accept hex or brand color keys; fonts accept `heading`, `body`,
`display`, or `brand`.

Every block accepts `gap_before` in base pixels:

| Type | Fields |
| --- | --- |
| `text` | `text`, `font`, `color`, `size` (32), `fit`, `max_size`, `wrap` (true), `line_height` (1.2), `align` |
| `image` | `path` or `attachment_id`, `width` (fraction <=1 or base pixels), `max_height` |
| `qr` | `url`, `size` (content-width fraction, default .5), `card_color` (white), `quiet_modules` (1–4) |
| `row` | `items` (text/image blocks), `gap` (16 base pixels); one line, vertically centered |
| `list` | string `items`, `style` (`numbered`, `bullets`, `plain`), `marker_color`, `font`, `color`, `size`, `line_height` |
| `divider` | `width` (fraction or base pixels), `thickness` (2), `color` |
| `spacer` | `height`, `flex` |

`fit` text shrinks to fit its width; with wrapping enabled it fits the longest
word, and with `wrap: false` it fits the entire line. `max_size` optionally
sets a larger starting size. If the stack is too tall, fit-enabled text and
lists shrink proportionally, down to 1px, before reporting overflow. Fixed
images, QR codes, spacers and non-fitting text retain their dimensions. No
blocks are removed to resolve overflow.

Flex spacers share spare height; without flex spacers, spare height is spread
between blocks. QR quiet-zone cards are included in layout bounds and use
nearest-neighbor scaling. A QR provider must answer the existing
`datamachine/image_generation/qr_image_path` filter; it receives the final QR
pixel size and returns a temporary image that the template deletes.

Ability results include `warnings` for unresolved overflow, unavailable assets
and unsupported blocks. `dropped_fields` identifies skipped asset/block paths;
height overflow itself does not mark blocks as dropped. The CLI prints warnings.
13 changes: 12 additions & 1 deletion inc/Abilities/Media/ImageTemplateAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ private static function render_image_template_definition(): array {
),
'data' => array(
'type' => 'object',
'description' => 'Structured data matching the template fields',
'description' => 'Structured data matching the template fields. stack accepts background (hex/token), padding (1080px base), align (center|left), blocks: ordered text (text/font/color/size/fit/max_size/wrap/line_height/align), image (path or attachment_id/width/max_height), qr (url/size/card_color/quiet_modules), row (text/image items, gap), list (items/style/marker_color/font/color/size), divider (width/thickness/color), spacer (height/flex). Every block accepts gap_before. Widths are fractions or base px; QR size is a fraction.',
),
'brand' => BrandTokens::override_schema(),
'preset' => array(
Expand Down Expand Up @@ -177,6 +177,11 @@ private static function render_image_template_definition(): array {
'items' => array( 'type' => 'string' ),
'description' => 'Field keys the template omitted to make the layout fit. Empty when every field was drawn.',
),
'warnings' => array(
'type' => 'array',
'items' => array( 'type' => 'string' ),
'description' => 'Layout and asset warnings, including unresolved stack overflow. No blocks are removed to fit a stack.',
),
'message' => array( 'type' => 'string' ),
),
),
Expand Down Expand Up @@ -308,6 +313,10 @@ public static function renderTemplate( array $input ): array|\WP_Error {
? array_values( array_map( 'strval', (array) $template->get_dropped_fields() ) )
: array();

$warnings = method_exists( $template, 'get_warnings' )
? array_values( array_map( 'strval', (array) $template->get_warnings() ) )
: array();

// Keep local paths intact for in-process callers; publish temp renders
// through the existing repository for remote callers.
if ( 'cached_file' !== $output ) {
Expand Down Expand Up @@ -336,6 +345,7 @@ public static function renderTemplate( array $input ): array|\WP_Error {
'file_urls' => $file_urls,
'template_id' => $template_id,
'dropped_fields' => $dropped_fields,
'warnings' => $warnings,
'message' => sprintf( 'Generated %d image(s) using template "%s"', count( $file_paths ), $template_id ),
);
}
Expand All @@ -352,6 +362,7 @@ public static function renderTemplate( array $input ): array|\WP_Error {
'success' => true,
'template_id' => $template_id,
'dropped_fields' => $dropped_fields,
'warnings' => $warnings,
),
$cached_result
);
Expand Down
28 changes: 2 additions & 26 deletions inc/Abilities/Media/Templates/PosterTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -1593,18 +1593,7 @@ private static function widest_line_width( GDRenderer $r, string $text, int $siz
* @param int $w Column width.
*/
private static function fit_word_size( GDRenderer $r, string $text, int $size, string $font, int $w ): int {
$words = preg_split( '/\s+/', trim( $text ) );
while ( $size > 12 ) {
$widest = 0;
foreach ( (array) $words as $word ) {
$widest = max( $widest, $r->measure_text_width( (string) $word, $size, $font ) );
}
if ( $widest <= $w ) {
break;
}
--$size;
}
return $size;
return TemplateHelpers::fit_text( $r, $text, $size, $font, $w, true, 12 );
}

/**
Expand Down Expand Up @@ -1994,19 +1983,6 @@ public static function plan_layout( int $height, int $margin, int $content_heigh
* @return string|null Readable temp PNG path the caller deletes, or null.
*/
private static function qr_image_path( string $url, int $size ): ?string {
if ( '' === $url ) {
return null;
}
/**
* Resolve a QR code image for a poster.
*
* @param string|null $path Null by default (no QR provider).
* @param string $url URL to encode.
* @param int $size Drawn size in pixels.
* @return string|null Path to a temp PNG the template owns and deletes.
*/
// phpcs:ignore WordPress.NamingConventions.ValidHookName -- Intentional slash-separated hook namespace.
$path = apply_filters( 'datamachine/image_generation/qr_image_path', null, $url, $size );
return ( is_string( $path ) && '' !== $path && is_readable( $path ) ) ? $path : null;
return TemplateHelpers::qr_image_path( $url, $size );
}
}
Loading
Loading