Case Study

Product Image Rendering Platform

Full-stack rendering service generating customisable print-ready images with pixel-perfect consistency between web preview and production output.

2,845
Lines of PHP
120+
Commits
4
API Endpoints
1.5-4.5s
Render Time

The Challenge

A print-on-demand e-commerce platform needed customers to preview customised product designs with personalised text and image overlays in real-time, then produce production-quality print images at precise resolutions for manufacturing.

Show why pixel-perfect consistency matters

The customer experience problem: Users expect their browser preview to exactly match the final printed product. Any deviation creates returns, complaints, and lost trust.

The technical challenge: Server-side image libraries (GD, ImageMagick) render text differently than browsers. Font metrics, kerning, line breaking, and canvas effects all produce subtly different results.

The business impact: Even 2-3px differences in text positioning can cause misaligned designs on physical products. For customisable merchandise, this gap is unacceptable.

  • Complex text layout and custom web font rendering requiring browser-native capabilities
  • Maintaining pixel-perfect consistency between browser preview and production print output
  • Generating exact dimensions for manufacturing whilst providing responsive web previews
  • Managing cross-language process communication between PHP orchestration and Node.js rendering
  • Ensuring API documentation stays synchronised with implementation without manual maintenance overhead
Show how server-side rendering falls short

GD/ImageMagick limitations:

  • Primitive text layout algorithms (no browser-quality line breaking)
  • Inconsistent font rendering across operating systems
  • No web font loading support (Google Fonts, custom typefaces)
  • Limited canvas effects (shadows, gradients, transformations)
  • No CSS-like styling capabilities

Browser rendering solves all of these: Real browser engines render text with production-quality algorithms, load web fonts via standard APIs, and support full HTML5 canvas capabilities.

The gap between preview and production was unacceptable for customer experience. We chose browser rendering to guarantee pixel-perfect consistency.

Technical requirement for pixel-perfect consistency

The Solution

The platform employs a hybrid architecture where PHP orchestrates business logic whilst Node.js handles browser-based rendering. This separation acknowledges that complex text layout, web font rendering, and canvas graphics are best handled by actual browser technology.

Show how the hybrid architecture works

Request flow:

API Request → Symfony Validation → URL Generation → Puppeteer Subprocess → Canvas Extraction → ImageMagick Post-Processing → Response

  • PHP Layer: Symfony handles HTTP, validation, business logic, database persistence, and subprocess orchestration
  • Node.js Layer: Puppeteer launches headless Chrome, navigates to PHP-generated template URL, waits for fonts, captures canvas
  • React Layer: Konva.js renders design layers on HTML5 canvas with precise positioning and styling
  • Post-Processing: ImageMagick crops print images to exact specifications after browser render

Why this works: Each layer handles what it does best. PHP orchestrates, browsers render, ImageMagick crops. No compromises.

  • Hybrid architecture using Puppeteer headless browser for pixel-perfect rendering
  • Dual-output pipeline generating both web preview and print images from single render
  • Domain-driven design with embedded value objects for type-safe position and resolution handling
  • OpenAPI documentation generated from PHP attributes to prevent documentation drift
  • Font loading synchronisation via Document Fonts API for consistent typeface rendering
Zero
Documentation Drift

OpenAPI documentation generated from PHP attributes keeps the API specification in sync with implementation.

Show why dual-output from single render

The requirement: E-commerce platforms need responsive web previews (full canvas with background), whilst print production requires exact dimensional specifications (cropped area, transparent background).

Alternative approaches considered:

  • Render on demand: Generate only what's requested - rejected due to state consistency concerns (preview and print could render different states)
  • Single image, crop client-side: Send full canvas, let client crop - rejected due to file size and precision requirements

Chosen solution: Both images render from identical state at the same moment. Consistency guarantee matters more than theoretical efficiency. Total render time: 3-9 seconds for print + preview pair.

Need a Custom Rendering Solution?

We build production-grade image processing pipelines for complex requirements.

Technical Implementation

The rendering pipeline uses Symfony's Process component to spawn Node.js subprocesses, passing configuration via JSON serialisation. Puppeteer navigates to a PHP-hosted template URL, waits for fonts to load, captures the canvas, and writes output to the filesystem. This hybrid architecture solves three critical problems: font consistency, preview-production parity, and precise dimensional output.

The most technically complex aspect is font synchronisation. Custom web fonts load asynchronously in browsers, and capturing the canvas before they're ready produces incorrect output with fallback fonts.

This synchronisation point solved the consistency problem completely. Every render now waits for fonts before capture, guaranteeing identical typeface rendering between preview and production. The operational overhead of running headless Chrome is worth it for this consistency guarantee.

Beyond rendering consistency, the platform needed self-documenting API endpoints for third-party e-commerce integration. Traditional API documentation maintained separately from code inevitably drifts, creating integration bugs.

This attributes-based approach eliminated an entire class of integration bugs. The platform exposes four focused endpoints (design fetch, options list, render, validate) with Swagger UI at /api/doc. Documentation drift became structurally impossible.

Show how cross-language IPC works

The challenge: PHP needs to orchestrate Node.js subprocesses, passing complex configuration and receiving binary image output.

Communication mechanism:

  • PHP → Node.js: Symfony serialises request objects to JSON, passes as command-line argument
  • Node.js processing: Puppeteer launches, renders, writes image to filesystem path specified in JSON
  • Node.js → PHP: Exit code signals success/failure, PHP reads image from filesystem

Why filesystem IPC: Both processes run on same server. Filesystem is fastest, simplest IPC mechanism. Adding Redis/RabbitMQ/S3 would introduce network latency for zero benefit.

The final piece of the rendering pipeline is post-processing. After browser capture, the platform applies different transformations to create two distinct outputs from a single render.

This dual-output approach maintains perfect consistency between preview and print whilst meeting their different requirements. Web users see contextual product mockups; print production receives exact dimensional specifications with transparent backgrounds.

Font loading synchronisation via Document Fonts API ensures Google Fonts and custom typefaces render correctly in the final output.

Critical for production-quality rendering

Show why browser rendering beats server-side libraries

The decision to use Puppeteer instead of pure PHP image libraries (GD, ImageMagick) wasn't obvious initially. Server-side rendering seemed simpler, faster, and less operationally complex.

What changed the decision:

  • Text layout algorithms in GD are primitive - no proper line breaking, kerning, or complex text shaping
  • Font rendering differs between browser and ImageMagick, creating subtle positioning differences
  • Web font loading not supported - Google Fonts and custom typefaces unavailable
  • Canvas effects limited - shadows, gradients, transforms all constrained

The realisation: Browsers spent decades perfecting text rendering. Why rebuild that from scratch? Use the production-quality implementation that already exists.

Results & Impact

40+
PHP Source Files

Service architecture implementing domain-driven design patterns with embedded value objects across 2,845 lines.

11
React Components

Frontend canvas rendering with Konva.js across 600+ lines of TypeScript in 11 TSX files.

6
Database Tables

PostgreSQL schema with Doctrine ORM: designs, layers, fonts, image options using embedded value objects.

The dual-output rendering pipeline generates both web-optimised preview images and production-quality print images from a single render request, maintaining pixel-perfect consistency between formats.

Show see the performance breakdown

Rendering pipeline timing:

  • Validation: 1-5ms (Symfony Validator constraints + business rules)
  • Puppeteer launch: 500-800ms (headless Chrome startup)
  • Page render: 500-2000ms (navigate + React render + Konva draw)
  • Font loading: 200-1000ms (Google Fonts + custom typefaces)
  • Canvas extraction: 100-300ms (toDataURL + base64 decode)
  • Image crop: 100-300ms (ImageMagick crop + write)

Single render total: 1.5-4.5 seconds

Dual-output total: 3-9 seconds (print + preview rendered sequentially)

Key Achievements

  • Perfect parity between browser preview and production output
  • Self-documenting API that stays in sync with implementation
  • Type-safe domain model preventing parameter confusion
  • Dual-resolution system serving both web and print requirements
  • Transparent background support for print-on-demand production
Show architecture scalability improvements possible

Current bottlenecks:

  • Sequential rendering: Print and preview renders execute one after another, doubling total time
  • New browser per render: Puppeteer launches fresh Chrome instance for each request
  • No render caching: Identical designs re-render from scratch

Optimisation opportunities:

  • Parallel rendering: Render print and preview simultaneously (halves total time to 1.5-4.5s)
  • Browser worker pool: Keep Chrome instances warm, reuse across requests (eliminates 500-800ms launch overhead)
  • Redis render cache: Cache output by design hash + input values (instant response for repeated requests)

These optimisations weren't needed for initial scale but provide clear upgrade path for higher throughput requirements.

Complex Integration Challenges?

We specialise in hybrid architectures that bridge multiple technologies.

Key Learnings

Browser-Based Rendering Worth The Complexity

For this project, server-side image libraries had limitations with complex text layout, web font rendering, and canvas graphics. Using Puppeteer to capture actual browser output achieved perfect preview/production parity. The operational overhead of running headless Chrome is worth it for the consistency benefits.

Show the full story: why we chose browser rendering

Initial assumption: Server-side image libraries (GD, ImageMagick) would be simpler, faster, and more straightforward. Headless Chrome seemed like unnecessary complexity.

What we discovered: The gap between browser rendering and server-side libraries is larger than expected. Font metrics differ subtly but consistently. Text positioning varies by 2-3 pixels. Canvas effects look different.

The turning point: After implementing both approaches in parallel, the consistency difference was undeniable. Browser rendering matched preview pixel-perfectly. Server-side rendering had subtle but consistent differences.

Lesson learned: For problems where browsers have invested decades of engineering effort (text layout, font rendering, canvas graphics), use the production-quality implementation that already exists. Don't rebuild it poorly in a different environment.

Value Objects Pay Dividends

Creating dedicated classes for Position and Resolution seemed like over-engineering initially. In practice, these value objects made method signatures self-documenting, prevented parameter confusion, and enabled IDE autocompletion. The implementation cost was small, but the benefit grew across the entire codebase.

Show the value objects transformation

Before value objects: Method signatures looked like createLayer(192, 361, 610, 110, 'title'). Which parameters are position? Which are dimensions? Is it x-y-width-height or width-height-x-y?

After value objects: Method signatures became createLayer(position, size, 'title') with explicit new PositionValue(192, 361). Parameter confusion became structurally impossible.

Unexpected benefits:

  • IDE autocompletion works perfectly (suggests x/y properties)
  • Type system catches mistakes at compile time
  • Database schema stays flat (embedded objects don't create joins)
  • DTOs become self-documenting (React components receive typed objects)

API Documentation That Cannot Drift

Using PHP attributes to define both routing and OpenAPI documentation eliminates a whole class of bugs. The documentation generates directly from the code that handles requests. Drift becomes structurally impossible. This approach takes discipline but removes manual documentation maintenance entirely.

Show how documentation drift became structurally impossible

Traditional API documentation flow:

  1. Write code
  2. Write separate Swagger/OpenAPI spec
  3. Keep them synchronised manually
  4. Watch them inevitably drift apart

Attributes-based approach: PHP 8 attributes define routing, validation, and documentation in a single location. NelmioApiDocBundle reads these attributes at runtime and generates Swagger UI.

Why this works: Documentation isn't a separate artefact. It's generated from the code that handles requests. Change a parameter? Docs update automatically. Modify validation? Swagger reflects it instantly. Drift becomes structurally impossible.

Real-world impact: Four RESTful endpoints (design fetch, options list, render, validate) with zero manually-written documentation. Swagger UI available at /api/doc always perfectly synchronised.

DDD Applies Beyond Business Domains

Domain-driven design patterns work just as well in a rendering domain as in traditional business domains. Concepts like canvas size, print resolution, and crop position are genuine domain entities. Treating them as value objects rather than primitives brought clarity to the entire system.

Show rendering as a domain model

Common misconception: Domain-driven design is for business logic (orders, customers, invoices). Technical systems don't need domain models.

Reality: Rendering has genuine domain concepts - canvas size, print resolution, crop position, layer composition. These aren't just technical details, they're domain entities with business rules.

What treating them as domain entities enables:

  • Value objects prevent parameter confusion (PositionValue vs ResolutionValue are distinct types)
  • Embedded objects keep database normalised whilst maintaining object composition
  • Factory patterns encapsulate complex construction logic
  • Type safety flows from database to React components

Lesson learned: Domain-driven patterns apply to any domain with complex concepts and rules. Don't reserve them only for "business logic".

Ready to eliminate your technical debt?

Transform unmaintainable legacy code into a clean, modern codebase that your team can confidently build upon.