Files
prautoblogger/tests/unit/Core/ImageComposerImagickTest.php
peptiderepo 28947a0caa feat: deterministic image composer — OG + square variants, branding, baked caption (v0.17.0)
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
2026-06-11 12:31:03 +07:00

105 lines
4.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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' );
}
}