0.9.0 - the ZapV version updated to current php standards. Brave search used, while Bing and Google search api are deprecated

This commit is contained in:
Fabian de Boer 2026-07-15 13:57:05 +02:00
commit 4ddaddda3d
31 changed files with 1778 additions and 1 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
sessions/*
ignore

View file

@ -1,3 +1,53 @@
# 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`:
1. **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.html, fill in the form and voila.
# how it works
- index.html contains user interface
- user input put into Session variables in init.php
- display.inc.php dynamically displays two img tags that are loaded in turn with image data from zap.php using display.js
- zap.php downloads images one by one using imgSearch Class
- zap.php processes collages one by one using imgProcess Class
- All downloaded images, collage images and logfiles(feature is far from ready yet) are put in a folder named 'Session/zap'.date()
- the machine is ready when an error is loaded into img document class. (a weak point in our construction)
# yet to come
- Logfile also generated in imgProcess Class (0.9.2)
- Number and camera filename (virgin images) input (0.9.3)
- Proper stop meganism (0.9.1)
- better error handling: dead links give dynamicly generated image thats allso used in collage (0.9.3)
- visualisation of logfile, so viewers can see what is happening (0.9.1)
- replacing all 'random' image processing parameters (0.7)

48
changelog.txt Executable file
View file

@ -0,0 +1,48 @@
ZAP is a refactorment of the Zap Machine V0.3.2 from ApFab
It's more like a framework now.
todo ZAP
- add .htaccess file
# ZAP
0.5.0
- updated the search engines removed Yahoo, added Bing
# ZAPV
0.1.0
- new public release of ZAP
0.0.9
- made logging dependent
- made adult searches config dependent
0.0.8
- refactored the ImgProcess class and removed code to two other classes
- added ImgIOTools and ImgTools. They contain static functions
0.0.7
- refactored the whole search engine part
- made an interface for future implementations of other search engines
0.0.6
- added a Config class
0.0.5
- split the search code to separate functions
- added ZAPCurl util class
0.0.4
- split the zap code to separate functions
0.0.3
- refactored all classes to more readable code
- created Session utils class
0.0.2
- created new index.php and front controller
- added conf.php and init.php
0.0.1
- reworked frontend to separate html and js

28
cli_scripts/reset.php Executable file
View file

@ -0,0 +1,28 @@
<?php
session_start();
unset($_SESSION['imgOriginal']);
unset($_SESSION['words']);
unset($_SESSION['folder']);
unset($_SESSION['collages']);
unset($_SESSION['width']);
unset($_SESSION['height']);
echo '<p>session reset</p><p>';
if ($_GET['allcollages'] == 'clear') {
$sessions = glob('./Sessions/*');
foreach($sessions as $folder){
$files = glob($folder.'/*');
foreach($files as $fileToDelete){
echo "$fileToDelete deleted<br />";
unlink($fileToDelete);
}
echo "$folder deleted<br />";
rmdir($folder);
}
}
echo '</p>';
session_destroy();
?>

192
cli_scripts/test.php Executable file
View file

@ -0,0 +1,192 @@
#!/usr/bin/env php
<?php
/**
* ZapMachine Test Script
*
* Usage:
* php test.php Run all tests
* php test.php --brave Test Brave search JSON output
* php test.php --search Test ImgSearch for a word and show JSON results
*/
// Make sure relative paths in init.php resolve against this script's directory
chdir(__DIR__);
// Bootstrap the application
require_once __DIR__ . '/../conf/init.php';
$passed = 0;
$failed = 0;
/**
* Assert a condition and print result
*/
function test($description, $condition) {
global $passed, $failed;
if ($condition) {
echo "$description\n";
$passed++;
} else {
echo "$description\n";
$failed++;
}
}
/**
* Run basic application tests
*/
function runBasicTests() {
echo "\n=== Basic Tests ===\n";
// Version test
test('ZAP_VERSION is defined', defined('ZAP_VERSION'));
test('ZAP_VERSION is 0.9.0', ZAP_VERSION === '0.9.0');
// Directory constants
test('ZAP_APP_BASE_DIR is defined', defined('ZAP_APP_BASE_DIR'));
test('ZAP_APP_BASE_DIR exists', is_dir(ZAP_APP_BASE_DIR));
test('ZAP_RESOURCE_DIR is defined', defined('ZAP_RESOURCE_DIR'));
test('ZAP_UTILS_DIR is defined', defined('ZAP_UTILS_DIR'));
// Resource directory
$resourceDir = ZAP_APP_BASE_DIR . DIRECTORY_SEPARATOR . ZAP_RESOURCE_DIR;
test('Resource directory exists', is_dir($resourceDir));
test('Search interface exists', file_exists($resourceDir . '/Search.interface.php'));
test('Brave class exists', file_exists($resourceDir . '/Brave.class.php'));
// Utils directory
$utilsDir = ZAP_APP_BASE_DIR . DIRECTORY_SEPARATOR . ZAP_UTILS_DIR;
test('Utils directory exists', is_dir($utilsDir));
test('ZAPCurl class exists', file_exists($utilsDir . '/ZAPCurl.class.php'));
// PHP extensions
test('ImageMagick PHP extension (imagick) is installed', extension_loaded('imagick'));
// Config
test('BRAVE_API_KEY is defined', defined('BRAVE_API_KEY'));
test('BRAVE_BASE is defined', defined('BRAVE_BASE'));
global $search_engines;
test('$search_engines is set', isset($search_engines));
test('$search_engines contains brave', isset($search_engines) && in_array('brave', $search_engines));
}
/**
* Test ImgSearch class for a given word
*/
function testImgSearch() {
echo "\n=== ImgSearch Test ===\n";
echo "Enter a search word: ";
$handle = fopen("php://stdin", "r");
$word = trim(fgets($handle));
fclose($handle);
if (empty($word)) {
echo " ⚠ No word entered. Aborting.\n";
return;
}
$folder = ZAP_APP_BASE_DIR . DIRECTORY_SEPARATOR . ZAP_SESSIONS_DIR . DIRECTORY_SEPARATOR . 'search_test_' . ZAP_MOMENT;
if (!is_dir($folder)) {
@mkdir($folder, 0777, true);
}
echo "\nQuery: '$word'\n\n";
// Register configured search engines so ImgSearch can pick one
global $search_engines;
$config = Config::getInstance();
$config->register('engines', $search_engines);
$search = new ImgSearch($folder, $word);
$data = $search->getImageData();
echo "Raw JSON output:\n";
echo json_encode($data, JSON_PRETTY_PRINT) . "\n\n";
test('Result is an array', is_array($data));
test('Has result key', isset($data['result']));
if (isset($data['result']) && $data['result'] === true) {
test('Search returned results', true);
test('Has results array', isset($data['results']));
if (isset($data['results'])) {
$count = count($data['results']);
echo " Results count: $count\n";
}
} else {
echo " ⚠ No results returned. Check your API key and configuration.\n";
}
}
/**
* Test Brave search JSON output
*/
function testBraveSearch() {
echo "\n=== Brave Search JSON Test ===\n";
echo "Query: 'test' | Results: 3\n\n";
$brave = new Brave();
$brave->setQuery('test');
$brave->setParam('size', 3);
$json = $brave->getData();
echo "Raw JSON output:\n";
echo json_encode($json, JSON_PRETTY_PRINT) . "\n\n";
// Validate structure
test('Result is an array', is_array($json));
test('Has result key', isset($json['result']));
if (isset($json['result']) && $json['result'] === true) {
test('Search returned results', true);
test('Has results array', isset($json['results']));
test('Engine is Brave', isset($json['engine']) && $json['engine'] === 'Brave');
if (isset($json['results'])) {
$count = count($json['results']);
test("Results count is 3 (got $count)", $count === 3);
// Check first result structure
if ($count > 0) {
$first = $json['results'][0];
test('First result has clickurl', isset($first['clickurl']));
test('First result has thumb', isset($first['thumb']));
test('First result has referer', isset($first['referer']));
}
}
} else {
echo "\n ⚠ No results returned. Check your API key and CX.\n";
}
}
// Main execution
if (in_array('--search', $argv)) {
testImgSearch();
} elseif (in_array('--brave', $argv)) {
testBraveSearch();
} else {
runBasicTests();
// Ask if user wants to run Brave test
if (defined('BRAVE_API_KEY') && BRAVE_API_KEY !== 'YOUR_BRAVE_API_KEY') {
echo "\nRun Brave search test? (y/n): ";
$handle = fopen("php://stdin", "r");
$line = fgets($handle);
if (trim(strtolower($line)) === 'y') {
testBraveSearch();
}
fclose($handle);
} else {
echo "\n ⚠ Skipping Brave test: API key not configured\n";
}
}
// Summary
echo "\n=== Results ===\n";
echo "Passed: $passed\n";
echo "Failed: $failed\n";
echo ($failed === 0 ? "All tests passed! ✓" : "Some tests failed.") . "\n\n";
exit($failed > 0 ? 1 : 0);
?>

35
conf/Config.class.php Executable file
View file

@ -0,0 +1,35 @@
<?php
/**
* Singleton configuration class
* Register configuration directives with this class
*/
class Config {
private static $instance = null;
private $config = array();
private function __construct() {
}
public static function getInstance() {
if (self::$instance === null) {
$clazz = __CLASS__;
self::$instance = new $clazz();
}
return self::$instance;
}
public function register($key, $value) {
$this->config[$key] = $value;
return;
}
public function retrieve($key) {
if (isset($this->config[$key])) {
return $this->config[$key];
} else {
return false;
}
}
}
?>

25
conf/conf.php Executable file
View file

@ -0,0 +1,25 @@
<?php
/**
* Local configuration directives here.
* Change as you like
*/
define('MAX_WORDS', 4);
define('MAX_ZAPS', 9);
define('ZAP_CURL_TIMEOUT', 12);
define('ZAP_IMG_TYPE', 'jpg');
define('ZAP_CANVAS_NAME', 'collage');
define('ZAP_DELETE_SOURCE', false);
define('ZAP_ADULT', false);
define('ZAP_LOG', true);
define('ZAP_DEBUG', true);
// Brave Search API
// Get your API key: https://api.search.brave.com/
define('BRAVE_API_KEY', 'your api key');
define('BRAVE_BASE', 'https://api.search.brave.com/res/v1/images/search');
$search_engines = array('brave');
?>

99
conf/init.php Executable file
View file

@ -0,0 +1,99 @@
<?php
include_once 'conf.php';
/**
* Define some app-wide constants
*/
define('ZAP_APP_BASE_DIR', realpath('.' . DIRECTORY_SEPARATOR . '..'));
define('ZAP_WEB_DIR', 'www');
define('ZAP_RESOURCE_DIR', 'resource');
define('ZAP_UTILS_DIR', 'utils');
define('ZAP_SESSIONS_DIR', 'sessions');
define('ZAP_LIB_DIR', 'lib');
define('ZAP_CONF_DIR', 'conf');
define('ZAP_TPL_DIR', 'tpl');
/**
* Error decoration
*/
define('ERROR_PREFIX', 'ZAP Error: ');
/**
* Set include path for application
*/
function setZAPIncludePath() {
$app_dirs = array(ZAP_RESOURCE_DIR, ZAP_UTILS_DIR, ZAP_SESSIONS_DIR,
ZAP_LIB_DIR, ZAP_TPL_DIR, ZAP_CONF_DIR);
$app_include_path = '';
foreach ($app_dirs as $app_dir) {
$app_include_path .= PATH_SEPARATOR . ZAP_APP_BASE_DIR . DIRECTORY_SEPARATOR . $app_dir;
}
if (set_include_path(get_include_path() . $app_include_path) == false) {
throw new Exception(ERROR_PREFIX . 'Error while creating include path...');
}
define('ZAP_INCLUDE_PATH_SET', true);
return;
}
/**
* Initiate autoload function for application
*/
spl_autoload_register(function ($class_name) {
if (!defined('ZAP_INCLUDE_PATH_SET') || ZAP_INCLUDE_PATH_SET === false) {
try {
setZAPIncludePath();
} catch (Exception $e) {
echo $e->getMessage();
die();
}
}
$include_path = get_include_path();
$include_path_tokens = explode(':', $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;
}
}
}
});
/**
* Define remaining app-wide constants
*/
define('ZAP_MOMENT', date("Y-m-d-H_i_s"));
define('ZAP_VERSION', '0.9.0');
define('ZAP_SESSION_NAME', 'ZAP');
// Only start session in web mode (not CLI)
if (php_sapi_name() !== 'cli') {
Session::start(ZAP_SESSION_NAME);
}
/**
* returns string of path of template file
* @return string
*/
function getZAPTemplate($tplname) {
if (!defined('ZAP_APP_BASE_DIR')) {
throw new Exception(ERROR_PREFIX . 'No base directory defined?!');
}
$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';
}
}
?>

50
lib/ImgIOTools.class.php Executable file
View file

@ -0,0 +1,50 @@
<?php
/**
* Image Input Output Tools
*/
class ImgIOTools {
public static $maxScreenWidth = 1067; //screen dimensions
public static $maxScreenHeight = 600;
/**
* Flushes image to screen
* @param Imagick $im
* @return void
*/
public static function onScreen(Imagick $img) {
$img->scaleImage(self::$maxScreenWidth, self::$maxScreenHeight, true);
//$img = self::scaleByLength($img, $this->maxScreenWidth, $this->maxScreenHeight);
header('Content-type: image/' . ZAP_IMG_TYPE);
echo $img->getImageBlob();
}
/**
* Puts image from $imgUrl (if exists) onscreen
* @param string $imgUrl
* @return boolean or imagedata
*/
public static function urlOnScreen($imgUrl) {
if (file_exists($imgUrl)) {
$img = new Imagick($imgUrl);
self::onScreen($img);
} else {
return false;
}
}
/**
* Saves active image from $im as $filename.$filetype in $folder
* @param Imagick $im
* @param string $folder
* @param string $filename
* @param string $filetype
* @return boolean
*/
public static function saveImg(Imagick $img, $folder, $filename) {
$path = $folder . $filename . '.' . ZAP_IMG_TYPE;
return $img->writeImage($path);
}
}
?>

92
lib/ImgProcess.class.php Executable file
View file

@ -0,0 +1,92 @@
<?php
/**
* ImgProcess
* Actual image manipulation
*/
class ImgProcess {
private $folder; //folder for reading source images and write
private $canvasName;
private $zapWidth;//collage dimensions
private $zapHeight;
private $maxRGB; //depends on colorDepth. 8 bits: 255; 16 bits : 65536;
/**
* constructor
* @param $imgFolder string
* @param $width int
* @param $height int
* @param $nr int
*/
public function __construct($imgFolder, $width, $height, $nr) {
$this->folder = $imgFolder;
$this->zapWidth = $width;
$this->zapHeight = $height;
$this->canvasName = DIRECTORY_SEPARATOR . ZAP_CANVAS_NAME;
$this->canvasName .= $_SESSION['date'] . "_nr_" . sprintf("%02d", $nr);
$tmpImg = new Imagick();
$tmpAr = $tmpImg->getQuantumRange();
$this->maxRGB = $tmpAr['quantumRangeLong'];
$tmpImg->destroy();
}
/**
* Makes a basic canvasFile from sourceImage
* and saves it into imageFolder
* returns imageBLOB from saved image
* @param string $sourceImgName
* @return void
*/
public function createBasicCanvas($sourceImgName) {
$canvas = new Imagick($sourceImgName);
$canvas = ImgTools::zoomCrop($canvas, $this->zapWidth, $this->zapHeight, TRUE);
ImgIOTools::saveImg($canvas, $this->folder, $this->canvasName);
if (ZAP_DELETE_SOURCE == true) {
unlink($sourceImgName);
}
ImgIOTools::onScreen($canvas);
}
/**
* adds processed version of $sourceImage to canvas
* @param string $sourceImgName
* @return void
*/
public function addToCanvas($sourceImgName) {
$canvasUri = $this->folder . $this->canvasName . '.' . ZAP_IMG_TYPE;
$canvas = new Imagick($canvasUri);
$source_im = new Imagick($sourceImgName);
$newCanvas_im = $this->zapToCanvas($source_im, $canvas);
ImgIOTools::saveImg($newCanvas_im, $this->folder, $this->canvasName);
if (ZAP_DELETE_SOURCE == true) {
unlink($sourceImgName);
}
ImgIOTools::onScreen($newCanvas_im);
}
/**
* @param Imagick $source_im
* @param Imagick $canvas
* @return Imagick Object
*/
private function zapToCanvas(Imagick $source_im, $canvas) {
//$val = ImgTools::imgCenterValueProm($source_im, $this->maxRGB);
//merge resized random transparised image on top of canvas
$transparent = ImgTools::transparantRandom($source_im, $this->maxRGB);
$resized = ImgTools::resize($transparent, $this->zapWidth, $this->zapHeight);
$canvas->compositeImage($resized, imagick::COMPOSITE_OVER, 0, 0);
return $canvas;
}
}
?>

145
lib/ImgSearch.class.php Executable file
View file

@ -0,0 +1,145 @@
<?php
/**
* ZAP image search class
*/
class ImgSearch {
private $hitPosition;
private $numberResults = 16;
private $folder;
private $word;
private $engine;
private $allowedFileTypes = array('image/jpeg','image/gif','image/png');
private $log;
/**
* Constructor
* @param string $imgFolder path to save images to
* @param string $word keyword to search for
*/
public function __construct($imgFolder, $word) {
$this->folder = $imgFolder;
$this->word = $word;
$this->hitPosition = (int) rand(0,10);
if (ZAP_LOG == true) {
$this->log = new Log($this->folder);
}
$config = Config::getInstance();
$engines = $config->retrieve('engines');
$this->engine = $engines[array_rand($engines)];
}
/**
* download image that corresponds to $keyWord
* returns filename of image if downloaded succeeded, or false if failed
* @return string $localUrl / boolean
*/
public function getImage() {
$imgData = $this->getImageData();
if ($imgData == false) {
$this->addLog('error','Search: ', 'bad search data received' );
return false;
}
$this->addLog('ok', 'Engine: ', $imgData['engine']);
$localUrl = $this->imageToTmp($imgData['results'], $this->folder);
return $localUrl;
}
/**
* Returns an array that contains searchdata for $word
* @return array $searchData or false
*/
public function getImageData() {
$clazz = ucfirst($this->engine);
$search = new $clazz();
$search->setQuery(urlencode($this->word));
$search->setParam('start', $this->hitPosition);
$search->setParam('size', $this->numberResults);
$searchData = $search->getData();
return $searchData['result'] == false ? false : $searchData;
}
/**
* Saves image or returns false
* @param array $results
* @param string $tmpDir
* @param integer $hit
* @return mixed
*/
private function imageToTmp($results, $tmpDir, $hit = 0) {
$first = $results[$hit]['clickurl'];
$imageName = $tmpDir . DIRECTORY_SEPARATOR . $this->filename($first);
$myCurl = new ZAPCurl($first, true);
$imageData = $myCurl->fetchCurlData();
if($imageData == false) {
$this->addLog('error', 'Search: ', 'received empty file' . PHP_EOL);
return false;
}
if(file_put_contents($imageName, $imageData)) {
if($this->imageTypeOk($imageName)) {
$this->addLog('ok', 'Search: ', "downloaded to $imageName");
$this->addLog('ok', 'Search: ', 'source: ' . $results[$hit]['referer']);
$this->addLog('ok', 'Search: ', 'thumb: ' . $results[$hit]['thumb'] . PHP_EOL);
return $imageName;
} else {
$this->addLog('error', 'Search: ', 'wrong file type: image removed');
unlink($imageName);
$nextHit = $hit + 1;
return $this->imageToTmp($results, $tmpDir, $nextHit);
}
} else {
$this->addLog('error', 'Search: ', 'failed saving image');
return false;
}
}
/**
* Check mime type of image
* @param string $fileUri
* @return boolean
*/
private function imageTypeOk($fileUri) {
$mimeType = mime_content_type($fileUri);
return in_array($mimeType, $this->allowedFileTypes);
}
/**
* Gets filename from a url
* @param string $urlString
* @return string
*/
private function filename($urlString) {
$ar = explode('/', $urlString);
$filename = array_pop($ar);
// Strip query string so the saved file has a real extension
$filename = explode('?', $filename)[0];
return $filename;
}
/**
* Log messages
*/
private function addLog($type, $head, $message) {
if (ZAP_LOG == true) {
$this->log->addToLog($type, $head, $message);
}
return;
}
}
?>

180
lib/ImgTools.class.php Executable file
View file

@ -0,0 +1,180 @@
<?php
/**
* Library of tool functions to manipulate an image
*/
class ImgTools {
public static $maxScreenWidth = 1067; //screen dimensions
public static $maxScreenHeight = 600;
public static $minFuzzFactor = 0.08; //minimal factor for $fuzz
public static $maxFuzzFactor = 0.64; //maximal factor for $fuzz
/**
* Returns $im with random transparent color
* @param Imagick $im
* @param int $maxRGB
* @return Imagick $im
*/
public static function transparantRandom(Imagick $img, $maxRGB) {
//compute random fuzzyness
$minFuzz = intval(self::$minFuzzFactor * $maxRGB);
$maxFuzz = intval(self::$maxFuzzFactor * $maxRGB);
$randFuzz = mt_rand ($minFuzz,$maxFuzz);
//take average RGB value of random pixel
$xPoint = mt_rand(0,$img->getImageWidth());
$yPoint = mt_rand(0,$img->getImageHeight());
$randomPixel = $img->getImagePixelColor($xPoint, $yPoint);
$randomPixelColor = $randomPixel->getColorAsString();
//make transparency
$img->transparentPaintImage($randomPixelColor, 0.0, $randFuzz, false);
return $img;
}
/**
* random Rotate image
* @param Imagick $im
* @return Imagick $im
*/
public static function randomRotate(Imagick $img) {
$transp = new ImagickPixel('#ffffff');
$ar = array(0,90,180,270);
$img->rotateImage($transp, $ar[mt_rand(0,3)]);
return $img;
}
/**
* Resize image
* @param Imagick $im
* @param integer $retWidth
* @param integer $retHeight
* @return Imagick $im
*/
public static function resize(Imagick $img, $retWidth, $retHeight) {
$img->resizeImage((int)$retWidth, (int)$retHeight, imagick::FILTER_POINT, 0);
return $img;
}
/**
* Recolor image
* @param Imagick $im
* @return Imagick $im
*/
public static function recolor(Imagick $img){
$recolorValue = mt_rand(4, 250);
$times = mt_rand(1, 4);
for($i = 0; $i <= $times; $i++) {
$img->cycleColormapImage($recolorValue);
}
return $img;
}
/**
* returns MagickWand with randomly zoomed part of image $im with size $retWidth * $retHeight
* @param Imagick $im
* @param integer $retWidth
* @param integer $retHeight
* @param boolean $rotate
*/
public static function zoomCrop(Imagick $img, $retWidth, $retHeight, $rotate = false) {
if ($rotate == true) {
$img = self::randomRotate($img);
}
$fact = mt_rand(10, 40) / 100; //zoomfactor 10 - 40%
$width = $img->getImageWidth();
$height = $img->getImageHeight();
$newWidth = (int)($width * $fact);
$newHeight = (int)($height * $fact);
//crop $im
//x and y offset random 0 - width (or height) of blown up image minus crop area (of course!)
$img->cropImage($newWidth, $newHeight,
mt_rand(0, (int)($width - $newWidth)),
mt_rand(0, (int)($height - $newHeight)));
$img->resizeImage((int)$retWidth, (int)$retHeight, imagick::FILTER_POINT, 0);
return $img;
}
/**
* converts all gif images in array to png images
* returns Array with converted filenames
* @param array $imgAr
* @return array $newArray
*/
public static function convertGifsToPng($imgAr) {
$newArray = array();
while ($filename = array_shift($imgAr)) {
$imgInfo = getimagesize($filename);
$mimeType = $imgInfo['mime'];
//find gif
if ($mimeType == "image/gif") {
$imagetype = 'png';
//full image name
$Dot = '.';
$newFilename = "$filename$Dot$imagetype";
//convert
$img = new Imagick($filename);
if($img->writeImage($newFilename)) {
//Remove original and rename new to save space
unlink($filename);
array_push($newArray, $newFilename);
}
} else {
array_push($newArray, $filename);
}
}
return $newArray;
}
/**
* LEGACY?
* INT (0 - 1000) -> promilage of average RGB value of center pixel of $im related to $maxRGB
* @param Imagick $im
* @param int $maxRGB
* @return int
*/
public static function imgCenterValueProm(Imagick $img, $maxRGB) {
$centerX = $img->getImageWidth() / 2;
$centerY = $img->getImageHeight() / 2;
//take average of the RGB of most middle pixel of each image and put it into an array
$centerPixel = $img->getImagePixelColor($centerX,$centerY);
$colors = $centerPixel->getColor();
$averageColor = round( ($colors['r'] + $colors['g'] + $colors['b']) / 3);
return round(($averageColor / $maxRGB) * 1000);
}
/**
* LEGACY?
* returns magickWand scaled proportionaly to $maxW or $maxH
* @param Imagick $im
* @param integer $maxW
* @param integer $maxH
* @return Imagick $im
*/
public static function scaleByLength(Imagick $img, $maxW, $maxH) {
$width = $img->getImageWidth();
$height = $img->getImageHeight();
if(($width <= self::$maxScreenWidth) && ($height <= self::$maxScreenHeight)) {
return $img;
} else {
if ($width >= $height) { //horizontal image
$new_x = $maxW;
$new_y = round(($new_x / $width) * $height, 0);
} else { //vertical image
$new_y = $maxH;
$new_x = round(($new_y / $height) * $width, 0);
}
return self::resize($img, $new_x, $new_y);
}
}
}
?>

41
lib/ZAPController.class.php Executable file
View file

@ -0,0 +1,41 @@
<?php
/**
* Frontend handler
*/
class ZAPController {
/**
* String mode
*/
private $mode;
/**
* Parsable content object
*/
private $action;
/**
* Class prefix
*/
private $prefix = 'ZAP';
/**
* Constructor
*/
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();
}
/**
* @return string content
*/
public function fetch() {
return $this->action->getContent();
}
}
?>

91
lib/ZAPDisplay.class.php Executable file
View file

@ -0,0 +1,91 @@
<?php
/**
* Class is responsible for starting up ZAP machine
*/
class ZAPDisplay {
/**
* Template file
*/
private $template = 'display';
/**
* Folder string
*/
private $folder = '';
/**
* Words array
*/
private $words = array();
/**
* Content string
*/
private $tpl = '';
/**
* Constructor
*/
public function __construct() {
ob_start();
$this->folder = $this->createFolder();
$this->words = $this->handleWords();
$_SESSION['words'] = $this->words;
$_SESSION['folder'] = $this->folder;
$_SESSION['collages'] = (int) $_POST['collages']; //number of zaps
$_SESSION['width'] = (int) $_POST['width'];
$_SESSION['height'] = (int) $_POST['height'];
$_SESSION['date'] = ZAP_MOMENT;
$ver = ZAP_VERSION;
include getZAPTemplate($this->template);
$this->tpl .= ob_get_clean();
}
/**
* Create folder and return path
* @return string $folder path or throws exception
*/
private function createFolder() {
try {
$folder = ZAP_APP_BASE_DIR . DIRECTORY_SEPARATOR
. ZAP_SESSIONS_DIR . DIRECTORY_SEPARATOR . 'zap_' . ZAP_MOMENT;
if (!@mkdir($folder, 0777)) {
throw new Exception('<p>' . ERROR_PREFIX . 'Unable to create base dir: ' . $folder . '</p>');
}
return $folder;
} catch (Exception $e) {
$this->tpl .= $e->getMessage();
return;
}
}
/**
* Create an array based on post data
* @return array $words
*/
private function handleWords() {
$words = array();
$ix = 1;
$postix = sprintf("%02d", $ix);
$wd = 'word' . $postix;
while(!empty($_POST[$wd])) {
array_push($words, $_POST[$wd]);
$ix++;
$postfix = sprintf("%02d", $ix);
$wd = 'word' . $postfix;
}
return $words;
}
/**
* return rendered content
*/
public function getContent() {
return $this->tpl;
}
}
?>

40
lib/ZAPHome.class.php Executable file
View file

@ -0,0 +1,40 @@
<?php
/**
* Index class
*/
class ZAPHome {
/**
* Template file prefix
*/
private $template = 'index';
/**
* ZAP version
*/
private $ver;
/**
* Parsable content
*/
private $tpl = '';
/**
* Constructor
*/
public function __construct() {
$this->ver = ZAP_VERSION;
extract(get_object_vars($this));
ob_start();
include getZAPTemplate($this->template);
$this->tpl .= ob_get_clean();
}
/**
* @return cotent string
*/
public function getContent() {
return $this->tpl;
}
}
?>

121
lib/ZAPZap.class.php Executable file
View file

@ -0,0 +1,121 @@
<?php
/**
* ZAP factory
*/
class ZAPZap {
private $tpl = '';
private $folder;
private $height;
private $width;
private $words;
public function __construct() {
ob_start();
if (!isset($_SESSION['collages']) || $_SESSION['collages'] <= 0) {
$this->destroySession();
$this->tpl .= ob_get_clean();
return;
}
$this->width = $_SESSION['width'];
$this->height = $_SESSION['height'];
$this->folder = $_SESSION['folder'];
$this->words = $_SESSION['words'];
if (!empty($this->words)) {
// Still downloading source images for the words.
$this->showFoundImage();
} elseif (empty($_SESSION['images'])) {
// Start a new collage.
$this->buildNextCollage();
} else {
// Add another source image to the current collage.
$imgName = array_shift($_SESSION['images']);
$image = $this->createImage($_SESSION['collageNr']);
$image->addToCanvas($imgName);
}
$this->tpl .= ob_get_clean();
}
/**
* Build the next collage if any remain, otherwise finish.
*/
private function buildNextCollage() {
if ($_SESSION['collages'] <= 0) {
$this->destroySession();
return;
}
// On the very first collage, load the downloaded source images.
// On later collages, reuse the original list.
if (!isset($_SESSION['imgOriginal'])) {
$_SESSION['images'] = ImgTools::convertGifsToPng($this->retrieveImages());
$_SESSION['imgOriginal'] = $_SESSION['images'];
} else {
$_SESSION['images'] = $_SESSION['imgOriginal'];
shuffle($_SESSION['images']);
}
if (empty($_SESSION['images'])) {
$this->destroySession();
return;
}
// Track the current collage number separately so addToCanvas always
// reads/writes the same file while this collage is being built.
$_SESSION['collageNr'] = $_SESSION['collages'];
$_SESSION['collages']--;
$firstIm = array_shift($_SESSION['images']);
$image = $this->createImage($_SESSION['collageNr']);
$image->createBasicCanvas($firstIm);
}
private function createImage($collages) {
return new ImgProcess($this->folder, $this->width, $this->height, $collages);
}
private function destroySession() {
if (Session::destroy()) {
$this->tpl .= "<p>" . ERROR_PREFIX . "Image ready</p>";
} else {
$this->tpl .= "<p>" . ERROR_PREFIX . "Strange... Could not end PHP session";
}
return;
}
private function setSessionVar($key, $value) {
$_SESSION[$key] = $value;
}
private function showFoundImage() {
$keyWord = array_shift($this->words);
$search = new ImgSearch($this->folder, $keyWord);
$imageUrl = $search->getImage();
if ($imageUrl) {
ImgIOTools::urlOnScreen($imageUrl);
} else {
ImgIOTools::urlOnScreen(ZAP_APP_BASE_DIR . DIRECTORY_SEPARATOR
. ZAP_WEB_DIR . DIRECTORY_SEPARATOR
. 'images' . DIRECTORY_SEPARATOR . 'error_noHit.png');
}
$this->setSessionVar('words', $this->words);
}
private function retrieveImages() {
$img_ar = glob('{' . $this->folder . DIRECTORY_SEPARATOR .'*.jpg,'
. $this->folder . DIRECTORY_SEPARATOR .'*.png,'
. $this->folder . DIRECTORY_SEPARATOR .'*.jpeg,'
. $this->folder . DIRECTORY_SEPARATOR .'*.JPG,'
. $this->folder . DIRECTORY_SEPARATOR .'*.gif'
. '}', GLOB_BRACE);
shuffle($img_ar);
return $img_ar;
}
public function getContent() {
return $this->tpl;
}
}
?>

70
resource/Brave.class.php Normal file
View file

@ -0,0 +1,70 @@
<?php
class Brave implements Search {
private $key = BRAVE_API_KEY;
private $base_url = BRAVE_BASE;
private $q_prefix = '?q=';
private $count_prefix = '&count=';
private $adultPrefix = '&safesearch=';
private $word;
private $start;
private $size;
private $myCurl;
public function __construct() {
if (ZAP_ADULT == true) {
$this->adultPrefix .= 'off';
} else {
$this->adultPrefix .= 'strict';
}
}
public function setQuery($word) {
$this->word = $word;
$request = $this->q_prefix . urlencode($word);
if (isset($this->start)) {
$request .= '&offset=' . $this->start;
}
$request .= $this->count_prefix . (isset($this->size) ? (int)$this->size : 3);
$request .= $this->adultPrefix;
$this->myCurl = new ZAPCurl($this->base_url . $request, false);
$this->myCurl->setHeader('X-Subscription-Token', $this->key);
$this->myCurl->setHeader('Accept', 'application/json');
}
public function setParam($key, $value) {
$this->$key = $value;
}
public function getData() {
$return_array = $this->prepareData();
return $return_array;
}
/**
* Prepare a project wide universal array with results
*/
private function prepareData() {
$json = $this->myCurl->fetchCurlData();
$se_data = json_decode($json, true);
if (!isset($se_data['results'][0]['properties']['url'])) {
$data['result'] = false;
return $data;
}
$data['result'] = true;
$data['engine'] = __CLASS__;
$i = 0;
foreach($se_data['results'] as $result) {
$data['results'][$i]['clickurl'] = $result['properties']['url'];
$data['results'][$i]['referer'] = isset($result['url']) ? $result['url'] : '';
$data['results'][$i]['thumb'] = isset($result['thumbnail']['src']) ? $result['thumbnail']['src'] : '';
$i++;
}
return $data;
}
}
?>

25
resource/Search.interface.php Executable file
View file

@ -0,0 +1,25 @@
<?php
/**
* Interface for search engine implementations
*/
interface Search {
/**
* Set query word to search for
* @param string $word
*/
public function setQuery($word);
/**
* Set additional parameters
* @param string $key
* @param string $value
*/
public function setParam($key, $value);
/**
* Get received data as array
* @return array
*/
public function getData();
}
?>

24
tpl/display.tpl Executable file
View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Zap Machine <?php echo $ver; ?></title>
<link rel="stylesheet" media="screen" href="css/style.css">
<script src="js/display.js"></script>
</head>
<body onload="go(this,['img1','img2'],5000);">
<table style="width:100%; height:100%;">
<tr>
<td style="text-align:center; vertical-align:middle;">
<img src="images/downloading.gif" id="img1" alt="">
<img src="images/downloading.gif" id="img2" style="display:none;" alt="">
</td>
</tr>
<tr>
<td>
<div id="ready"></div>
</td>
</tr>
</table>
</body>
</html>

46
tpl/index.tpl Executable file
View file

@ -0,0 +1,46 @@
<?php $status = isset($_GET['stat']) ? $_GET['stat'] : ''; ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Zap ZapMachine <?php echo $ver; ?></title>
<link rel="stylesheet" media="screen" href="css/style.css">
<script src="js/main.js"></script>
</head>
<body>
<h1>Zap Machine <?php echo $ver; ?></h1>
<!--input modus: words-->
<div id="inpWords" class="inpSection">
<h2>Input words</h2>
<form id="form1" action="index.php?mode=display" method="post">
<?php for ($i = 1; $i <= MAX_WORDS; $i++) : ?>
<?php $nr = sprintf("%02d", $i); ?>
<div class="inp">
word <?php echo $nr; ?><input type="text" name="word<?php echo $nr; ?>" id="word<?php echo $nr; ?>" size="30" maxlength="30" value="">
</div>
<?php endfor; ?>
<div class="inp">
# Zaps
<select name="collages">
<?php for ($j = 1; $j <= MAX_ZAPS; $j++) : ?>
<option <?php echo $j == 4 ? 'selected' : ''; ?>><?php echo $j; ?></option>
<?php endfor; ?>
</select>
</div>
<div class="inp">
width <input type="text" name="width" id="width" size="5" maxlength="4" value="1067"> px
</div>
<div class="inp">
height <input type="text" name="height" id="height" size="5" maxlength="4" value="600"> px
</div>
<div class="inp">
<input type="button" class="btn" onclick="form1Submit()" value="Go &amp; Generate Some Zaps">
<input type="hidden" name="modus" value="words">
</div>
</form>
</div>
<!--End input modus words-->
<div id="status"><?php echo $status; ?></div>
<div id="loading"></div>
</body>
</html>

28
utils/Log.class.php Executable file
View file

@ -0,0 +1,28 @@
<?php
/**
* Puts log information on images in two files
*/
class Log {
private $folder;
private $logFileName;
private $logFileXmlName;
function __construct($imgFolder) {
$this->folder = $imgFolder;
$this->logFileName = $imgFolder . DIRECTORY_SEPARATOR . 'log.txt';
$this->logFileXmlName = $imgFolder . DIRECTORY_SEPARATOR . 'log.xml';
}
public function addToLog($type, $head, $addString) {
$addToLog = "$head $addString" . PHP_EOL;
$addToXmlLog = "<div class=\"$type\">" . PHP_EOL . "\t<strong>$head</strong>" . PHP_EOL;
$addToXmlLog .= "\t<p>$addString</p>" . PHP_EOL . "</div>" . PHP_EOL;
$txtOk = @file_put_contents($this->logFileName, $addToLog, FILE_APPEND);
$xmlOk = @file_put_contents($this->logFileXmlName, $addToXmlLog, FILE_APPEND);
}
}
?>

63
utils/Session.class.php Executable file
View file

@ -0,0 +1,63 @@
<?php
/**
* Basic session wrapper / adapter
*/
class Session {
/**
* Start session
* @param string $name name of session
* @param string $path override ini directive session.save_path
*/
public static function start($name, $path = null) {
$started = false;
if (!$started) {
session_name($name);
if ($path !== null) {
session_save_path($path);
}
session_start();
$started = true;
}
Session::setCacheControl('private');
}
/**
* Set cache control
* @param string $value sets the http cache control value
*/
public static function setCacheControl($value) {
header('Cache-Control: ' . $value);
}
/**
* reset session array
*/
public static function reset() {
$_SESSION = array();
}
/**
* delete client's cookie
*/
public static function deleteCookie() {
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-42000, '/');
}
}
/**
* Destroy session completely
* @return boolean
*/
public static function destroy() {
self::reset();
self::deleteCookie();
if (session_destroy()) {
return true;
} else {
return false;
}
}
}
?>

64
utils/ZAPCurl.class.php Executable file
View file

@ -0,0 +1,64 @@
<?php
class ZAPCurl {
private $url;
private $binary;
private $referer;
private $headers = array();
public $error = null;
public $httpCode = null;
//private $proxyIp = '127.0.0.1:81';
public function __construct($url, $binary = false) {
$this->url = $url;
$this->binary = $binary;
}
/**
* Set a custom header for the request
* @param string $key Header name
* @param string $value Header value
*/
public function setHeader($key, $value) {
$this->headers[] = $key . ': ' . $value;
}
/**
* Curl wrapper
* @param string $url
* @param boolean $binary
* @return mixed $curlData
*/
public function fetchCurlData() {
$cUrl = curl_init();
curl_setopt($cUrl, CURLOPT_URL, $this->url);
curl_setopt($cUrl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($cUrl, CURLOPT_TIMEOUT, ZAP_CURL_TIMEOUT);
if (isset($this->referer)) {
curl_setopt($cUrl, CURLOPT_REFERER, $this->referer);
}
if (!empty($this->headers)) {
curl_setopt($cUrl, CURLOPT_HTTPHEADER, $this->headers);
}
//curl_setopt($cUrl, CURLOPT_PROXY, $this->proxyIp);
$curlData = curl_exec($cUrl);
$this->error = curl_error($cUrl);
$this->httpCode = curl_getinfo($cUrl, CURLINFO_HTTP_CODE);
// curl_close() is deprecated as of PHP 8.5 (no effect since PHP 8.0)
if ($this->error) {
error_log('ZAPCurl error: ' . $this->error . ' URL: ' . $this->url);
} elseif ($this->httpCode >= 400) {
error_log('ZAPCurl HTTP ' . $this->httpCode . ' URL: ' . $this->url);
}
return $curlData;
}
}
?>

89
www/css/style.css Executable file
View file

@ -0,0 +1,89 @@
body, html {
margin:0;
padding:0;
color:#000;
background-color: #fff;
}
img {
padding: 12px;
border: #bbb dotted thin;
}
h1 {
font-family: Georgia, serif;
letter-spacing: 3px;
font-size: 16px;
position: relative;
margin-left: 22%;
}
h2 {
font-family: Georgia, serif;
font-size: 14px;
position: relative;
margin-left: 22%;
}
.inpSection {
margin-top: 6%;
position: relative;
border: #bbb dotted thin;
}
.inp {
font-family: sans-serif;
font-size: 10px;
display: block;
}
.inpTxt {
width: 18px;
text-align: right;
overflow: visible;
margin-right: 20px;
display: inline;
}
form {
padding: 7px 0 20px 20%;
}
input {
margin: 12px 15px 0 15px;
}
select {
margin: 12px 0 0 15px;
}
#status {
font-size: 10px;
color: red;
margin:15px 20px 0 0;
position: relative;
left: 20px;
}
#loading {
width: 100%;
height: 100%;
margin: 0;
position: absolute;
left: 0;
top: 0;
visibility: hidden;
background: white url('../images/downloading.gif') no-repeat fixed center center;
}
#ready {
text-align: center;
font-size: 14px;
text-decoration: blink;
}
.btn {
font-family: sans-serif;
font-size: 10px;
}

BIN
www/images/._downloading.gif Executable file

Binary file not shown.

BIN
www/images/._error_noHit.png Executable file

Binary file not shown.

BIN
www/images/downloading.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

BIN
www/images/error_noHit.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

42
www/index.php Executable file
View file

@ -0,0 +1,42 @@
<?php
/**
* ZapMachine (re)based on the ZapV
* ZAPV - Zap Machine as modified by Vincent Bruijn
* that was based on ApFab - Zap Machine V0.3.2
* ApFab: thanks for the basic work,
* Vincent Bruijn thanks for enhencing
* version 0.9.0
*/
if (!extension_loaded('imagick')) {
echo 'Enable the php imagick extension to get this running.';
die;
}
$mode = isset($_GET['mode']) ? $_GET['mode'] : 'home';
require_once '..' . DIRECTORY_SEPARATOR . 'conf' . DIRECTORY_SEPARATOR . 'init.php';
if (defined('ZAP_DEBUG') && ZAP_DEBUG) {
error_reporting(E_ALL);
ini_set('display_startup_errors', '1');
// Only display errors inline for HTML pages; raw image responses must stay clean.
if ($mode === 'zap') {
ini_set('display_errors', '0');
} else {
ini_set('display_errors', '1');
}
} else {
error_reporting(0);
ini_set('display_errors', '0');
}
$config = Config::getInstance();
$config->register('engines', $search_engines);
$frontcontroller = new ZAPController($mode);
$output = $frontcontroller->fetch();
print($output);
exit;
?>

33
www/js/display.js Executable file
View file

@ -0,0 +1,33 @@
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++);
};

54
www/js/main.js Executable file
View file

@ -0,0 +1,54 @@
function IsNumeric(sText) {
var ValidChars = "0123456789";
var IsNumber=true;
var Char;
for (i = 0; i < sText.length && IsNumber == true; i++) {
Char = sText.charAt(i);
if (ValidChars.indexOf(Char) == -1) {
IsNumber = false;
}
}
return IsNumber;
}
function form1Submit() {
//check if form is well filled
//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;
}
//tests ok
document.getElementById('status').innerHTML = ' ';
//download image visible and form invisible
document.getElementById('loading').style.visibility = 'visible';
document.getElementById('form1').style.visibility = 'hidden';
//document.getElementById('inpNrs').style.visibility = 'hidden';
//go for it!
document.getElementById('form1').submit();
}