Die Beschriftung einer Diagrammachse soll senkrecht stehen, und ein Anmeldeformular braucht einen Zahlencode, bei dem nicht jeder Buchstabe gleich aussieht. Beides verlangt Zugriff auf jedes einzelne Zeichen, und genau dafür gibt es PHP imagechar. Die Funktion zeichnet ein Zeichen, nicht einen Text, und dieser Unterschied ist kein Schönheitsfehler, sondern ihr ganzer Zweck. Wie ein Bild angelegt und eine Farbe reserviert wird, zeigt das Tutorial zur GD-Bibliothek. Dieser Text setzt dort an, wo das fertige Bild eine Beschriftung braucht.
Das Fadenkreuz sitzt beim aufrechten Zeichen an der oberen linken Ecke und bei der gedrehten Variante woanders. Warum, klärt der Abschnitt zu den Koordinaten.
Was PHP imagechar() zeichnet
Die Funktion setzt eine einzelne Glyphe aus einer der fünf eingebauten Bitmap-Schriften auf ein bestehendes Bild. Sie erzeugt nichts Neues, sie gibt nur einen Wahrheitswert zurück, und sie braucht wie alle Zeichenbefehle in GD einen Farbindex aus imagecolorallocate(). Das kleinste lauffähige Beispiel besteht aus vier Zeilen Vorbereitung und einer Zeile für das Zeichen.
<?php
$bild = imagecreatetruecolor(200, 80);
$weiss = imagecolorallocate($bild, 255, 255, 255);
$tinte = imagecolorallocate($bild, 20, 20, 20);
imagefilledrectangle($bild, 0, 0, 199, 79, $weiss);
imagechar($bild, 5, 20, 20, 'A', $tinte);
header('Content-Type: image/png');
imagepng($bild);
imagedestroy($bild);
Die 5 im zweiten Parameter ist die Nummer der eingebauten Schrift, mit der PHP imagechar() zeichnet. Höhere Nummern ergeben größere Zeichen, allerdings nicht streng der Reihe nach. Wer die genauen Maße braucht, holt sie sich über imagefontwidth() und imagefontheight(), die bei jeder Nummer die Zeichenzelle in Pixeln melden.
Syntax, Parameter und Rückgabewert im Original
<?php
imagechar(
GdImage $image,
GdFont|int $font,
int $x,
int $y,
string $char,
int $color
): bool
/* draws the FIRST character of char and
silently drops the rest of the string
font: 1 to 5, or an object that
imageloadfont() has returned
x, y: the top left corner of the character
cell, not the baseline
char: only char[0] is ever used
color: an index from imagecolorallocate()
returns true, even for an empty string;
imagecharup() takes the same arguments but
turns the glyph by 90 degrees */
Der Schrifttyp ist seit PHP 8.0 GdFont|int und nicht mehr resource|int. $x und $y liegen an der oberen linken Ecke der Zeichenzelle und nicht auf der Grundlinie, wie man es von Textsatzsystemen kennt. Der Rückgabewert von PHP imagechar() taugt kaum zur Fehlersuche, weil er auch dann true lautet, wenn nichts gezeichnet wurde.
Nur das erste Zeichen landet im Bild
Das ist der Punkt, an dem die meisten stolpern. Übergibt man der Funktion eine längere Zeichenkette, wird alles ab dem zweiten Zeichen stillschweigend verworfen. Es gibt keine Warnung, keine Ausnahme und keinen abweichenden Rückgabewert. Der folgende Vergleich macht es sichtbar: Beide Aufrufe bekommen dieselbe Zeichenkette, nur das Ergebnis unterscheidet sich.
<?php
imagechar($bild, 5, 10, 10, 'ABC', $tinte);
imagestring($bild, 5, 10, 40, 'ABC', $tinte);
/* Tinte nachgemessen, Schrift 5:
imagechar x 10 bis 17 = 8 px, ein Zeichen
imagestring x 10 bis 35 = 26 px, drei Zeichen */
Auch eine leere Zeichenkette ist kein Fehlerfall: PHP imagechar() gibt true zurück und zeichnet nichts. Wer also ein Bild ohne Beschriftung vor sich hat, findet die Ursache nicht am Rückgabewert, sondern nur am Bild selbst.
imagechar() und imagestring() im Vergleich
Die Entscheidung zwischen beiden Funktionen hängt an einer einzigen Frage: Soll je Zeichen etwas Eigenes passieren? Eine andere Farbe, ein senkrechter Versatz, ein unregelmäßiger Abstand oder eine Drehung sind mit einem einzelnen Aufruf für den ganzen Text nicht zu haben.
| Funktion | Was gezeichnet wird | Zugriff je Zeichen | wann sinnvoll |
| imagechar() | genau ein Zeichen | voll, über die Schleife | Captcha, Farbverlauf im Wort, freie Abstände |
| imagecharup() | ein Zeichen, um 90 Grad gedreht | voll, über die Schleife | senkrechte Achsenbeschriftung ohne Schriftdatei |
| imagestring() | die ganze Zeichenkette | keiner, eine Farbe für alles | gewöhnliche Beschriftungen, der Normalfall |
Die Schleife kostet Zeilen und bringt nur dann etwas ein, wenn im Rumpf wirklich etwas variiert. Ein Wort in einer Farbe gehört zu imagestring(), ein Wort mit wechselnder Farbe zu PHP imagechar(). So sieht der zweite Fall aus.
<?php
$wort = 'Status';
$farben = [$rot, $grau, $dunkel];
$x = 10;
foreach (str_split($wort) as $i => $z) {
$farbe = $farben[$i % 3];
imagechar($bild, 4, $x, 20, $z, $farbe);
$x += imagefontwidth(4);
}
Wohin die Koordinaten x und y zeigen
Bei PHP imagechar() bezeichnen $x und $y die obere linke Ecke der Zeichenzelle. Der Kasten wächst also nach rechts und nach unten, wie man es von imagecopy() kennt. Bei imagecharup() ist es anders, und hier rät fast jeder zuerst falsch: $y ist dort die Unterkante, der Kasten wächst nach rechts und nach oben. Die Drehung erfolgt gegen den Uhrzeigersinn, das Zeichen liest sich also von unten nach oben.
<?php
imagechar($bild, 1, 60, 60, 'A', $tinte);
/* Tinte bei x 60 bis 63 und y 61 bis 66 */
imagecharup($bild, 1, 60, 60, 'A', $tinte);
/* Tinte bei x 61 bis 66 und y 57 bis 60 */
Aus der 5 mal 8 Pixel großen Zelle der Schrift 1 ist eine 8 mal 5 Pixel große geworden, und die angegebene y-Koordinate 60 liegt an ihrem unteren Rand. Wer das übersieht, zeichnet seine Achsenbeschriftung aus dem Bild heraus. Übrigens gilt die Einzelzeichen-Regel auch für die gedrehte Variante: imagecharup() mit der Zeichenkette 'ABC' hinterlässt exakt dieselbe Tinte wie mit 'A'.
flowchart TD
A[Zeichen setzen] --> B{Je Zeichen eigen?}
B -- nein --> C[imagestring]
B -- ja --> D{Gedreht?}
D -- nein --> E[imagechar]
E --> F[x,y oben links]
D -- ja --> G[imagecharup]
G --> H[x,y unten links]
F --> I[PNG ausgeben]
H --> I
C --> I
imagecharup(): Zeichen um 90 Grad gedreht
Ohne TrueType-Schriftdatei ist imagecharup() die einzige Möglichkeit, senkrechten Text auf ein GD-Bild zu bekommen. Bis auf die Drehung verhält sich die Funktion genau wie imagechar(). Andere Winkel gibt es nicht, nur diese eine Drehung. Das klingt nach wenig, deckt aber den häufigsten Fall ab, nämlich die Beschriftung einer senkrechten Achse. Wer das Zeichen in die Gegenrichtung drehen will, muss das fertige Bild anschließend mit imagerotate() wenden.
Am schnellsten begreift man die Sache, wenn beide Varianten nebeneinander im selben Bild stehen. Ein F eignet sich dafür besser als ein A, weil man ihm die Richtung ansieht.
<?php
$bild = imagecreatetruecolor(120, 80);
$weiss = imagecolorallocate($bild, 255, 255, 255);
$tinte = imagecolorallocate($bild, 20, 20, 20);
imagefilledrectangle($bild, 0, 0, 119, 79, $weiss);
imagechar($bild, 5, 30, 30, 'F', $tinte);
imagecharup($bild, 5, 70, 45, 'F', $tinte);
imagepng($bild, 'gedreht.png');
Der Querbalken des aufrechten F zeigt nach rechts, beim gedrehten zeigt er nach oben. Der Stamm liegt dort waagerecht auf der Linie y gleich 45, und das ist genau die Koordinate aus dem Aufruf. Alles, was zu diesem Zeichen gehört, steht über dieser Linie.
Praxis: eine senkrechte Achsenbeschriftung
Die Schleife läuft von unten nach oben, also mit abnehmendem $y. Der Schritt je Durchlauf ist die Zeichenbreite der gewählten Schrift, bei Schrift 5 also 9 Pixel. Woher dieser Wert kommt und wie man ihn für jede Schrift erfragt, steht im Tutorial zu imagefontwidth() und der Textbreite.
<?php
$text = 'Wert';
$vorschub = 9; /* Zeichenbreite Schrift 5 */
$y = 150;
foreach (str_split($text) as $z) {
imagecharup($bild, 5, 20, $y, $z, $tinte);
$y -= $vorschub;
}
/* vier Zeichen belegen 35 px Höhe
bei 36 px Vorschub */
Der Unterschied von einem Pixel zwischen Vorschub und belegter Höhe ist kein Rechenfehler, sondern der Leerstreifen am Rand der letzten Zeichenzelle. Diese Beschriftung gehört neben eine Diagrammfläche, und wie die entsteht, zeigt das Tutorial zu imagearc().
Praxis: ein einfaches Captcha
Ein Captcha ist der Lehrbuchfall für PHP imagechar(), weil hier wirklich jedes Zeichen anders aussehen soll: eigene Farbe, zufälliger senkrechter Versatz und ein unregelmäßiger Abstand zum nächsten. Dazu kommen ein paar Störlinien quer über das Bild.
<?php
$farben = [
imagecolorallocate($bild, 20, 20, 20),
imagecolorallocate($bild, 120, 120, 120),
imagecolorallocate($bild, 60, 60, 60),
];
$code = 'K7FQ3M';
$x = 12;
foreach (str_split($code) as $i => $z) {
$farbe = $farben[$i % 3];
$y = 14 + random_int(-6, 6);
imagechar($bild, 5, $x, $y, $z, $farbe);
$x += imagefontwidth(5) + random_int(2, 6);
}
for ($i = 0; $i < 6; $i++) {
imageline(
$bild, 0, random_int(0, 49),
139, random_int(0, 49), $farben[1]
);
}
Dazu gehört ein ehrlicher Hinweis. Ein solches Bild hält einfache Formularroboter auf, mehr nicht. Die eingebauten Schriften sind Bitmaps in fester Größe, und genau das macht sie für eine Texterkennung leicht lesbar: Jede Glyphe ist ein bekanntes Pixelmuster, der Versatz ändert daran nichts. Gegen gezielte Automatisierung braucht es andere Mittel. Wer den erzeugten Code für Testläufe reproduzierbar haben will, findet die passende Technik im Tutorial zu mt_srand().
Eigene Bitmap-Schriften mit imageloadfont()
Die fünf eingebauten Schriften sind schnell ausgereizt, und PHP imagechar() ist nicht auf sie festgelegt. imageloadfont() lädt eine eigene Bitmap-Schrift aus einer Datei, und das Format ist erfreulich schlicht. Der Dateikopf umfasst 16 Byte, nämlich vier Ganzzahlen zu je vier Byte in der Byte-Reihenfolge des Rechners, auf dem PHP läuft: Anzahl der Zeichen, Nummer des ersten Zeichens, Breite und Höhe. Danach folgen die Bilddaten, ein Byte je Pixel mit dem Wert 0 oder 1, zeilenweise je Zeichen. Die Gesamtlänge ist damit 16 + anzahl * breite * hoehe. Das Formatzeichen V im Beispiel unten schreibt die vier Zahlen ausdrücklich little-endian und trifft damit jede heute gebräuchliche Plattform; ein big-endian geschriebener Kopf wird von imageloadfont() abgelehnt.
<?php
$breite = 8;
$hoehe = 8;
$muster = [
'A' => [
'..####..', '.##..##.', '##....##',
'##....##', '########', '##....##',
'##....##', '........',
],
/* B und C nach demselben Schema */
];
$daten = pack(
'V4', count($muster), 65, $breite, $hoehe
);
foreach ($muster as $zeilen) {
foreach ($zeilen as $zeile) {
foreach (str_split($zeile) as $p) {
$daten .= chr($p === '#' ? 1 : 0);
}
}
}
file_put_contents('meine.gdf', $daten);
/* mit A, B und C im Muster:
16 Byte Kopf + 3 * 64 Byte = 208 Byte */
Die 65 im Kopf ist der Codepunkt des ersten Zeichens, hier also das große A. Beim Laden ist eine Sache wichtig, die viele ältere Beispiele im Netz falsch zeigen: imageloadfont() liefert seit PHP 8.0 ein Objekt der Klasse GdFont. Eine Prüfung mit is_resource(), wie sie bis PHP 7.4 richtig war, schlägt deshalb immer fehl und wirft das Skript in einen Fehlerzweig, obwohl die Schrift geladen wurde. Richtig ist der Vergleich mit false.
<?php
$eigen = imageloadfont('meine.gdf');
if ($eigen === false) {
throw new RuntimeException('Schrift kaputt');
}
var_dump($eigen instanceof GdFont); /* true */
var_dump(is_resource($eigen)); /* false */
imagechar($bild, $eigen, 60, 5, 'B', $tinte);
Das geladene Schriftobjekt verhält sich danach wie eine eingebaute Schrift. Es lässt sich an PHP imagechar() übergeben, an imagecharup() und an imagestring(), und auch die Maßfunktionen melden korrekt 8 und 8.
Umlaute und die Kodierung der eingebauten Schriften
Die eingebauten Schriften arbeiten byteweise und sind Latin2-kodiert (ISO-8859-2), nicht Latin-1. Das lässt sich nachprüfen, statt es zu glauben: Zeichnet man das Byte 191, erscheint ein z mit Punkt darüber, und beim Byte 230 ein c mit Akut. In Latin-1 stünden an diesen Stellen das umgekehrte Fragezeichen ¿ und die Ligatur æ.
Für deutsche Texte bleibt das folgenlos, und auch das ist nachgerechnet: Die sieben deutschen Sonderzeichen liegen in Latin-1 und Latin-2 auf denselben Codepunkten, nämlich 196, 214, 220, 228, 246, 252 und 223. Alle fünf eingebauten Schriften tragen die passenden Glyphen dazu. Der Fehler entsteht also nicht an der Kodierung der Schrift, sondern an UTF-8 auf der anderen Seite. Dort belegt ein großes Ä zwei Byte und beginnt mit dem Byte 195. PHP imagechar() zeichnet dieses erste Byte, und an Position 195 steht in der Schrift ein ganz anderes Zeichen.
<?php
$wort = 'Äpfel';
echo ord($wort[0]); /* 195, erstes UTF-8-Byte */
$latin = mb_convert_encoding(
$wort, 'ISO-8859-2', 'UTF-8'
);
echo ord($latin[0]); /* 196, wie erwartet */
foreach (str_split($latin) as $i => $z) {
imagechar($bild, 5, 10 + $i * 9, 10, $z, $tinte);
}
Für rein deutsche Texte führt 'ISO-8859-1' an dieser Stelle zum selben Ergebnis, weil die Codepunkte übereinstimmen. Wer polnische oder tschechische Namen zeichnen will, muss dagegen ISO-8859-2 nehmen, sonst landen die Sonderzeichen daneben.
Fazit
PHP imagechar() zeichnet ein Zeichen, und der Rest der Zeichenkette verschwindet ohne Meldung. Wer das weiß, benutzt die Funktion bewusst: in einer Schleife, wenn je Zeichen Farbe, Abstand oder Versatz wechseln sollen, und sonst gar nicht. Bei imagecharup() ist die y-Koordinate die Unterkante und die Drehung läuft gegen den Uhrzeigersinn; mit diesen beiden Sätzen sitzt eine senkrechte Achsenbeschriftung auf Anhieb richtig. Und wenn die fünf eingebauten Schriften nicht reichen, ist eine eigene Bitmap-Schrift kein großes Vorhaben: 16 Byte Kopf, ein Byte je Pixel, fertig.