Skip to content

Commit c568394

Browse files
PhotoboothPhotobooth
authored andcommitted
Merge branch 'dev' into bugfix/csrf
Resolve conflict in assets/js/admin/index.js: keep photoboothTools-only globals (CSRF via ajaxWithCsrf) and eslint-env browser from dev. Made-with: Cursor
2 parents 1c6177e + 274b5d1 commit c568394

35 files changed

+1260
-267
lines changed

api/getCollageLayouts.php

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
<?php
2+
3+
require_once '../lib/boot.php';
4+
5+
use Photobooth\Collage;
6+
use Photobooth\Utility\PathUtility;
7+
8+
header('Content-Type: application/json');
9+
10+
$allowedOrientations = ['landscape', 'portrait'];
11+
$requestedOrientation = isset($_GET['orientation']) ? (string) $_GET['orientation'] : 'landscape';
12+
$orientation = in_array($requestedOrientation, $allowedOrientations, true) ? $requestedOrientation : 'landscape';
13+
$fallbackOrientation = $orientation === 'landscape' ? 'portrait' : 'landscape';
14+
15+
function evaluateCollageExpression(string $expr, float $x, float $y): float
16+
{
17+
$expr = str_replace(['x', 'y'], [(string) $x, (string) $y], $expr);
18+
19+
try {
20+
if (preg_match('/^[\d\.\+\-\*\/\(\)\s]+$/', $expr)) {
21+
return (float) eval("return $expr;");
22+
}
23+
} catch (\Throwable $e) {
24+
return 0.0;
25+
}
26+
27+
return (float) $expr;
28+
}
29+
30+
function loadCollageLayoutData(string $layoutId, string $orientation): ?array
31+
{
32+
$jsonPath = Collage::getCollageConfigPath($layoutId, $orientation);
33+
34+
if ($jsonPath === null || !is_file($jsonPath)) {
35+
return null;
36+
}
37+
38+
$jsonContent = file_get_contents($jsonPath);
39+
if ($jsonContent === false) {
40+
return null;
41+
}
42+
43+
$data = json_decode($jsonContent, true);
44+
if (!is_array($data)) {
45+
return null;
46+
}
47+
48+
if (isset($data['layout']) && is_array($data['layout'])) {
49+
return $data;
50+
}
51+
52+
if (isset($data[0]) && is_array($data[0])) {
53+
return [
54+
'layout' => $data,
55+
];
56+
}
57+
58+
return null;
59+
}
60+
61+
function ensurePrivateLayoutDirectories(): void
62+
{
63+
$directories = [
64+
'private/collage/layouts',
65+
'private/collage/layouts/landscape',
66+
'private/collage/layouts/portrait',
67+
];
68+
69+
foreach ($directories as $directory) {
70+
$absolutePath = PathUtility::getAbsolutePath($directory);
71+
if (is_dir($absolutePath)) {
72+
continue;
73+
}
74+
75+
@mkdir($absolutePath, 0775, true);
76+
}
77+
}
78+
79+
function buildCollageLayoutPreviewSvg(string $layoutId, ?array $layoutData): string
80+
{
81+
if (!is_array($layoutData) || empty($layoutData['layout']) || !is_array($layoutData['layout'])) {
82+
$width = 1800;
83+
$height = 1200;
84+
$positions = [
85+
['x' => 0, 'y' => 0, 'w' => 90, 'h' => 60, 'num' => 1],
86+
['x' => 90, 'y' => 0, 'w' => 90, 'h' => 60, 'num' => 2],
87+
['x' => 0, 'y' => 60, 'w' => 90, 'h' => 60, 'num' => 3],
88+
['x' => 90, 'y' => 60, 'w' => 90, 'h' => 60, 'num' => 4],
89+
];
90+
} else {
91+
$width = (float) ($layoutData['width'] ?? 1800);
92+
$height = (float) ($layoutData['height'] ?? 1200);
93+
$scale = 0.1;
94+
95+
$layoutName = str_ends_with($layoutId, '.json') ? substr($layoutId, 0, -5) : $layoutId;
96+
$isPhotostrip = str_starts_with($layoutName, '2x');
97+
$layoutCount = count($layoutData['layout']);
98+
$uniquePhotoCount = $isPhotostrip ? (int) ($layoutCount / 2) : $layoutCount;
99+
100+
$positions = [];
101+
$photoNum = 1;
102+
103+
foreach ($layoutData['layout'] as $index => $photoLayout) {
104+
if (!is_array($photoLayout) || count($photoLayout) < 4) {
105+
continue;
106+
}
107+
108+
$x = evaluateCollageExpression((string) $photoLayout[0], $width, $height);
109+
$y = evaluateCollageExpression((string) $photoLayout[1], $width, $height);
110+
$w = evaluateCollageExpression((string) $photoLayout[2], $width, $height);
111+
$h = evaluateCollageExpression((string) $photoLayout[3], $width, $height);
112+
113+
$displayNum = $isPhotostrip && $index >= $uniquePhotoCount
114+
? ($index - $uniquePhotoCount + 1)
115+
: $photoNum;
116+
117+
$positions[] = [
118+
'x' => $x * $scale,
119+
'y' => $y * $scale,
120+
'w' => $w * $scale,
121+
'h' => $h * $scale,
122+
'num' => $displayNum,
123+
];
124+
125+
if (!$isPhotostrip || $index < $uniquePhotoCount - 1) {
126+
$photoNum++;
127+
} elseif ($index === $uniquePhotoCount - 1) {
128+
$photoNum = 1;
129+
}
130+
}
131+
132+
if (empty($positions)) {
133+
$positions = [
134+
['x' => 0, 'y' => 0, 'w' => 90, 'h' => 60, 'num' => 1],
135+
['x' => 90, 'y' => 0, 'w' => 90, 'h' => 60, 'num' => 2],
136+
['x' => 0, 'y' => 60, 'w' => 90, 'h' => 60, 'num' => 3],
137+
['x' => 90, 'y' => 60, 'w' => 90, 'h' => 60, 'num' => 4],
138+
];
139+
}
140+
}
141+
142+
$viewBoxWidth = $width * 0.1;
143+
$viewBoxHeight = $height * 0.1;
144+
145+
$svg = sprintf(
146+
'<svg class="collageSelector__preview" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">',
147+
(int) round($viewBoxWidth),
148+
(int) round($viewBoxHeight)
149+
);
150+
151+
foreach ($positions as $pos) {
152+
$svg .= sprintf(
153+
'<rect x="%s" y="%s" width="%s" height="%s" fill="#4A90E2" stroke="#FFFFFF" stroke-width="2" rx="2"/>',
154+
number_format($pos['x'] + 2, 1, '.', ''),
155+
number_format($pos['y'] + 2, 1, '.', ''),
156+
number_format($pos['w'] - 4, 1, '.', ''),
157+
number_format($pos['h'] - 4, 1, '.', '')
158+
);
159+
160+
$centerX = $pos['x'] + $pos['w'] / 2;
161+
$centerY = $pos['y'] + $pos['h'] / 2;
162+
$svg .= sprintf(
163+
'<text x="%s" y="%s" text-anchor="middle" dominant-baseline="middle" fill="#FFFFFF" font-size="28" font-weight="bold" font-family="Arial, sans-serif">%d</text>',
164+
number_format($centerX, 1, '.', ''),
165+
number_format($centerY + 2, 1, '.', ''),
166+
$pos['num']
167+
);
168+
}
169+
170+
$layoutName = str_ends_with($layoutId, '.json') ? substr($layoutId, 0, -5) : $layoutId;
171+
$isPhotostrip = str_starts_with($layoutName, '2x');
172+
if ($isPhotostrip) {
173+
if ($width > $height) {
174+
$middleY = $viewBoxHeight / 2;
175+
$svg .= sprintf(
176+
'<line x1="0" y1="%s" x2="%s" y2="%s" stroke="#FF0000" stroke-width="2" stroke-dasharray="5,5" opacity="0.8"/>',
177+
number_format($middleY, 1, '.', ''),
178+
number_format($viewBoxWidth, 1, '.', ''),
179+
number_format($middleY, 1, '.', '')
180+
);
181+
} else {
182+
$middleX = $viewBoxWidth / 2;
183+
$svg .= sprintf(
184+
'<line x1="%s" y1="0" x2="%s" y2="%s" stroke="#FF0000" stroke-width="2" stroke-dasharray="5,5" opacity="0.8"/>',
185+
number_format($middleX, 1, '.', ''),
186+
number_format($middleX, 1, '.', ''),
187+
number_format($viewBoxHeight, 1, '.', '')
188+
);
189+
}
190+
}
191+
192+
$svg .= sprintf(
193+
'<rect x="0" y="0" width="%s" height="%s" fill="none" stroke="#666666" stroke-width="1" rx="2"/>',
194+
number_format($viewBoxWidth, 1, '.', ''),
195+
number_format($viewBoxHeight, 1, '.', '')
196+
);
197+
$svg .= '</svg>';
198+
199+
return $svg;
200+
}
201+
202+
$layouts = [];
203+
$seen = [];
204+
205+
ensurePrivateLayoutDirectories();
206+
207+
$readLayoutsFromDir = static function (string $dirPath, bool $isPrivateSource) use (&$layouts, &$seen): void {
208+
if (!is_dir($dirPath)) {
209+
return;
210+
}
211+
212+
$iterator = new DirectoryIterator($dirPath);
213+
foreach ($iterator as $fileInfo) {
214+
if (!$fileInfo->isFile()) {
215+
continue;
216+
}
217+
218+
if (strtolower($fileInfo->getExtension()) !== 'json') {
219+
continue;
220+
}
221+
222+
$layoutId = pathinfo($fileInfo->getFilename(), PATHINFO_FILENAME);
223+
if ($layoutId === '') {
224+
continue;
225+
}
226+
227+
$label = $layoutId;
228+
$isValidJson = true;
229+
$contents = file_get_contents($fileInfo->getPathname());
230+
if ($contents !== false) {
231+
$decoded = json_decode($contents, true);
232+
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
233+
$isValidJson = false;
234+
} elseif (isset($decoded['name']) && is_string($decoded['name']) && $decoded['name'] !== '') {
235+
$label = $decoded['name'];
236+
}
237+
} else {
238+
$isValidJson = false;
239+
}
240+
241+
if ($isPrivateSource && !$isValidJson) {
242+
continue;
243+
}
244+
245+
if (isset($seen[$layoutId])) {
246+
$layouts[$seen[$layoutId]] = [
247+
'id' => $layoutId,
248+
'label' => $label,
249+
];
250+
continue;
251+
}
252+
253+
$layouts[] = [
254+
'id' => $layoutId,
255+
'label' => $label,
256+
];
257+
$seen[$layoutId] = count($layouts) - 1;
258+
}
259+
};
260+
261+
$readLayoutFile = static function (string $filePath, bool $isPrivateSource) use (&$layouts, &$seen): void {
262+
if (!is_file($filePath)) {
263+
return;
264+
}
265+
266+
if (strtolower((string) pathinfo($filePath, PATHINFO_EXTENSION)) !== 'json') {
267+
return;
268+
}
269+
270+
$layoutId = (string) pathinfo($filePath, PATHINFO_FILENAME);
271+
if ($layoutId === '') {
272+
return;
273+
}
274+
275+
$label = $layoutId;
276+
$isValidJson = true;
277+
$contents = file_get_contents($filePath);
278+
if ($contents !== false) {
279+
$decoded = json_decode($contents, true);
280+
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
281+
$isValidJson = false;
282+
} elseif (isset($decoded['name']) && is_string($decoded['name']) && $decoded['name'] !== '') {
283+
$label = $decoded['name'];
284+
}
285+
} else {
286+
$isValidJson = false;
287+
}
288+
289+
if ($isPrivateSource && !$isValidJson) {
290+
return;
291+
}
292+
293+
if (isset($seen[$layoutId])) {
294+
$layouts[$seen[$layoutId]] = [
295+
'id' => $layoutId,
296+
'label' => $label,
297+
];
298+
return;
299+
}
300+
301+
$layouts[] = [
302+
'id' => $layoutId,
303+
'label' => $label,
304+
];
305+
$seen[$layoutId] = count($layouts) - 1;
306+
};
307+
308+
$templateLayoutsDir = PathUtility::getAbsolutePath('template/collage');
309+
$templateOrientationDirs = [
310+
$templateLayoutsDir . DIRECTORY_SEPARATOR . 'landscape',
311+
$templateLayoutsDir . DIRECTORY_SEPARATOR . 'portrait',
312+
];
313+
314+
$privateLayoutsDir = PathUtility::getAbsolutePath('private/collage/layouts');
315+
$privateOrientationDirs = [
316+
$privateLayoutsDir . DIRECTORY_SEPARATOR . 'landscape',
317+
$privateLayoutsDir . DIRECTORY_SEPARATOR . 'portrait',
318+
];
319+
320+
$legacyPrivateCollageDir = PathUtility::getAbsolutePath('private/collage');
321+
$legacyPrivateOrientationDirs = [
322+
$legacyPrivateCollageDir . DIRECTORY_SEPARATOR . 'landscape',
323+
$legacyPrivateCollageDir . DIRECTORY_SEPARATOR . 'portrait',
324+
];
325+
$legacyPrivateRootCollageJson = PathUtility::getAbsolutePath('private/collage.json');
326+
327+
foreach ($templateOrientationDirs as $orientationDir) {
328+
$readLayoutsFromDir($orientationDir, false);
329+
}
330+
$readLayoutsFromDir($templateLayoutsDir, false);
331+
332+
foreach ($privateOrientationDirs as $orientationDir) {
333+
$readLayoutsFromDir($orientationDir, true);
334+
}
335+
$readLayoutsFromDir($privateLayoutsDir, true);
336+
337+
foreach ($legacyPrivateOrientationDirs as $orientationDir) {
338+
$readLayoutsFromDir($orientationDir, true);
339+
}
340+
$readLayoutsFromDir($legacyPrivateCollageDir, true);
341+
$readLayoutFile($legacyPrivateRootCollageJson, true);
342+
343+
foreach ($layouts as &$layout) {
344+
$layoutId = (string) $layout['id'];
345+
$layoutData = loadCollageLayoutData($layoutId, $orientation);
346+
if ($layoutData === null && $fallbackOrientation !== $orientation) {
347+
$layoutData = loadCollageLayoutData($layoutId, $fallbackOrientation);
348+
}
349+
if (is_array($layoutData) && isset($layoutData['name']) && is_string($layoutData['name']) && $layoutData['name'] !== '') {
350+
$layout['label'] = $layoutData['name'];
351+
}
352+
$layout['preview'] = buildCollageLayoutPreviewSvg($layoutId, $layoutData);
353+
}
354+
unset($layout);
355+
356+
usort($layouts, static function (array $left, array $right): int {
357+
return strnatcasecmp($left['label'], $right['label']);
358+
});
359+
360+
echo json_encode($layouts);
361+
362+
exit();

0 commit comments

Comments
 (0)