Commit 2 (final scope) of the in-plugin editorial image pipeline (ARCHITECTURE.md #21). A deterministic local composer now runs between provider bytes and sideload: Image A yields a corner-marked featured (1200x632) + branded OG (1200x630, teal band, logo, baked caption in Poppins SemiBold) + square card (1080x1080, cream panel, Poppins Bold, teal footer with the reverse lockup); Image B gets the corner mark only. - Capability ladder: imagick (full compositing) -> wp_get_image_editor resize-only (unbranded, WARNING once) -> pass-through (WARNING once); probe cached in prautoblogger_image_compose_capability, fingerprinted by PHP version + imagick/gd presence so host flips auto-invalidate. compose() never throws; composition can never fail a publish. - Variants sideloaded with a role filename suffix; attachment meta _prautoblogger_image_role + _prautoblogger_base_attachment_id; post meta _prautoblogger_og_image_id (frozen for the rebuilt SEO stage) + _prautoblogger_square_image_id. No cost-tracker rows ($0 local stage; duration logged on the image_composer channel). - New Images settings: compose_enabled ('1'), compose_variants ('og,square'), featured_mark_enabled ('1') — read at point of use. Geometry ships as Layout defaults behind the prautoblogger_image_compose_layout filter. - Vendored brand rasters (assets/brand) + bundled OFL Poppins faces (assets/fonts) for per-environment byte-stable output (stripImage + PNG date/time chunks excluded). Validated on Hostinger PHP 8.3.30 / ImageMagick 7.1.1-43: php -l clean on all touched files, double render byte-identical (sha256), exact variant geometry. - Fix: featured/Image B alt text is the editorial caption (was the model name); model still persists in _prautoblogger_image_model. - Image_Attacher extracted from Image_Pipeline (300-line compliance); sideloader gains an optional filename suffix param (back-compat ''). - Version 0.16.0 -> 0.17.0 + CHANGELOG + ARCHITECTURE.md #21 + CONVENTIONS.md composer guide in this commit. Unit tests: layout clamp boundaries, capability ladder, cache invalidation, attacher meta-linking, pipeline integration, imagick determinism (gated). Agent-Session: engineer-prautoblogger-image-composer-20260611
105 lines
4.3 KiB
PHP
105 lines
4.3 KiB
PHP
<?php
|
||
/**
|
||
* Tests for PRAutoBlogger_Image_Composer_Imagick (real renders).
|
||
*
|
||
* These run only where ext-imagick is loaded (skipped on the standard CI
|
||
* matrix); the same renders are exercised by the standalone harness that
|
||
* produced the PR samples. Asserts the determinism contract — identical
|
||
* inputs yield byte-identical output in the same environment — plus the
|
||
* exact variant geometry.
|
||
*
|
||
* @package PRAutoBlogger\Tests\Core
|
||
*/
|
||
|
||
namespace PRAutoBlogger\Tests\Core;
|
||
|
||
use PRAutoBlogger\Tests\BaseTestCase;
|
||
|
||
class ImageComposerImagickTest extends BaseTestCase {
|
||
|
||
protected function setUp(): void {
|
||
parent::setUp();
|
||
if ( ! extension_loaded( 'imagick' ) ) {
|
||
$this->markTestSkipped( 'ext-imagick not available — renderer covered by the server-side harness.' );
|
||
}
|
||
}
|
||
|
||
/** Helper: renderer wired to the real bundled assets. */
|
||
private function renderer(): \PRAutoBlogger_Image_Composer_Imagick {
|
||
$root = dirname( __DIR__, 3 );
|
||
|
||
return new \PRAutoBlogger_Image_Composer_Imagick(
|
||
\PRAutoBlogger_Image_Composer_Layout::defaults(),
|
||
$root . '/assets/brand/',
|
||
$root . '/assets/fonts/'
|
||
);
|
||
}
|
||
|
||
/** Helper: deterministic 1200×632 fixture base image (gradient + disc). */
|
||
private function fixture_bytes(): string {
|
||
$image = new \Imagick();
|
||
$image->newPseudoImage( 1200, 632, 'gradient:#1B8A92-#FAFAF7' );
|
||
$draw = new \ImagickDraw();
|
||
$draw->setFillColor( new \ImagickPixel( '#FF8A3D' ) );
|
||
$draw->circle( 600, 316, 600, 470 );
|
||
$image->drawImage( $draw );
|
||
$image->setImageFormat( 'png' );
|
||
|
||
return $image->getImageBlob();
|
||
}
|
||
|
||
/**
|
||
* Determinism: rendering the same input twice yields byte-identical
|
||
* variants (metadata stripped, PNG date/time chunks excluded).
|
||
*/
|
||
public function test_same_input_renders_byte_identical_output(): void {
|
||
$bytes = $this->fixture_bytes();
|
||
$caption = 'Collagen fragments may aid joint repair';
|
||
|
||
$first = $this->renderer()->compose_og( $bytes, $caption );
|
||
$second = $this->renderer()->compose_og( $bytes, $caption );
|
||
$this->assertSame( hash( 'sha256', $first['bytes'] ), hash( 'sha256', $second['bytes'] ), 'OG render must be byte-stable' );
|
||
|
||
$first = $this->renderer()->compose_square( $bytes, $caption );
|
||
$second = $this->renderer()->compose_square( $bytes, $caption );
|
||
$this->assertSame( hash( 'sha256', $first['bytes'] ), hash( 'sha256', $second['bytes'] ), 'Square render must be byte-stable' );
|
||
|
||
$first = $this->renderer()->compose_featured( $bytes );
|
||
$second = $this->renderer()->compose_featured( $bytes );
|
||
$this->assertSame( hash( 'sha256', $first['bytes'] ), hash( 'sha256', $second['bytes'] ), 'Featured render must be byte-stable' );
|
||
}
|
||
|
||
/**
|
||
* Variant geometry matches the spec: featured keeps base dimensions,
|
||
* OG is exactly 1200×630, square exactly 1080×1080, all PNG.
|
||
*/
|
||
public function test_variant_dimensions_and_mime(): void {
|
||
$bytes = $this->fixture_bytes();
|
||
$caption = 'Collagen fragments may aid joint repair';
|
||
|
||
$featured = $this->renderer()->compose_featured( $bytes );
|
||
$this->assertSame( [ 1200, 632, 'image/png', 'featured' ], [ $featured['width'], $featured['height'], $featured['mime_type'], $featured['role'] ] );
|
||
|
||
$og = $this->renderer()->compose_og( $bytes, $caption );
|
||
$this->assertSame( [ 1200, 630, 'image/png', 'og' ], [ $og['width'], $og['height'], $og['mime_type'], $og['role'] ] );
|
||
|
||
$square = $this->renderer()->compose_square( $bytes, $caption );
|
||
$this->assertSame( [ 1080, 1080, 'image/png', 'square' ], [ $square['width'], $square['height'], $square['mime_type'], $square['role'] ] );
|
||
}
|
||
|
||
/**
|
||
* Output PNG bytes embed no date text chunks or tIME chunk, so a render
|
||
* is stable across wall-clock time, not just within one process. (We
|
||
* scan the raw bytes: Imagick synthesizes `date:*` image properties at
|
||
* read time even for chunk-free PNGs, so getImageProperties() can't
|
||
* distinguish embedded dates from synthesized ones.)
|
||
*/
|
||
public function test_output_contains_no_date_chunks(): void {
|
||
$og = $this->renderer()->compose_og( $this->fixture_bytes(), 'Short caption' );
|
||
|
||
$this->assertFalse( strpos( $og['bytes'], 'date:create' ), 'PNG must not embed a date:create text chunk' );
|
||
$this->assertFalse( strpos( $og['bytes'], 'date:modify' ), 'PNG must not embed a date:modify text chunk' );
|
||
$this->assertFalse( strpos( $og['bytes'], 'tIME' ), 'PNG must not embed a tIME chunk' );
|
||
}
|
||
}
|