- PHP 84.6%
- Go Template 6.7%
- JavaScript 6.1%
- CSS 2.6%
Replaces the legacy 'how it works' section (which referenced non-existent
index.html, zap.php, display.inc.php, init.php) with a comprehensive
'ZAP Framework Guide' documenting the full application architecture:
- Front-controller routing (?mode=X -> ZAPController -> ZAP{Mode})
- PSR-0 autoloader in conf/init.php
- Config singleton pattern
- Session management (verbatim keys)
- Plugin-based search engines (Search interface, Brave)
- Image processing pipeline (ImgSearch, ImgProcess, ImgIOTools, ImgTools)
- CLI bootstrap pattern (chdir + require_once, session_start skipped in CLI)
- Logging via Log class
Template rendering pipeline documented with:
- ob_start/include/extract/ob_get_clean pattern
- getZAPTemplate() helper
- Step-by-step guide for adding new templates
Interactive elements documented:
- Pattern 1: Form with client-side validation (index.tpl + js/main.js)
- Pattern 2: Polling image display (display.tpl + js/display.js, onload-driven
two-img swap with 5s cadence)
Includes end-to-end summary for creating new features.
|
||
|---|---|---|
| cli_scripts | ||
| conf | ||
| lib | ||
| resource | ||
| tpl | ||
| utils | ||
| www | ||
| .gitignore | ||
| changelog.txt | ||
| LICENSE | ||
| README.md | ||
ZapMachine
Revival of the old ZapMachine. A collage machine that merges random images layer by layer remaining about 50% of the original images
########################################################
ApFab - Zap Machine V0.9.0
######################################################## 🄯 2009 - 2026 Fabian de Boer, Vincent Bruijne www.apfab.com
Zap stands for 'Zapped Artificial Picture', a collage made from Internet images. A Zap translates a set of words or a text into an image which reflects the visual representation of the words on the Internet.
This web application, Zap Machine, needs a number of words as input. It then takes the most relevant image that search engine Yahoo comes up with. After having collected all images the Zap Machine creates a given number of different collages from those images.
installation
First you need to run a server with PHP. Please do not use a Windows machine for this purpose, while Windows suck. ApFab recommends Linux or Mac;)
ApFab Zap Machine needs some PHP extensions:
- JSON (default installed)
- CUrl (default installed)
- Imagick (Image Magick image processing Class)
You need to acquire the following API key and put it into conf/conf.php:
- Brave Search API Key - https://api.search.brave.com/
See conf/conf.php for the exact constant names to use.
Adjust permission (chmod 777) for Sessions folder, run index.php in the www/ directory, fill in the form and voila.
ZAP Framework Guide
This document explains the internal architecture of ZapMachine and provides step‑by‑step instructions for extending it with new handlers, templates, and interactive elements.
Architecture Overview
Front-Controller Routing (?mode=X → ZAPController → ZAP{Mode})
All HTTP requests enter through www/index.php. The front controller reads the mode query parameter and dispatches to a class whose name is formed as ZAP + the ucfirst'd mode value.
// www/index.php (simplified)
$mode = isset($_GET['mode']) ? $_GET['mode'] : 'home';
require_once '..' . DIRECTORY_SEPARATOR . 'conf' . DIRECTORY_SEPARATOR . 'init.php';
$config = Config::getInstance();
$config->register('engines', $search_engines);
$frontcontroller = new ZAPController($mode);
$output = $frontcontroller->fetch();
print($output);
ZAPController (in lib/ZAPController.class.php) constructs the class name from a prefix (ZAP) and the mode, verifies it exists via the autoloader, instantiates it, and calls getContent():
class ZAPController {
private $prefix = 'ZAP';
public function __construct($mode = 'home') {
$mode = trim(strip_tags($mode));
$this->mode = ucfirst($mode);
$clazz = $this->prefix . $this->mode;
if (!class_exists($clazz)) {
die('Wrong parameter');
}
$this->action = new $clazz();
}
public function fetch() {
return $this->action->getContent();
}
}
Registered modes and their handlers:
?mode= |
Class | Template | Purpose |
|---|---|---|---|
home (default) |
ZAPHome |
tpl/index.tpl |
Word-input form |
display |
ZAPDisplay |
tpl/display.tpl |
Collage-generation progress screen |
zap |
ZAPZap |
(raw image output) | Downloads images & processes collages, returns image blobs |
Autoloader
conf/init.php registers an spl_autoload_register callback that searches every directory on the include path for both {ClassName}.class.php and {ClassName}.interface.php:
spl_autoload_register(function ($class_name) {
$include_path_tokens = explode(PATH_SEPARATOR, get_include_path());
foreach ($include_path_tokens as $prefix) {
$path[0] = $prefix . DIRECTORY_SEPARATOR . $class_name . '.interface.php';
$path[1] = $prefix . DIRECTORY_SEPARATOR . $class_name . '.class.php';
foreach ($path as $thisPath) {
if (file_exists($thisPath)) {
require_once $thisPath;
return;
}
}
}
});
The include path is built from these directories (relative to the project root):
| Constant | Directory |
|---|---|
ZAP_RESOURCE_DIR |
resource/ — search-engine implementations |
ZAP_UTILS_DIR |
utils/ — utility classes (Session, ZAPCurl, Log) |
ZAP_SESSIONS_DIR |
sessions/ |
ZAP_LIB_DIR |
lib/ — core handlers and image processing |
ZAP_TPL_DIR |
tpl/ — templates |
ZAP_CONF_DIR |
conf/ — configuration |
This means any .class.php or .interface.php file placed in one of those directories is automatically found — no manual require_once calls needed for library classes.
Config Singleton
Configuration values that must be accessible from any class are managed through Config (conf/Config.class.php), a classic singleton:
$config = Config::getInstance();
$config->register('engines', $search_engines); // store
$engines = $config->retrieve('engines'); // retrieve — returns false if not found
The singleton is wired in www/index.php immediately after init.php is loaded.
Session Management
The Session class (utils/Session.class.php) wraps PHP's native sessions:
Session::start('ZAP'); // Begin session (sessions named 'ZAP')
Session::reset(); // Clear $_SESSION array
Session::deleteCookie(); // Remove client-side session cookie
Session::destroy(); // Reset + delete cookie + session_destroy()
Session data is stored verbatim in $_SESSION and uses these keys:
| Key | Type | Set by | Purpose |
|---|---|---|---|
words |
string[] |
ZAPDisplay |
Keyword list for image search |
folder |
string |
ZAPDisplay |
Absolute path to session folder |
collages |
int |
ZAPDisplay |
Number of collages to generate |
width |
int |
ZAPDisplay |
Collage width in pixels |
height |
int |
ZAPDisplay |
Collage height in pixels |
images |
string[] |
ZAPZap |
Queue of source image paths for current collage |
imgOriginal |
string[] |
ZAPZap |
Immutable copy of the original image list |
collageNr |
int |
ZAPZap |
Current collage sequence number |
CLI note: Session is only started automatically in web mode. See CLI Bootstrap below.
Plugin-Based Search Engines
Search engines implement the Search interface (resource/Search.interface.php):
interface Search {
public function setQuery($word);
public function setParam($key, $value);
public function getData();
}
Each engine class lives in resource/ and returns data in a universal array format:
$data['result'] = true; // Boolean: success / failure
$data['engine'] = __CLASS__; // Engine name, e.g. 'Brave'
$data['results'][0]['clickurl'] = '...'; // Image URL to download
$data['results'][0]['referer'] = '...'; // Source page URL
$data['results'][0]['thumb'] = '...'; // Thumbnail URL
Available engines are registered as an array of names in conf/conf.php and wired through the Config singleton at runtime. ImgSearch randomly selects an engine from the registered list each time it runs:
$config = Config::getInstance();
$engines = $config->retrieve('engines');
$this->engine = $engines[array_rand($engines)];
Current engine: Brave (resource/Brave.class.php) — uses the Brave Search API.
To add a new engine:
- Create
resource/YourEngine.class.phpimplementingSearch. - Add
'yourengine'to the$search_enginesarray inconf/conf.php.
Image Processing Pipeline
The image pipeline runs entirely server-side through three classes in lib/:
-
ImgSearch(lib/ImgSearch.class.php) — searches for an image URL via a registered engine, downloads the file, validates its MIME type (image/jpeg,image/gif,image/png), and saves it to the session folder. -
ImgProcess(lib/ImgProcess.class.php) — constructs collages using ImageMagick. Each collage starts withcreateBasicCanvas()(zoom-crops the first source image) then layers additional images viaaddToCanvas()(random transparency + composite). -
ImgIOTools(lib/ImgIOTools.class.php) — handles output:onScreen()streams an Imagick object as an HTTP image response;urlOnScreen()loads a file path first;saveImg()writes to disk. -
ImgTools(lib/ImgTools.class.php) — utility functions:zoomCrop(),transparantRandom(),randomRotate(),resize(),recolor(),convertGifsToPng().
CLI Bootstrap
CLI scripts (in cli_scripts/) bootstrap the application differently from web requests. Because init.php skips Session::start() when php_sapi_name() === 'cli', CLI scripts must handle sessions manually if needed.
Example — cli_scripts/test.php:
#!/usr/bin/env php
<?php
// Make sure relative paths in init.php resolve against this script's directory
chdir(__DIR__);
// Bootstrap the application (autoloader, constants, Config, but NOT session)
require_once __DIR__ . '/../conf/init.php';
// If you need sessions in CLI, call Session::start() manually:
// Session::start('ZAP');
// Now all classes, the autoloader, and Config singleton are available
$config = Config::getInstance();
$config->register('engines', $search_engines);
Key differences from the web entry point:
chdir(__DIR__)is required beforerequire_once 'init.php'becauseinit.phpusesrealpath('.')to defineZAP_APP_BASE_DIR.Session::start()is not called — theinit.phpconditionphp_sapi_name() !== 'cli'guards against it.- You must explicitly register engines with
Config::getInstance()->register('engines', …)becausewww/index.phpnormally does this.
Logging
The Log class (utils/Log.class.php) writes timestamped messages to a session folder. It is used by ImgSearch (controlled by the ZAP_LOG constant in conf/conf.php). Each log call appends to both log.txt (plain text) and log.xml (XML-friendly) in the session's output directory.
if (ZAP_LOG == true) {
$log = new Log($this->folder);
$log->addToLog('ok', 'Search: ', "downloaded to $imageName");
$log->addToLog('error', 'Search: ', 'bad search data received');
}
Template Rendering Pipeline
The template system is a minimal PHP-native pattern built around ob_start() / include / extract() / ob_get_clean(). There is no template engine — templates are plain PHP files that receive variables via the symbol table.
How Templates Work
Every ZAP{Mode} handler follows the same rendering pattern:
class ZAPHome {
private $template = 'index'; // corresponds to tpl/index.tpl
private $tpl = '';
public function __construct() {
$this->ver = ZAP_VERSION;
extract(get_object_vars($this)); // ← makes all private properties available as $variables
ob_start(); // ← start output buffer
include getZAPTemplate($this->template); // ← include the .tpl file
$this->tpl .= ob_get_clean(); // ← capture buffer content, clean buffer
}
public function getContent() {
return $this->tpl;
}
}
Step by step:
- Properties are set in the constructor (often from
$_POST,$_SESSION, or constants likeZAP_VERSION). extract(get_object_vars($this))promotes every private property to a local variable in the current symbol table. A property$this->template = 'index'becomes$template = 'index'.ob_start()begins capturing all output.include getZAPTemplate($this->template)includes the filetpl/{name}.tpl. The included file inherits the current symbol table, so it can use$ver,$folder,$words, etc. directly.ob_get_clean()retrieves the captured HTML and closes the buffer.getContent()returns the accumulated output string, whichZAPController::fetch()returns toindex.phpforprint().
The getZAPTemplate() Helper
function getZAPTemplate($tplname) {
$tplfile = $tplname . '.tpl';
$tplfilepath = ZAP_APP_BASE_DIR . DIRECTORY_SEPARATOR
. ZAP_TPL_DIR . DIRECTORY_SEPARATOR . $tplfile;
if (file_exists($tplfilepath)) {
return $tplfilepath;
} else {
return ERROR_PREFIX . 'No template';
}
}
All templates live in the tpl/ directory and have the .tpl extension. The helper resolves the absolute path so the include call works from any working directory.
Adding a New Template
To add a new page/view:
-
Create the
ZAP{Mode}handler inlib/. The class must be namedZAP{Mode}(case-sensitive, PascalCase after the prefix) and must implement agetContent()method that returns a string:<?php // lib/ZAPExample.class.php class ZAPExample { private $template = 'example'; private $tpl = ''; public function __construct() { $this->pageTitle = 'My Example Page'; extract(get_object_vars($this)); ob_start(); include getZAPTemplate($this->template); $this->tpl .= ob_get_clean(); } public function getContent() { return $this->tpl; } } -
Create the template at
tpl/example.tpl:<!DOCTYPE html> <html lang="en"> <head><title>Example — <?php echo $pageTitle; ?></title></head> <body> <h1><?php echo $pageTitle; ?></h1> <p>ZAP version <?php echo $ver; ?></p> </body> </html> -
Use the route: Navigate to
index.php?mode=example. The autoloader findsZAPExampleinlib/, the front controller instantiates it, andgetZAPTemplate()loadstpl/example.tpl.
No registration step is required — the front controller's naming convention (ZAP + ucfirst(mode)) and the autoloader handle discovery automatically.
Adding Interactive Elements
This section documents the two interactive-element patterns used in the application. Each pattern pairs a server-side template with a client-side JavaScript file.
Pattern 1: Form with Client-Side Validation (index.tpl + js/main.js)
Used by ZAPHome to collect word inputs, validate them in the browser, and POST to the display mode.
Server side (ZAPHome in lib/ZAPHome.class.php):
The handler passes two template variables into tpl/index.tpl:
$ver— the ZAP version string.$status— an optional status message from the query string (?stat=...).
The template renders a <form id="form1"> that POSTs to index.php?mode=display. Each input is named word01, word02, … word{MAX_WORDS} (zeropadded to two digits). A <select name="collages"> controls the number of collages, and width/height text inputs control dimensions.
Client side (js/main.js):
The form1Submit() function performs validation before the form is submitted:
function form1Submit() {
// Minimum of two words required
var word1 = document.getElementById('word01').value;
var word2 = document.getElementById('word02').value;
var width = document.getElementById('width').value;
var height = document.getElementById('height').value;
if (word1.length <= 0) {
document.getElementById('status').innerHTML = 'Minimum of two words required';
return;
}
if (word2.length <= 0) {
document.getElementById('status').innerHTML = 'Minimum of two words required';
return;
}
if (width.length <= 0) {
document.getElementById('status').innerHTML = 'Value <b>width</b> required';
return;
}
if (height.length <= 0) {
document.getElementById('status').innerHTML = 'Value <b>height</b> required';
return;
}
if (!IsNumeric(width)) {
document.getElementById('status').innerHTML = 'Value <b>width</b> needs numeric input';
return;
}
if (!IsNumeric(height)) {
document.getElementById('status').innerHTML = 'Value <b>height</b> needs numeric input';
return;
}
// Hide form, show loading indicator, then submit
document.getElementById('status').innerHTML = ' ';
document.getElementById('loading').style.visibility = 'visible';
document.getElementById('form1').style.visibility = 'hidden';
document.getElementById('form1').submit();
}
To reproduce this pattern in a new handler:
- Create a form in your
.tplthat POSTs toindex.php?mode=yourmode. - Create a
ZAPYourModehandler inlib/that reads$_POSTvalues in its constructor and stores them in$_SESSION. - Write a
js/your.jsfile with a validation function, and include<script src="js/your.js"></script>in the template's<head>. - Use a
<div id="status">element in the template to display validation errors.
Pattern 2: Polling Image Display (display.tpl + js/display.js)
Used by the display and zap modes to show collage progress. The server streams individual image responses, and the client alternates between two <img> tags to give the appearance of continuous updating.
Server side:
ZAPDisplay (in lib/ZAPDisplay.class.php) sets session variables from $_POST and renders tpl/display.tpl. The template declares onload="go(this,['img1','img2'],5000);" on the <body> tag, which kicks off the polling loop.
ZAPZap (in lib/ZAPZap.class.php) handles each individual poll request. On each invocation it either:
- Downloads the next source image (if
$_SESSION['words']is not empty) and streams it viaImgIOTools::onScreen(). - Starts the next collage (if
$_SESSION['words']is empty but$_SESSION['collages'] > 0). - Adds the next source image to the current collage via
ImgProcess::addToCanvas(). - Signals completion by destroying the session and returning an error string (which triggers the
onerrorhandler in JavaScript).
Client side (js/display.js):
var go = function(b, img_id, interval) {
var i = 0;
var img0 = document.getElementById(img_id[0]);
var img1 = document.getElementById(img_id[1]);
var img = [img0, img1];
var activeImage = 1; // index of visible image
var url = 'index.php?mode=zap';
if (isNaN(interval)) {
interval = 5000;
}
// Trigger the next image fetch after the current one has loaded.
img[0].onload = img[1].onload = function() {
var inactiveImage = activeImage === 0 ? 1 : 0;
img[activeImage].style.display = 'none';
img[inactiveImage].style.display = '';
activeImage = inactiveImage;
setTimeout(function() {
img[inactiveImage].src = url + '&i=' + (i++);
}, interval);
};
// When the backend signals completion it returns a small HTML page.
// Show the ready link and stop fetching new images.
img[0].onerror = img[1].onerror = function() {
document.getElementById('ready').innerHTML = '<a href="/">Ready</a>';
};
// Kick off the first request.
img[activeImage].src = url + '&i=' + (i++);
};
How the polling cadence works:
body onload="go(this,['img1','img2'],5000)"starts the loop with a 5 s interval between successive fetches.- Two
<img>tags (#img1and#img2) are used — one is visible, the other hidden. - When the visible image finishes loading (
img.onload), the pair swaps: the loaded image is hidden, and the previously hidden one is shown. After a 5 s delay (setTimeout), the newly hidden image'ssrcis set toindex.php?mode=zap&i={counter}. - Each request to
mode=zapreturns raw image data (aContent-Type: image/jpegresponse). The browser renders it into the<img>tag, triggeringonloadagain and continuing the cycle. - When
ZAPZapfinishes all collages, it destroys the session and returns an HTML error string instead of image data. The<img>tag fails to load an image, triggeringonerror, which replaces the image area with a "Ready" link.
To reproduce this pattern in a new handler:
- Create a template with two
<img>elements and<body onload="go(this,['id1','id2'],intervalMs)">. - Include
js/display.js(or copy the pattern into your own JS file). - Create a
ZAP{Mode}handler that returns raw image data (setContent-Typeheaders and useechowithreadfile()or Imagick'sgetImageBlob()). - Signal completion by returning a non-image response that will trigger
onerroron the<img>tag.
Summary: Creating a New Feature End-to-End
To add a completely new feature to ZapMachine, follow these steps:
- Add configuration in
conf/conf.php(constants, engine names) if needed. - Create the handler
lib/ZAP{Mode}.class.phpwithgetContent()returning a string. - Create the template
tpl/{mode}.tplusing theob_start/include/extract/ob_get_cleanpattern. - Choose an interactive pattern:
- For form input with validation: add a form in the template, write a JS validation function, and include the script.
- For polling/image updates: use the two-
<img>swap withonload/onerror.
- Create a JS file in
www/js/and reference it from the template's<head>. - Test the route at
index.php?mode={ModeName}. - Test from CLI using the bootstrap pattern in
cli_scripts/test.php(rememberchdir(__DIR__)+ manualSession::start()if needed).