Skip to content

Commit a410dac

Browse files
Photoboothandi34
authored andcommitted
fix: support text rotation in collage zone mode and respect admin settings
Previously, the text rotation configured in the admin panel was ignored when using collage layouts with zone-based text alignment (text_alignment.mode = "zone"). The zone rendering in Image::applyTextInZone() always passed rotation=0 to imagettftext(), and Collage.php allowed JSON layout files to override the admin rotation value. Changes in Collage.php: - Zone-based text alignment from layout JSON is now only applied when "Allow layout selection" (collageAllowSelection) is enabled. When disabled, admin panel coordinates (locationx, locationy, rotation) are used directly via legacy rendering - Admin rotation value always takes priority over JSON layout rotation in both zone mode and legacy mode when layout selection is active - text_disabled from JSON is also only honored when layout selection is enabled Changes in Image.php: - applyTextInZone() now applies the configured rotation angle to imagettftext() instead of hardcoding 0 - Rotated text lines are stacked perpendicular to the text direction using trigonometric offset calculation (sin/cos) so lines appear properly aligned at any angle - Each line is individually centered along the text direction based on its width relative to the widest line - The entire rotated text block is precisely centered within the text zone by calculating the actual bounding box of all rendered glyphs using imagettfbbox() with the rotation angle, then computing the offset needed for proper horizontal (align) and vertical (valign) zone alignment - Admin line spacing (linespace) is respected as minimum distance between lines, preventing text from collapsing when auto-fit reduces the font size - Removed outdated comment "currently only 0 supported" from textZoneRotation property Closes #1433
1 parent 5bfd8a2 commit a410dac

File tree

2 files changed

+121
-25
lines changed

2 files changed

+121
-25
lines changed

src/Collage.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,12 @@ public static function createCollage(array $config, array $srcImagePaths, string
203203
}
204204

205205
// JSON layout can only disable or customize text if admin has enabled it
206+
// Zone-based text alignment is only applied when layout selection is allowed,
207+
// otherwise admin panel coordinates (locationx, locationy, rotation) are used
206208
if ($adminTextOnCollageEnabled === 'enabled') {
207209
if ($c->collageAllowSelection && isset($collageJson['text_disabled']) && $collageJson['text_disabled'] === true) {
208210
$c->textOnCollageEnabled = 'disabled';
209-
} elseif (isset($collageJson['text_alignment']) && is_array($collageJson['text_alignment'])) {
211+
} elseif ($c->collageAllowSelection && isset($collageJson['text_alignment']) && is_array($collageJson['text_alignment'])) {
210212
$ta = $collageJson['text_alignment'];
211213
$c->textOnCollageEnabled = 'enabled';
212214

@@ -223,9 +225,9 @@ public static function createCollage(array $config, array $srcImagePaths, string
223225
$c->textZonePadding = isset($ta['padding']) ? (float) Helper::doMath(str_replace(array_keys($replace), array_values($replace), $ta['padding'])) : 0;
224226
$c->textZoneAlign = $ta['align'] ?? 'center';
225227
$c->textZoneValign = $ta['valign'] ?? 'middle';
226-
$c->textZoneRotation = isset($ta['rotation']) ? (int) $ta['rotation'] : 0;
228+
$c->textZoneRotation = $c->textOnCollageRotation;
227229

228-
// In zone mode: ignore admin X/Y/Rotation values
230+
// In zone mode: ignore admin X/Y values, use admin rotation
229231
// Keep admin font, color, text lines, fontSize (as start), lineHeight (as factor)
230232
} else {
231233
// Legacy mode: calculate X/Y position based on alignment
@@ -238,9 +240,7 @@ public static function createCollage(array $config, array $srcImagePaths, string
238240
$c->textOnCollageFontSize = (int) Helper::doMath(str_replace(array_keys($replace), array_values($replace), $ta['fontSize']));
239241
}
240242

241-
if (isset($ta['rotation'])) {
242-
$c->textOnCollageRotation = (int) $ta['rotation'];
243-
}
243+
// Admin rotation is always used, JSON rotation is ignored
244244

245245
if (isset($ta['lineHeight'])) {
246246
$c->textOnCollageLinespace = (int) Helper::doMath(str_replace(array_keys($replace), array_values($replace), $ta['lineHeight']));

src/Image.php

Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class Image
138138
public string $textZoneValign = 'middle';
139139

140140
/**
141-
* Rotation angle for zone text (currently only 0 supported)
141+
* Rotation angle for zone text
142142
*/
143143
public int $textZoneRotation = 0;
144144

@@ -986,8 +986,8 @@ private function applyTextInZone(GdImage $sourceResource, string $fontPath, int
986986
$fontSize = $minFontSize;
987987
}
988988

989-
// Recalculate with final font size
990-
$lineHeight = (int)($fontSize * $lineHeightFactor);
989+
// Recalculate with final font size, but ensure admin linespace is respected as minimum
990+
$lineHeight = max((int)($fontSize * $lineHeightFactor), $this->textLineSpacing);
991991
$blockHeight = (count($lines) - 1) * $lineHeight + $fontSize;
992992

993993
// Get ascent for baseline correction
@@ -1009,34 +1009,130 @@ private function applyTextInZone(GdImage $sourceResource, string $fontPath, int
10091009
break;
10101010
}
10111011

1012-
// Draw each line with individual horizontal alignment
1013-
foreach ($lines as $index => $line) {
1014-
// Measure this specific line
1015-
$bbox = @imagettfbbox($fontSize, 0, $fontPath, $line);
1016-
$lineWidth = $bbox !== false ? abs($bbox[2] - $bbox[0]) : 0;
1012+
// Calculate base X position for the text block
1013+
$rotation = $this->textZoneRotation;
1014+
$radians = deg2rad($rotation);
10171015

1018-
// Calculate X position based on align
1016+
if ($rotation != 0) {
1017+
// Measure all lines and collect widths
1018+
$maxLineWidth = 0;
1019+
$lineWidths = [];
1020+
foreach ($lines as $line) {
1021+
$bbox = @imagettfbbox($fontSize, 0, $fontPath, $line);
1022+
$w = $bbox !== false ? abs($bbox[2] - $bbox[0]) : 0;
1023+
$lineWidths[] = $w;
1024+
if ($w > $maxLineWidth) {
1025+
$maxLineWidth = $w;
1026+
}
1027+
}
1028+
1029+
$cosR = cos($radians);
1030+
$sinR = sin($radians);
1031+
$lineCount = count($lines);
1032+
1033+
// Calculate all draw positions (before zone centering) relative to origin (0,0)
1034+
// Each line is stacked perpendicular to text direction and centered along it
1035+
$positions = [];
1036+
foreach ($lines as $index => $line) {
1037+
$lineWidth = $lineWidths[$index];
1038+
$centerShift = ($maxLineWidth - $lineWidth) / 2;
1039+
1040+
// Perpendicular offset (stacking): direction (sin(θ), cos(θ))
1041+
$perpX = $index * $lineHeight * $sinR;
1042+
$perpY = $index * $lineHeight * $cosR;
1043+
1044+
// Parallel offset (centering): direction (cos(θ), -sin(θ))
1045+
$paraX = $centerShift * $cosR;
1046+
$paraY = -$centerShift * $sinR;
1047+
1048+
$positions[] = ['x' => $perpX + $paraX, 'y' => $perpY + $paraY];
1049+
}
1050+
1051+
// Calculate actual bounding box of all rendered text using imagettfbbox with rotation
1052+
$minBx = PHP_INT_MAX;
1053+
$minBy = PHP_INT_MAX;
1054+
$maxBx = PHP_INT_MIN;
1055+
$maxBy = PHP_INT_MIN;
1056+
foreach ($lines as $index => $line) {
1057+
$bbox = @imagettfbbox($fontSize, $rotation, $fontPath, $line);
1058+
if ($bbox !== false) {
1059+
$px = $positions[$index]['x'];
1060+
$py = $positions[$index]['y'];
1061+
// imagettfbbox returns 4 corners: ll, lr, ur, ul
1062+
for ($i = 0; $i < 8; $i += 2) {
1063+
$bx = $px + $bbox[$i];
1064+
$by = $py + $bbox[$i + 1];
1065+
$minBx = min($minBx, $bx);
1066+
$minBy = min($minBy, $by);
1067+
$maxBx = max($maxBx, $bx);
1068+
$maxBy = max($maxBy, $by);
1069+
}
1070+
}
1071+
}
1072+
1073+
$totalW = $maxBx - $minBx;
1074+
$totalH = $maxBy - $minBy;
1075+
1076+
// Calculate zone center offset for the text block
10191077
switch ($this->textZoneAlign) {
10201078
case 'right':
1021-
$drawX = (int)($zoneX + $zoneW - $lineWidth);
1079+
$offsetX = $zoneX + $zoneW - $totalW - $minBx;
10221080
break;
10231081
case 'center':
1024-
$drawX = (int)($zoneX + ($zoneW - $lineWidth) / 2);
1082+
$offsetX = $zoneX + ($zoneW - $totalW) / 2 - $minBx;
10251083
break;
10261084
case 'left':
10271085
default:
1028-
$drawX = (int)$zoneX;
1086+
$offsetX = $zoneX - $minBx;
1087+
break;
1088+
}
1089+
1090+
switch ($this->textZoneValign) {
1091+
case 'bottom':
1092+
$offsetY = $zoneY + $zoneH - $totalH - $minBy;
1093+
break;
1094+
case 'middle':
1095+
$offsetY = $zoneY + ($zoneH - $totalH) / 2 - $minBy;
1096+
break;
1097+
case 'top':
1098+
default:
1099+
$offsetY = $zoneY - $minBy;
10291100
break;
10301101
}
10311102

1032-
// Calculate Y position (baseline position)
1033-
// First line: startTopY + ascent (to position top of text at startTopY)
1034-
// Subsequent lines: add lineHeight for each
1035-
$drawY = (int)($startTopY + $ascent + ($index * $lineHeight));
1103+
// Draw each line at calculated position + zone offset
1104+
foreach ($lines as $index => $line) {
1105+
$drawX = (int)($positions[$index]['x'] + $offsetX);
1106+
$drawY = (int)($positions[$index]['y'] + $offsetY);
10361107

1037-
// Draw the text (rotation is 0 for zone mode)
1038-
if (!imagettftext($sourceResource, $fontSize, 0, $drawX, $drawY, $color, $fontPath, $line)) {
1039-
throw new \Exception('Could not add line ' . ($index + 1) . ' of text to resource.');
1108+
if (!imagettftext($sourceResource, $fontSize, $rotation, $drawX, $drawY, $color, $fontPath, $line)) {
1109+
throw new \Exception('Could not add line ' . ($index + 1) . ' of text to resource.');
1110+
}
1111+
}
1112+
} else {
1113+
// No rotation: per-line horizontal alignment
1114+
foreach ($lines as $index => $line) {
1115+
$bbox = @imagettfbbox($fontSize, 0, $fontPath, $line);
1116+
$lineWidth = $bbox !== false ? abs($bbox[2] - $bbox[0]) : 0;
1117+
1118+
switch ($this->textZoneAlign) {
1119+
case 'right':
1120+
$drawX = (int)($zoneX + $zoneW - $lineWidth);
1121+
break;
1122+
case 'center':
1123+
$drawX = (int)($zoneX + ($zoneW - $lineWidth) / 2);
1124+
break;
1125+
case 'left':
1126+
default:
1127+
$drawX = (int)$zoneX;
1128+
break;
1129+
}
1130+
1131+
$drawY = (int)($startTopY + $ascent + ($index * $lineHeight));
1132+
1133+
if (!imagettftext($sourceResource, $fontSize, 0, $drawX, $drawY, $color, $fontPath, $line)) {
1134+
throw new \Exception('Could not add line ' . ($index + 1) . ' of text to resource.');
1135+
}
10401136
}
10411137
}
10421138
}

0 commit comments

Comments
 (0)