1<?php
2/**
3 * MonsterID Generator for DokuWiki Avatar Plugin
4 *
5 * Generates identicon-style monster avatars based on a seed.
6 * Requires PHP GD extension.
7 *
8 *
9 * MIT License
10 *
11 * Copyright (c) 2007 Andreas Gohr <andi@splitbrain.org>
12 *     @source: https://github.com/splitbrain/monsterID
13 *
14 * Copyright (c) 2009 Gina Häußge <github.com/foosel>
15 *     @source: https://github.com/dokufreaks/plugin-avatar
16 *
17 * Copyright (c) 2025 Daniel Dias Rodrigues <danieldiasr@gmail.com>
18 *     @source: https://github.com/nerun/dokuwiki-plugin-avatar
19 *
20 * Permission is hereby granted, free of charge, to any person obtaining a copy
21 * of this software and associated documentation files (the "Software"), to deal
22 * in the Software without restriction, including without limitation the rights
23 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
24 * copies of the Software, and to permit persons to whom the Software is
25 * furnished to do so, subject to the following conditions:
26 *
27 * The above copyright notice and this permission notice shall be included in
28 * all copies or substantial portions of the Software.
29 *
30 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
36 * SOFTWARE.
37 */
38
39declare(strict_types=1);
40
41if (php_sapi_name() !== 'cli') {
42    $seed = preg_replace('/[^a-f0-9]/i', '', $_GET['seed'] ?? '');
43    $size = (int) ($_GET['size'] ?? 120);
44    $size = max(20, min(400, $size)); // limits between 20 and 400 pixels
45
46    header('Content-Type: image/png');
47    header('Cache-Control: public, max-age=86400');
48
49    $image = generate_monster($seed, $size);
50    if ($image) {
51        imagepng($image);
52        imagedestroy($image);
53    } else {
54        http_response_code(500);
55        echo 'Error generating image.';
56    }
57    exit;
58}
59
60/**
61 * Generates monster image based on seed and size
62 */
63function generate_monster(string $seed, int $size): ?GdImage
64{
65    if (!function_exists('imagecreatetruecolor')) {
66        return null;
67    }
68
69    $hash = md5($seed);
70
71    $parts = [
72        'legs'  => get_part(substr($hash, 0, 2), 1, 5),
73        'hair'  => get_part(substr($hash, 2, 2), 1, 5),
74        'arms'  => get_part(substr($hash, 4, 2), 1, 5),
75        'body'  => get_part(substr($hash, 6, 2), 1, 15),
76        'eyes'  => get_part(substr($hash, 8, 2), 1, 15),
77        'mouth' => get_part(substr($hash, 10, 2), 1, 10),
78    ];
79
80    $monster = imagecreatetruecolor(120, 120);
81    if (!$monster) return null;
82
83    $white = @imagecolorallocate($monster, 255, 255, 255);
84    imagefill($monster, 0, 0, $white);
85
86    foreach ($parts as $part => $index) {
87        $filename = __DIR__ . '/parts/' . $part . '_' . $index . '.png';
88        if (!file_exists($filename)) continue;
89        $part_img = imagecreatefrompng($filename);
90        imageSaveAlpha($part_img, true);
91        if ($part_img) {
92            imagecopy($monster, $part_img, 0, 0, 0, 0, 120, 120);
93            imagedestroy($part_img);
94
95             // color the body
96            if ($part === 'body') {
97                $r = get_part(substr($hash, 0, 4), 20, 235);
98                $g = get_part(substr($hash, 4, 4), 20, 235);
99                $b = get_part(substr($hash, 8, 4), 20, 235);
100                $color = imagecolorallocate($monster, $r, $g, $b);
101                imagefill($monster, 60, 60, $color);
102            }
103        }
104    }
105
106    if ($size !== 120) {
107        $resized = imagecreatetruecolor($size, $size);
108        imagefill($resized, 0, 0, $white);
109        imagecopyresampled($resized, $monster, 0, 0, 0, 0, $size, $size, 120, 120);
110        imagedestroy($monster);
111        return $resized;
112    }
113
114    return $monster;
115}
116
117/**
118 * Converts part of the hash into an image index
119 */
120function get_part(string $hex, int $min, int $max): int
121{
122    $val = hexdec($hex);
123    return ($val % ($max - $min + 1)) + $min;
124}
125
126