Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 732 |
|
0.00% |
0 / 33 |
CRAP | |
0.00% |
0 / 1 |
Panini2024Utils | |
0.00% |
0 / 732 |
|
0.00% |
0 / 33 |
22350 | |
0.00% |
0 / 1 |
parseSpec | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
56 | |||
renderSingle | |
0.00% |
0 / 183 |
|
0.00% |
0 / 1 |
812 | |||
render3x5Pages | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
156 | |||
render4x4Zip | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
42 | |||
getAllEntries | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
render4x4Pages | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
132 | |||
cachePictureId | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
getCachePathForPictureId | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getCachePathForZip | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getBookPdf | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
addBookPage | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
drawPlaceholder | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
drawEntryInfobox | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
72 | |||
drawText | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
convertString | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renderBookPages | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
20 | |||
getBookEntries | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
renderOlzPages | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
getOlzEntries | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getOlzPageXY | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
renderHistoryPages | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
getHistoryEntries | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getHistoryPageXY | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
2 | |||
renderDressesPages | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
getDressesEntries | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getDressesPageXY | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
renderMapsPages | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
42 | |||
getMapsEntries | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getMapsPageXY | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
2 | |||
renderBackPages | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
getBackEntries | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getBackPageXY | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
fromEnv | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Olz\Apps\Panini2024\Utils; |
4 | |
5 | use Olz\Entity\Panini2024\Panini2024Picture; |
6 | use Olz\Utils\WithUtilsTrait; |
7 | use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; |
8 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; |
9 | |
10 | class Panini2024Utils { |
11 | use WithUtilsTrait; |
12 | |
13 | public const DPI = 900; |
14 | public const MM_PER_INCH = 25.4; |
15 | |
16 | // This is the version with the self-printed foldable label paper: |
17 | // public const PANINI_SHORT = 42.8; // mm (50.8mm, 4mm margin) |
18 | // public const PANINI_LONG = 59; // mm (70mm, 5.5mm margin) |
19 | |
20 | // This is the version with 4x4 pictures per A4, which need to be cut: |
21 | public const PANINI_SHORT = 45; // mm |
22 | public const PANINI_LONG = 65; // mm |
23 | |
24 | public const ASSOCIATION_MAP = [ |
25 | 'Au ZH' => 'wappen/waedenswil.jpg', |
26 | 'Adliswil' => 'wappen/adliswil.jpg', |
27 | 'Hirzel' => 'wappen/hirzel.jpg', |
28 | 'Horgen' => 'wappen/horgen.jpg', |
29 | 'Kilchberg' => 'wappen/kilchberg.jpg', |
30 | 'Langnau am Albis' => 'wappen/langnau_am_albis.jpg', |
31 | 'Oberrieden' => 'wappen/oberrieden.jpg', |
32 | 'Richterswil' => 'wappen/richterswil.jpg', |
33 | 'Rüschlikon' => 'wappen/rueschlikon.jpg', |
34 | 'Samstagern' => 'wappen/richterswil.jpg', |
35 | 'Schönenberg' => 'wappen/waedenswil.jpg', |
36 | 'Thalwil' => 'wappen/thalwil.jpg', |
37 | 'Wädenswil' => 'wappen/waedenswil.jpg', |
38 | 'Zürich' => 'wappen/zuerich.jpg', |
39 | |
40 | 'Einsiedeln' => 'wappen/einsiedeln.jpg', |
41 | 'Küsnacht ZH' => 'wappen/kuesnacht_zh.jpg', |
42 | 'Oberwil' => 'wappen/daegerlen.jpg', |
43 | 'Maur' => 'wappen/maur.jpg', |
44 | 'Niederurnen GL' => 'wappen/niederurnen.jpg', |
45 | 'Pfäffikon ZH' => 'wappen/pfaeffikon_zh.jpg', |
46 | 'Basel' => 'wappen/basel.jpg', |
47 | 'Hausen am Albis' => 'wappen/hausen_am_albis.jpg', |
48 | 'Wollerau' => 'wappen/wollerau.jpg', |
49 | 'Riedikon' => 'wappen/uster.jpg', |
50 | 'Winterthur' => 'wappen/winterthur.jpg', |
51 | 'Landquart GR' => 'wappen/landquart_gr.jpg', |
52 | 'Wolhusen LU' => 'wappen/wolhusen_lu.jpg', |
53 | 'Wetzikon' => 'wappen/wetzikon.jpg', |
54 | 'Affoltern am Albis' => 'wappen/affoltern_am_albis.jpg', |
55 | 'Greifensee' => 'wappen/greifensee.jpg', |
56 | 'St. Gallen' => 'wappen/st_gallen.jpg', |
57 | 'Bern' => 'wappen/bern.jpg', |
58 | 'Seewen SZ' => 'wappen/seewen_sz.jpg', |
59 | ]; |
60 | |
61 | /** @return array{0: array<array{ids?: array<int>}>, 1: array{grid?: bool}} */ |
62 | public function parseSpec(string $spec, int $num_per_page): array { |
63 | $random_res = preg_match('/^random-([0-9]+)(-grid)?$/i', $spec, $random_matches); |
64 | if ($random_res) { |
65 | $num = intval($random_matches[1]); |
66 | $options = [ |
67 | 'grid' => ($random_matches[2] ?? '') === '-grid', |
68 | ]; |
69 | $panini_repo = $this->entityManager()->getRepository(Panini2024Picture::class); |
70 | $all_ids = array_map(function ($picture) { |
71 | return $picture->getId(); |
72 | }, $panini_repo->findAll()); |
73 | $ids_len = count($all_ids); |
74 | assert($ids_len > 0); |
75 | $pages = []; |
76 | for ($p = 0; $p < $num; $p++) { |
77 | $ids = []; |
78 | for ($i = 0; $i < 16; $i++) { |
79 | $id = $all_ids[random_int(0, $ids_len - 1)]; |
80 | $this->generalUtils()->checkNotNull($id, "ID was null"); |
81 | $ids[] = $id; |
82 | } |
83 | $pages[] = ['ids' => $ids]; |
84 | } |
85 | return [$pages, $options]; |
86 | } |
87 | $duplicate_res = preg_match('/^duplicate-([0-9]+)(-grid)?$/i', $spec, $duplicate_matches); |
88 | if ($duplicate_res) { |
89 | $id = intval($duplicate_matches[1]); |
90 | $ids = []; |
91 | for ($i = 0; $i < $num_per_page; $i++) { |
92 | $ids[] = $id; |
93 | } |
94 | $options = [ |
95 | 'grid' => ($duplicate_matches[2] ?? '') === '-grid', |
96 | ]; |
97 | $pages = [ |
98 | ['ids' => $ids], |
99 | ]; |
100 | return [$pages, $options]; |
101 | } |
102 | $pattern_param = $num_per_page - 1; |
103 | $pattern = "/^((?:[0-9]+,){{$pattern_param}}[0-9]+)(-grid)?$/i"; |
104 | $list_res = preg_match($pattern, $spec, $list_matches); |
105 | if ($list_res) { |
106 | $ids = array_map(function ($idstr) { |
107 | return intval($idstr); |
108 | }, explode(',', $list_matches[1])); |
109 | $options = [ |
110 | 'grid' => ($list_matches[2] ?? '') === '-grid', |
111 | ]; |
112 | $pages = [ |
113 | ['ids' => $ids], |
114 | ]; |
115 | return [$pages, $options]; |
116 | } |
117 | throw new NotFoundHttpException("Invalid spec: {$spec} ({$pattern})"); |
118 | } |
119 | |
120 | public function renderSingle(int $id): string { |
121 | $entity_manager = $this->dbUtils()->getEntityManager(); |
122 | $data_path = $this->envUtils()->getDataPath(); |
123 | $panini_path = "{$data_path}panini_data/"; |
124 | $masks_path = "{$panini_path}masks/"; |
125 | |
126 | $panini_repo = $entity_manager->getRepository(Panini2024Picture::class); |
127 | $picture = $panini_repo->findOneBy(['id' => $id]); |
128 | if (!$picture) { |
129 | throw new NotFoundHttpException("Kein solches Panini vorhanden"); |
130 | } |
131 | $owner = $picture->getOwnerUser(); |
132 | $is_landscape = $picture->getIsLandscape(); |
133 | $has_top = $picture->getHasTop(); |
134 | $lp_suffix = $is_landscape ? 'L' : 'P'; |
135 | $img_src = $picture->getImgSrc(); |
136 | $img_style = $picture->getImgStyle(); |
137 | $line1 = $picture->getLine1(); |
138 | $line2 = $picture->getLine2(); |
139 | $association = $picture->getAssociation(); |
140 | $res_wid_percent = preg_match('/width\:\s*([\-0-9\.]+)%/i', $img_style, $matches); |
141 | $img_wid_percent = floatval($res_wid_percent ? $matches[1] : 100); |
142 | $res_top_percent = preg_match('/top\:\s*([\-0-9\.]+)%/i', $img_style, $matches); |
143 | $img_top_percent = floatval($res_top_percent ? $matches[1] : 100); |
144 | $res_left_percent = preg_match('/left\:\s*([\-0-9\.]+)%/i', $img_style, $matches); |
145 | $img_left_percent = floatval($res_left_percent ? $matches[1] : 100); |
146 | |
147 | $has_panini = $this->authUtils()->hasPermission('panini2024'); |
148 | $current_user = $this->authUtils()->getCurrentUser(); |
149 | $is_mine = $owner && $current_user && ($owner->getId() === $current_user->getId()); |
150 | if (!$has_panini && !$is_mine) { |
151 | throw new AccessDeniedHttpException("Kein Zugriff"); |
152 | } |
153 | |
154 | $wid = max(1, intval(round(($is_landscape ? self::PANINI_LONG : self::PANINI_SHORT) |
155 | * self::DPI / self::MM_PER_INCH))); |
156 | $hei = max(1, intval(round(($is_landscape ? self::PANINI_SHORT : self::PANINI_LONG) |
157 | * self::DPI / self::MM_PER_INCH))); |
158 | $suffix = "{$lp_suffix}_{$wid}x{$hei}"; |
159 | $payload_folder = (intval($id) >= 1000) ? "portraits/{$id}/" : ''; |
160 | $payload_path = "{$panini_path}{$payload_folder}{$img_src}"; |
161 | $bottom_mask_path = "{$masks_path}bottom{$suffix}.png"; |
162 | $top_mask_path = "{$masks_path}top{$suffix}.png"; |
163 | $association_mask_path = "{$masks_path}association{$suffix}.png"; |
164 | $flag_mask_path = "{$masks_path}associationStencil{$suffix}.png"; |
165 | $association_file = self::ASSOCIATION_MAP[$association] ?? null; |
166 | $association_img_orig_path = "{$panini_path}{$association_file}"; |
167 | |
168 | $ident = json_encode([ |
169 | $is_landscape, |
170 | $has_top, |
171 | $img_src, |
172 | $img_style, |
173 | $line1, |
174 | $line2, |
175 | $association, |
176 | filemtime($payload_path), |
177 | filemtime($bottom_mask_path), |
178 | filemtime($top_mask_path), |
179 | filemtime($association_mask_path), |
180 | filemtime($flag_mask_path), |
181 | filemtime($association_img_orig_path), |
182 | md5(file_get_contents(__FILE__) ?: ''), |
183 | ]) ?: '[]'; |
184 | $md5 = md5($ident); |
185 | $cache_file = "{$panini_path}cache/{$id}-{$md5}.jpg"; |
186 | if (is_file($cache_file)) { |
187 | $this->log()->info("Read from cache: {$id}-{$md5}.jpg"); |
188 | return file_get_contents($cache_file) ?: ''; |
189 | } |
190 | |
191 | $img = imagecreatetruecolor($wid, $hei); |
192 | $this->generalUtils()->checkNotFalse($img, "renderSingle: Could not create img"); |
193 | gc_collect_cycles(); |
194 | |
195 | // Payload |
196 | $payload_img = imagecreatefromjpeg($payload_path); |
197 | if ($payload_img) { |
198 | $payload_wid = imagesx($payload_img); |
199 | $payload_hei = imagesy($payload_img); |
200 | imagecopyresampled( |
201 | $img, |
202 | $payload_img, |
203 | intval(round($img_left_percent * $wid / 100)), |
204 | intval(round($img_top_percent * $hei / 100)), |
205 | 0, |
206 | 0, |
207 | intval(round($wid * $img_wid_percent / 100)), |
208 | intval(round($wid * $img_wid_percent * $payload_hei / $payload_wid / 100)), |
209 | $payload_wid, |
210 | $payload_hei, |
211 | ); |
212 | imagedestroy($payload_img); |
213 | gc_collect_cycles(); |
214 | } |
215 | |
216 | // Masks |
217 | $bottom_mask = imagecreatefrompng($bottom_mask_path); |
218 | $this->generalUtils()->checkNotFalse($bottom_mask, "renderSingle: Could not read bottom_mask"); |
219 | imagecopy($img, $bottom_mask, 0, 0, 0, 0, $wid, $hei); |
220 | imagedestroy($bottom_mask); |
221 | gc_collect_cycles(); |
222 | if ($has_top) { |
223 | $top_mask = imagecreatefrompng($top_mask_path); |
224 | $this->generalUtils()->checkNotFalse($top_mask, "renderSingle: Could not create top_mask"); |
225 | imagecopy($img, $top_mask, 0, 0, 0, 0, $wid, $hei); |
226 | imagedestroy($top_mask); |
227 | gc_collect_cycles(); |
228 | } |
229 | |
230 | // Association |
231 | if ($association_file) { |
232 | $association_mask = imagecreatefrompng($association_mask_path); |
233 | $this->generalUtils()->checkNotFalse($association_mask, "renderSingle: Could not create association_mask"); |
234 | imagecopy($img, $association_mask, 0, 0, 0, 0, $wid, $hei); |
235 | imagedestroy($association_mask); |
236 | gc_collect_cycles(); |
237 | |
238 | $offset = intval(round(($wid + $hei) * 0.01) - 1); |
239 | $size = max(1, intval(round(($wid + $hei) * 0.09) + 1)); |
240 | |
241 | $flag_mask = imagecreatefrompng($flag_mask_path); |
242 | $this->generalUtils()->checkNotFalse($flag_mask, "renderSingle: Could not create flag_mask"); |
243 | |
244 | $association_img = imagecreatetruecolor($size, $size); |
245 | $this->generalUtils()->checkNotFalse($association_img, "renderSingle: Could not create association_img"); |
246 | $association_img_orig = imagecreatefromjpeg($association_img_orig_path); |
247 | $this->generalUtils()->checkNotFalse($association_img_orig, "renderSingle: Could not read association_img_orig"); |
248 | imagecopyresampled( |
249 | $association_img, |
250 | $association_img_orig, |
251 | 0, |
252 | 0, |
253 | 0, |
254 | 0, |
255 | $size, |
256 | $size, |
257 | imagesx($association_img_orig), |
258 | imagesy($association_img_orig), |
259 | ); |
260 | imagedestroy($association_img_orig); |
261 | gc_collect_cycles(); |
262 | |
263 | for ($x = 0; $x < $size; $x++) { |
264 | for ($y = 0; $y < $size; $y++) { |
265 | $flag_color_at = imagecolorat($flag_mask, $x + $offset, $y + $offset); |
266 | $this->generalUtils()->checkNotFalse($flag_color_at, "renderSingle: Could not get flag_color_at"); |
267 | $mask = imagecolorsforindex($flag_mask, $flag_color_at); |
268 | if ($mask['red'] > 0) { |
269 | $ratio = floatval($mask['red']) / 255.0; |
270 | $association_color_at = imagecolorat($association_img, $x, $y); |
271 | $this->generalUtils()->checkNotFalse($association_color_at, "renderSingle: Could not get association_color_at"); |
272 | $src = imagecolorsforindex($association_img, $association_color_at); |
273 | $src_r = floatval($src['red']); |
274 | $src_g = floatval($src['green']); |
275 | $src_b = floatval($src['blue']); |
276 | $color_at = imagecolorat($img, $x + $offset, $y + $offset); |
277 | $this->generalUtils()->checkNotFalse($color_at, "renderSingle: Could not get color_at"); |
278 | $dst = imagecolorsforindex($img, $color_at); |
279 | $dst_r = floatval($dst['red']); |
280 | $dst_g = floatval($dst['green']); |
281 | $dst_b = floatval($dst['blue']); |
282 | $color = imagecolorallocate( |
283 | $img, |
284 | max(0, min(255, intval($src_r * $ratio + $dst_r * (1 - $ratio)))), |
285 | max(0, min(255, intval($src_g * $ratio + $dst_g * (1 - $ratio)))), |
286 | max(0, min(255, intval($src_b * $ratio + $dst_b * (1 - $ratio)))), |
287 | ); |
288 | $this->generalUtils()->checkNotFalse($color, "renderSingle: Could not allocate color"); |
289 | imagesetpixel($img, $x + $offset, $y + $offset, $color); |
290 | imagecolordeallocate($img, $color); |
291 | } |
292 | } |
293 | } |
294 | imagedestroy($flag_mask); |
295 | imagedestroy($association_img); |
296 | gc_collect_cycles(); |
297 | } |
298 | |
299 | // Text |
300 | $size = ($hei + ($is_landscape ? $wid : $hei)) * 0.018; |
301 | $yellow = imagecolorallocate($img, 255, 255, 0); |
302 | $this->generalUtils()->checkNotFalse($yellow, "renderSingle: Could not create yellow"); |
303 | $shady = imagecolorallocatealpha($img, 0, 0, 0, 64); |
304 | $this->generalUtils()->checkNotFalse($shady, "renderSingle: Could not create shady"); |
305 | $shoff = $size / 10; |
306 | $font_path = "{$panini_path}fonts/OpenSans/OpenSans-SemiBold.ttf"; |
307 | $box = imagettfbbox($size, 0, $font_path, $line1); |
308 | $textwid = $box[2] ?? 0; |
309 | $x = ($line2 ? $wid * 0.95 - $textwid : $wid / 2 - $textwid / 2); |
310 | $y = $hei * ($is_landscape ? 0.95 : ($line2 ? 0.915 : 0.945)); |
311 | $this->drawText($img, $size, 0, $x + $shoff, $y + $shoff, $shady, $font_path, $line1); |
312 | $this->drawText($img, $size, 0, $x, $y, $yellow, $font_path, $line1); |
313 | if ($line2) { |
314 | $box = imagettfbbox($size, 0, $font_path, $line2); |
315 | $textwid = $box[2] ?? 0; |
316 | $x = $wid * 0.95 - $textwid; |
317 | $y = $hei * 0.975; |
318 | $this->drawText($img, $size, 0, $x + $shoff, $y + $shoff, $shady, $font_path, $line2); |
319 | $this->drawText($img, $size, 0, $x, $y, $yellow, $font_path, $line2); |
320 | } |
321 | gc_collect_cycles(); |
322 | |
323 | ob_start(); |
324 | imagejpeg($img, null, 90); |
325 | $image_data = ob_get_contents(); |
326 | ob_end_clean(); |
327 | imagedestroy($img); |
328 | $this->generalUtils()->checkNotFalse($image_data, "Could not retrieve image data"); |
329 | file_put_contents($cache_file, $image_data); |
330 | $this->log()->info("Written to cache: {$id}-{$md5}.jpg"); |
331 | return $image_data; |
332 | } |
333 | |
334 | /** |
335 | * @param array<array{ids?: array<int>}> $pages |
336 | * @param array{grid?: bool} $options |
337 | */ |
338 | public function render3x5Pages(array $pages, array $options): string { |
339 | if (!$this->authUtils()->hasPermission('panini2024')) { |
340 | throw new NotFoundHttpException(); |
341 | } |
342 | |
343 | $grid = (bool) ($options['grid'] ?? false); |
344 | $x_step = 70; |
345 | $x_margin = 5.5; |
346 | $x_offset = 0; |
347 | $y_step = 50.8; |
348 | $y_offset = 21.5; |
349 | $y_margin = 4; |
350 | |
351 | foreach ($pages as $page) { |
352 | $ids = $page['ids'] ?? []; |
353 | foreach ($ids as $id) { |
354 | $this->cachePictureId($id); |
355 | } |
356 | } |
357 | |
358 | $pdf = new \TCPDF('P', 'mm', 'A4'); |
359 | $pdf->setAutoPageBreak(false, 0); |
360 | $pdf->setPrintHeader(false); |
361 | $pdf->setPrintFooter(false); |
362 | foreach ($pages as $page) { |
363 | $ids = $page['ids'] ?? []; |
364 | $pdf->AddPage(); |
365 | |
366 | if ($grid) { |
367 | $pdf->SetDrawColor(0, 0, 0); |
368 | $pdf->SetLineWidth(0.1); |
369 | for ($x = 1; $x < 3; $x++) { |
370 | $pdf->Line( |
371 | $x_offset + $x_step * $x, |
372 | $y_offset + $y_step * 0, |
373 | $x_offset + $x_step * $x, |
374 | $y_offset + $y_step * 5, |
375 | ); |
376 | } |
377 | for ($y = 0; $y < 6; $y++) { |
378 | $pdf->Line( |
379 | $x_offset + $x_step * 0, |
380 | $y_offset + $y_step * $y, |
381 | $x_offset + $x_step * 3, |
382 | $y_offset + $y_step * $y, |
383 | ); |
384 | } |
385 | } |
386 | |
387 | $index = 0; |
388 | for ($y = 0; $y < 5; $y++) { |
389 | for ($x = 0; $x < 3; $x++) { |
390 | if ($y !== 2) { |
391 | $id = $ids[$index] ?? 0; |
392 | $temp_file_path = $this->getCachePathForPictureId($id); |
393 | if ($temp_file_path) { |
394 | $pdf->Image( |
395 | $temp_file_path, |
396 | $x_offset + $x_margin + $x_step * $x, |
397 | $y_offset + $y_margin + $y_step * $y, |
398 | $x_step - $x_margin * 2, |
399 | ); |
400 | } |
401 | $index++; |
402 | } |
403 | } |
404 | } |
405 | } |
406 | return $pdf->Output('3x5.pdf', 'S'); |
407 | } |
408 | |
409 | /** @param array{grid?: bool} $options */ |
410 | public function render4x4Zip(array $options): string { |
411 | $grid_or_empty = ($options['grid'] ?? false) ? '-grid' : ''; |
412 | $ids = $this->getAllEntries(); |
413 | |
414 | $panini_utils = Panini2024Utils::fromEnv(); |
415 | $zip = new \ZipArchive(); |
416 | $zip_path = $panini_utils->getCachePathForZip("duplicates{$grid_or_empty}"); |
417 | if ($zip->open($zip_path, \ZipArchive::CREATE) !== true) { |
418 | throw new \Exception("Could not open Zip"); |
419 | } |
420 | foreach ($ids as $id) { |
421 | $spec = "duplicate-{$id}{$grid_or_empty}"; |
422 | $pdf_out = null; |
423 | [$pages, $options] = $this->parseSpec($spec, /* num_per_page= */ 16); |
424 | $pdf_out = $panini_utils->render4x4Pages($pages, $options); |
425 | if (!$pdf_out) { |
426 | throw new \Exception("PDF generation failed for ID: {$id}"); |
427 | } |
428 | $zip->addFromString("{$spec}.pdf", $pdf_out); |
429 | gc_collect_cycles(); |
430 | } |
431 | $zip->close(); |
432 | |
433 | $content = file_get_contents($zip_path) ?: ''; |
434 | @unlink($zip_path); |
435 | return $content; |
436 | } |
437 | |
438 | /** @return array<int> */ |
439 | private function getAllEntries(): array { |
440 | $ids = []; |
441 | |
442 | $db = $this->dbUtils()->getDb(); |
443 | $result_olz = $db->query("SELECT id FROM panini24 ORDER BY id ASC"); |
444 | // @phpstan-ignore-next-line |
445 | for ($i = 0; $i < $result_olz->num_rows; $i++) { |
446 | // @phpstan-ignore-next-line |
447 | $row_olz = $result_olz->fetch_assoc(); |
448 | // @phpstan-ignore-next-line |
449 | $ids[] = intval($row_olz['id']); |
450 | } |
451 | return $ids; |
452 | } |
453 | |
454 | /** |
455 | * @param array<array{ids?: array<int>}> $pages |
456 | * @param array{grid?: bool} $options |
457 | */ |
458 | public function render4x4Pages(array $pages, array $options): string { |
459 | if (!$this->authUtils()->hasPermission('panini2024')) { |
460 | throw new NotFoundHttpException(); |
461 | } |
462 | |
463 | $grid = (bool) ($options['grid'] ?? false); |
464 | $x_step = 48; |
465 | $x_margin = 2; |
466 | $x_offset = 9; |
467 | $y_step = 68; |
468 | $y_offset = 12.5; |
469 | $y_margin = 2; |
470 | |
471 | foreach ($pages as $page) { |
472 | $ids = $page['ids'] ?? []; |
473 | foreach (array_unique($ids) as $id) { |
474 | $this->cachePictureId($id); |
475 | } |
476 | } |
477 | |
478 | $pdf = new \TCPDF('P', 'mm', 'A4'); |
479 | $pdf->setAutoPageBreak(false, 0); |
480 | $pdf->setPrintHeader(false); |
481 | $pdf->setPrintFooter(false); |
482 | foreach ($pages as $page) { |
483 | $ids = $page['ids'] ?? []; |
484 | $pdf->AddPage(); |
485 | |
486 | if ($grid) { |
487 | $pdf->SetDrawColor(0, 0, 0); |
488 | $pdf->SetLineWidth(0.1); |
489 | for ($x = 0; $x < 5; $x++) { |
490 | $line_x = $x_offset + $x_step * $x; |
491 | $pdf->Line($line_x, 0, $line_x, $y_offset - 1); |
492 | $pdf->Line($line_x, 297, $line_x, 297 - $y_offset + 1); |
493 | } |
494 | for ($y = 0; $y < 5; $y++) { |
495 | $line_y = $y_offset + $y_step * $y; |
496 | $pdf->Line(0, $line_y, $x_offset - 1, $line_y); |
497 | $pdf->Line(210, $line_y, 210 - $x_offset + 1, $line_y); |
498 | } |
499 | } |
500 | |
501 | $index = 0; |
502 | for ($y = 0; $y < 4; $y++) { |
503 | for ($x = 0; $x < 4; $x++) { |
504 | $id = $ids[$index] ?? 0; |
505 | $temp_file_path = $this->getCachePathForPictureId($id); |
506 | if ($temp_file_path) { |
507 | $pdf->Image( |
508 | $temp_file_path, |
509 | $x_offset + $x_margin + $x_step * $x, |
510 | $y_offset + $y_margin + $y_step * $y, |
511 | $x_step - $x_margin * 2, |
512 | ); |
513 | } |
514 | $index++; |
515 | } |
516 | } |
517 | } |
518 | return $pdf->Output('4x4.pdf', 'S'); |
519 | } |
520 | |
521 | private function cachePictureId(int $id): void { |
522 | if ($id === 0) { |
523 | return; |
524 | } |
525 | $temp_file_path = $this->getCachePathForPictureId($id); |
526 | $img = imagecreatefromstring($this->renderSingle($id)); |
527 | $this->generalUtils()->checkNotFalse($img, "cachePictureId: Could create image from string (ID:{$id})"); |
528 | $wid = imagesx($img); |
529 | $hei = imagesy($img); |
530 | if ($hei < $wid) { |
531 | $img = imagerotate($img, 90, 0); |
532 | $this->generalUtils()->checkNotFalse($img, "cachePictureId: Failed rotating"); |
533 | } |
534 | imagejpeg($img, $temp_file_path); |
535 | imagedestroy($img); |
536 | gc_collect_cycles(); |
537 | } |
538 | |
539 | private function getCachePathForPictureId(int $id): ?string { |
540 | if ($id === 0) { |
541 | return null; |
542 | } |
543 | $data_path = $this->envUtils()->getDataPath(); |
544 | $temp_path = "{$data_path}temp/"; |
545 | if (!is_dir($temp_path)) { |
546 | mkdir($temp_path, 0o777, true); |
547 | } |
548 | return "{$temp_path}paninipdf-{$id}.jpg"; |
549 | } |
550 | |
551 | public function getCachePathForZip(string $ident): string { |
552 | $data_path = $this->envUtils()->getDataPath(); |
553 | $temp_path = "{$data_path}temp/"; |
554 | if (!is_dir($temp_path)) { |
555 | mkdir($temp_path, 0o777, true); |
556 | } |
557 | return "{$temp_path}paninizip-{$ident}.zip"; |
558 | } |
559 | |
560 | // --- BOOK ------------------------------------------------------------------------------------ |
561 | |
562 | private function getBookPdf(): \TCPDF { |
563 | $data_path = $this->envUtils()->getDataPath(); |
564 | $panini_path = "{$data_path}panini_data/"; |
565 | |
566 | $pdf = new \TCPDF('P', 'mm', 'A4'); |
567 | $pdf->setAutoPageBreak(false, 0); |
568 | $pdf->setPrintHeader(false); |
569 | $pdf->setPrintFooter(false); |
570 | |
571 | $font_path = "{$panini_path}fonts/OpenSans/OpenSans-SemiBold.ttf"; |
572 | $fontname = \TCPDF_FONTS::addTTFfont($font_path, 'TrueTypeUnicode'); |
573 | if (!$fontname) { |
574 | throw new \Exception("Error with font {$font_path}"); |
575 | } |
576 | $pdf->SetFont($fontname); |
577 | |
578 | return $pdf; |
579 | } |
580 | |
581 | private function addBookPage(\TCPDF $pdf): void { |
582 | $pdf->AddPage(); |
583 | $mainbg_path = __DIR__.'/../../../../assets/icns/mainbg.png'; |
584 | $info = getimagesize($mainbg_path); |
585 | $wid_px = $info[0] ?? 0; |
586 | $hei_px = $info[1] ?? 0; |
587 | $dpi = 150; |
588 | $wid_mm = $wid_px / $dpi * self::MM_PER_INCH; |
589 | $hei_mm = $hei_px / $dpi * self::MM_PER_INCH; |
590 | for ($tile_x = 0; $tile_x < ceil(210 / $wid_mm); $tile_x++) { |
591 | for ($tile_y = 0; $tile_y < ceil(297 / $hei_mm); $tile_y++) { |
592 | $pdf->Image( |
593 | $mainbg_path, |
594 | $tile_x * $wid_mm, |
595 | $tile_y * $hei_mm, |
596 | $wid_mm, |
597 | ); |
598 | } |
599 | } |
600 | } |
601 | |
602 | /** @param array<string, mixed> $entry */ |
603 | private function drawPlaceholder( |
604 | \TCPDF $pdf, |
605 | array $entry, |
606 | float $x, |
607 | float $y, |
608 | float $wid, |
609 | float $hei, |
610 | ): void { |
611 | $is_landcape = $wid > $hei; |
612 | |
613 | $pdf->SetLineWidth(0.1); |
614 | $pdf->SetDrawColor(200, 200, 200); |
615 | $pdf->SetFillColor(255, 255, 255); |
616 | $pdf->Rect($x, $y, $wid, $hei, 'DF'); |
617 | |
618 | $pdf->SetTextColor(200, 200, 200); |
619 | $pdf->SetFontSize($is_landcape ? 7.75 : 8.75); |
620 | $line1 = $this->convertString($entry['line1']); |
621 | $line2 = $this->convertString($entry['line2']); |
622 | $has_line2 = $line2 !== ''; |
623 | $align = $has_line2 ? 'R' : 'C'; |
624 | $pdf->setXY($x, $y + $hei - ($has_line2 ? 10 : 8)); |
625 | $pdf->Cell($wid, 5, $line1, 0, 0, $align); |
626 | if ($has_line2) { |
627 | $pdf->setXY($x, $y + $hei - 6); |
628 | $pdf->Cell($wid, 5, $line2, 0, 0, $align); |
629 | } |
630 | } |
631 | |
632 | /** @param array<string, mixed> $entry */ |
633 | private function drawEntryInfobox( |
634 | \TCPDF $pdf, |
635 | array $entry, |
636 | float $x, |
637 | float $y, |
638 | float $wid, |
639 | float $hei, |
640 | ): void { |
641 | $pdf->SetLineWidth(0.1); |
642 | $pdf->SetDrawColor(0, 117, 33); |
643 | $pdf->SetFillColor(212, 231, 206); |
644 | $pdf->Rect($x, $y, $wid, $hei, 'F'); |
645 | $line_y = $y; |
646 | $pdf->Line($x, $line_y, $x + $wid, $line_y); |
647 | $line_y = $y + $hei; |
648 | $pdf->Line($x, $line_y, $x + $wid, $line_y); |
649 | |
650 | $pdf->SetFontSize(11); |
651 | $birthday = $entry['birthdate'] ?? ''; |
652 | if (substr($birthday, 4) === '-00-00') { |
653 | $birthday = substr($birthday, 0, 4); |
654 | } |
655 | if ($entry['birthdate'] === null) { // No information => ERROR! |
656 | $pdf->SetTextColor(255, 0, 0); |
657 | $birthday = '!!!'; |
658 | } elseif (substr($birthday, 0, 4) === '0000') { // Year Zero => Do not show |
659 | $birthday = ''; |
660 | } else { |
661 | $pdf->SetTextColor(0, 117, 33); |
662 | if (strlen($birthday) === 10) { |
663 | $birthday = date('d.m.Y', strtotime($birthday) ?: 0); |
664 | } |
665 | } |
666 | $pdf->setXY($x, $y + 1); |
667 | $pdf->Cell($wid * 0.7 - 2, 5, $birthday); |
668 | |
669 | $pdf->SetFontSize(14); |
670 | $num_mispunch = strval($entry['num_mispunches']); |
671 | if ($entry['num_mispunches'] === null) { // No information => ERROR! |
672 | $pdf->SetTextColor(255, 0, 0); |
673 | $num_mispunch = '!!!'; |
674 | } elseif (intval($entry['num_mispunches']) < 0) { // Negative count => Do not show |
675 | $num_mispunch = ''; |
676 | } else { |
677 | $pdf->SetTextColor(0, 117, 33); |
678 | } |
679 | $pdf->setXY($x + $wid * 0.7, $y + 1); |
680 | $pdf->Cell($wid * 0.3, 5, $num_mispunch, 0, 0, 'R'); |
681 | |
682 | $pdf->SetTextColor(0, 117, 33); |
683 | $pdf->SetFontSize(8); |
684 | $infos = json_decode($entry['infos'], true) ?? []; |
685 | $favourite_map = $this->convertString($infos[0] ?? ''); |
686 | $pdf->setXY($x, $y + 6); |
687 | $pdf->Cell($wid, 4, $favourite_map); |
688 | $since_when = $this->convertString($infos[3] ?? ''); |
689 | $pdf->setXY($x, $y + 10); |
690 | $pdf->Cell($wid, 4, $since_when); |
691 | $motto = $this->convertString($infos[4] ?? ''); |
692 | $pdf->setXY($x, $y + 14); |
693 | $pdf->Multicell($wid, 11, $motto, 0, 'L'); |
694 | } |
695 | |
696 | /** @param array{} $options */ |
697 | private function drawText( |
698 | \GdImage $image, |
699 | float $size, |
700 | float $angle, |
701 | int|float $x, |
702 | int|float $y, |
703 | int $color, |
704 | string $font_filename, |
705 | string $text, |
706 | array $options = [], |
707 | ): void { |
708 | $x = intval(round($x)); |
709 | $y = intval(round($y)); |
710 | imagettftext($image, $size, $angle, $x, $y, $color, $font_filename, $text, $options); |
711 | } |
712 | |
713 | private function convertString(?string $string): string { |
714 | return $string ?? ''; |
715 | } |
716 | |
717 | public function renderBookPages(): string { |
718 | if (!$this->authUtils()->hasPermission('panini2024')) { |
719 | throw new NotFoundHttpException(); |
720 | } |
721 | $pdf = $this->getBookPdf(); |
722 | $entries = $this->getBookEntries(); |
723 | |
724 | $x_step = 46; |
725 | $x_margin = 1; |
726 | $x_offset = 13; |
727 | $y_step = 92; |
728 | $y_offset = 10.5; |
729 | $y_margin = 1; |
730 | $y_box = 26; |
731 | |
732 | $index = 0; |
733 | foreach ($entries as $entry) { |
734 | if (($index % 12) === 0) { |
735 | $this->addBookPage($pdf); |
736 | } |
737 | $x_index = $index % 4; |
738 | $y_index = floor($index / 4) % 3; |
739 | |
740 | $x = $x_offset + $x_margin + $x_step * $x_index; |
741 | $y = $y_offset + $y_margin + $y_step * $y_index; |
742 | $wid = $x_step - $x_margin * 2; |
743 | |
744 | $placeholder_hei = $y_step - $y_box - $y_margin * 2; |
745 | $this->drawPlaceholder($pdf, $entry, $x, $y, $wid, $placeholder_hei); |
746 | |
747 | $box_y = $y + $y_step - $y_box - $y_margin; |
748 | $box_hei = $y_box - $y_margin; |
749 | $this->drawEntryInfobox($pdf, $entry, $x, $box_y, $wid, $box_hei); |
750 | |
751 | $index++; |
752 | } |
753 | return $pdf->Output('book.pdf', 'S'); |
754 | } |
755 | |
756 | /** @return array<array<string, mixed>> */ |
757 | private function getBookEntries(): array { |
758 | $entries = []; |
759 | |
760 | $db = $this->dbUtils()->getDb(); |
761 | $result_associations = $db->query("SELECT *, (img_src = 'wappen/other.jpg') AS is_other FROM panini24 WHERE img_src LIKE 'wappen/%' ORDER BY is_other ASC, line1 ASC"); |
762 | $esc_associations = []; |
763 | // @phpstan-ignore-next-line |
764 | for ($i = 0; $i < $result_associations->num_rows; $i++) { |
765 | // @phpstan-ignore-next-line |
766 | $row_association = $result_associations->fetch_assoc(); |
767 | $entries[] = $row_association; |
768 | |
769 | if ($row_association['is_other'] ?? false) { |
770 | $sql = implode("', '", $esc_associations); |
771 | $result_portraits = $db->query("SELECT * FROM panini24 WHERE association NOT IN ('{$sql}') ORDER BY line2 ASC, line1 ASC"); |
772 | // @phpstan-ignore-next-line |
773 | for ($j = 0; $j < $result_portraits->num_rows; $j++) { |
774 | // @phpstan-ignore-next-line |
775 | $row_portrait = $result_portraits->fetch_assoc(); |
776 | $entries[] = $row_portrait; |
777 | } |
778 | } else { |
779 | $line1 = $row_association['line1'] ?? ''; |
780 | $esc_association = $db->real_escape_string("{$line1}"); |
781 | $esc_associations[] = $esc_association; |
782 | $result_portraits = $db->query("SELECT * FROM panini24 WHERE association = '{$esc_association}' ORDER BY line2 ASC, line1 ASC"); |
783 | // @phpstan-ignore-next-line |
784 | for ($j = 0; $j < $result_portraits->num_rows; $j++) { |
785 | // @phpstan-ignore-next-line |
786 | $row_portrait = $result_portraits->fetch_assoc(); |
787 | $entries[] = $row_portrait; |
788 | } |
789 | } |
790 | } |
791 | // @phpstan-ignore-next-line |
792 | return $entries; |
793 | } |
794 | |
795 | public function renderOlzPages(): string { |
796 | if (!$this->authUtils()->hasPermission('panini2024')) { |
797 | throw new NotFoundHttpException(); |
798 | } |
799 | $pdf = $this->getBookPdf(); |
800 | $entries = $this->getOlzEntries(); |
801 | |
802 | $index = 0; |
803 | $last_page = 0; |
804 | foreach ($entries as $entry) { |
805 | [$page, $x, $y] = $this->getOlzPageXY($index); |
806 | for ($p = $last_page; $p < $page; $p++) { |
807 | $this->addBookPage($pdf); |
808 | } |
809 | $last_page = $page; |
810 | |
811 | $short = 44; |
812 | $long = 64; |
813 | $wid = $entry['is_landscape'] ? $long : $short; |
814 | $hei = $entry['is_landscape'] ? $short : $long; |
815 | |
816 | $this->drawPlaceholder($pdf, $entry, $x - $wid / 2, $y - $hei / 2, $wid, $hei); |
817 | |
818 | $index++; |
819 | } |
820 | return $pdf->Output('olz.pdf', 'S'); |
821 | } |
822 | |
823 | /** @return array<array<string, mixed>> */ |
824 | private function getOlzEntries(): array { |
825 | $entries = []; |
826 | |
827 | $db = $this->dbUtils()->getDb(); |
828 | $result_olz = $db->query("SELECT * FROM panini24 WHERE id >= 10 AND id < 20 ORDER BY id ASC"); |
829 | // @phpstan-ignore-next-line |
830 | for ($i = 0; $i < $result_olz->num_rows; $i++) { |
831 | // @phpstan-ignore-next-line |
832 | $row_olz = $result_olz->fetch_assoc(); |
833 | $entries[] = $row_olz; |
834 | } |
835 | // @phpstan-ignore-next-line |
836 | return $entries; |
837 | } |
838 | |
839 | /** @return array{0: int, 1: float, 2: float} */ |
840 | private function getOlzPageXY(int $index): array { |
841 | $a4_wid = 210; |
842 | $olz = [ |
843 | [1, $a4_wid * 0.75, 10.5 + 22], |
844 | [1, $a4_wid * 0.25, 10.5 + 44 + 10.5 + 32], |
845 | [1, $a4_wid * 0.8, 10.5 + 44 + 10.5 + 32], |
846 | [1, $a4_wid * 0.2, 10.5 + 44 + 10.5 + 64 + 10.5 + 32], |
847 | [2, $a4_wid * 0.8, 10.5 + 44 + 10.5 + 64 + 10.5 + 64 + 10.5 + 32], |
848 | [2, $a4_wid * 0.25, 10.5 + 22], |
849 | [2, $a4_wid * 0.75, 10.5 + 44 + 10.5 + 32], |
850 | [2, $a4_wid * 0.2, 10.5 + 44 + 10.5 + 64 + 10.5 + 32], |
851 | [2, $a4_wid * 0.25, 10.5 + 44 + 10.5 + 64 + 10.5 + 64 + 10.5 + 32], |
852 | ]; |
853 | return $olz[$index]; |
854 | } |
855 | |
856 | public function renderHistoryPages(): string { |
857 | if (!$this->authUtils()->hasPermission('panini2024')) { |
858 | throw new NotFoundHttpException(); |
859 | } |
860 | $pdf = $this->getBookPdf(); |
861 | $entries = $this->getHistoryEntries(); |
862 | |
863 | $index = 0; |
864 | $last_page = 0; |
865 | foreach ($entries as $entry) { |
866 | [$page, $x, $y] = $this->getHistoryPageXY($index); |
867 | for ($p = $last_page; $p < $page; $p++) { |
868 | $this->addBookPage($pdf); |
869 | } |
870 | $last_page = $page; |
871 | |
872 | $short = 44; |
873 | $long = 64; |
874 | $wid = $entry['is_landscape'] ? $long : $short; |
875 | $hei = $entry['is_landscape'] ? $short : $long; |
876 | |
877 | $this->drawPlaceholder($pdf, $entry, $x - $wid / 2, $y - $hei / 2, $wid, $hei); |
878 | |
879 | $index++; |
880 | } |
881 | return $pdf->Output('history.pdf', 'S'); |
882 | } |
883 | |
884 | /** @return array<array<string, mixed>> */ |
885 | private function getHistoryEntries(): array { |
886 | $entries = []; |
887 | |
888 | $db = $this->dbUtils()->getDb(); |
889 | $result_olz = $db->query("SELECT * FROM panini24 WHERE id >= 50 AND id < 100 ORDER BY id ASC"); |
890 | // @phpstan-ignore-next-line |
891 | for ($i = 0; $i < $result_olz->num_rows; $i++) { |
892 | // @phpstan-ignore-next-line |
893 | $row_olz = $result_olz->fetch_assoc(); |
894 | $entries[] = $row_olz; |
895 | } |
896 | // @phpstan-ignore-next-line |
897 | return $entries; |
898 | } |
899 | |
900 | /** @return array{0: int, 1: float, 2: float} */ |
901 | private function getHistoryPageXY(int $index): array { |
902 | $a4_wid = 210; |
903 | $olz = [ |
904 | [1, $a4_wid * 0.75, 5.5 + 32 * 1], |
905 | [1, $a4_wid * 0.25, 5.5 + 32 * 2], |
906 | [1, $a4_wid * 0.75, 5.5 + 32 * 3], |
907 | [1, $a4_wid * 0.25, 5.5 + 32 * 4], |
908 | [1, $a4_wid * 0.75, 5.5 + 32 * 5], |
909 | [1, $a4_wid * 0.25, 5.5 + 32 * 6], |
910 | [1, $a4_wid * 0.75, 5.5 + 32 * 7], |
911 | [1, $a4_wid * 0.25, 5.5 + 32 * 8], |
912 | [2, $a4_wid * 0.75, 5.5 + 32 * 8], |
913 | [2, $a4_wid * 0.25, 5.5 + 32 * 7], |
914 | [2, $a4_wid * 0.75, 5.5 + 32 * 6], |
915 | [2, $a4_wid * 0.25, 5.5 + 32 * 5], |
916 | [2, $a4_wid * 0.75, 5.5 + 32 * 4], |
917 | [2, $a4_wid * 0.25, 5.5 + 32 * 3], |
918 | [2, $a4_wid * 0.75, 5.5 + 32 * 2], |
919 | [2, $a4_wid * 0.25, 5.5 + 32 * 1], |
920 | ]; |
921 | return $olz[$index]; |
922 | } |
923 | |
924 | public function renderDressesPages(): string { |
925 | if (!$this->authUtils()->hasPermission('panini2024')) { |
926 | throw new NotFoundHttpException(); |
927 | } |
928 | $pdf = $this->getBookPdf(); |
929 | $entries = $this->getDressesEntries(); |
930 | |
931 | $index = 0; |
932 | $last_page = 0; |
933 | foreach ($entries as $entry) { |
934 | [$page, $x, $y] = $this->getDressesPageXY($index); |
935 | for ($p = $last_page; $p < $page; $p++) { |
936 | $this->addBookPage($pdf); |
937 | } |
938 | $last_page = $page; |
939 | |
940 | $short = 44; |
941 | $long = 64; |
942 | $wid = $entry['is_landscape'] ? $long : $short; |
943 | $hei = $entry['is_landscape'] ? $short : $long; |
944 | |
945 | $this->drawPlaceholder($pdf, $entry, $x - $wid / 2, $y - $hei / 2, $wid, $hei); |
946 | |
947 | $index++; |
948 | } |
949 | return $pdf->Output('dresses.pdf', 'S'); |
950 | } |
951 | |
952 | /** @return array<array<string, mixed>> */ |
953 | private function getDressesEntries(): array { |
954 | $entries = []; |
955 | |
956 | $db = $this->dbUtils()->getDb(); |
957 | $result_olz = $db->query("SELECT * FROM panini24 WHERE id >= 40 AND id < 50 ORDER BY id ASC"); |
958 | // @phpstan-ignore-next-line |
959 | for ($i = 0; $i < $result_olz->num_rows; $i++) { |
960 | // @phpstan-ignore-next-line |
961 | $row_olz = $result_olz->fetch_assoc(); |
962 | $entries[] = $row_olz; |
963 | } |
964 | // @phpstan-ignore-next-line |
965 | return $entries; |
966 | } |
967 | |
968 | /** @return array{0: int, 1: float, 2: float} */ |
969 | private function getDressesPageXY(int $index): array { |
970 | $a4_wid = 210; |
971 | $olz = [ |
972 | [1, $a4_wid * 0.25, 42 + 22], |
973 | [1, $a4_wid * 0.75, 42 + 44 - 6 + 22], |
974 | [1, $a4_wid * 0.25, 42 + 44 - 6 + 44 - 6 + 22], |
975 | [1, $a4_wid * 0.75, 42 + 44 - 6 + 44 - 6 + 44 - 6 + 22], |
976 | [1, $a4_wid * 0.25, 42 + 44 - 6 + 44 - 6 + 44 - 6 + 44 - 6 + 22], |
977 | [1, $a4_wid * 0.75, 42 + 44 - 6 + 44 - 6 + 44 - 6 + 44 - 6 + 44 - 6 + 22], |
978 | ]; |
979 | return $olz[$index]; |
980 | } |
981 | |
982 | public function renderMapsPages(): string { |
983 | if (!$this->authUtils()->hasPermission('panini2024')) { |
984 | throw new NotFoundHttpException(); |
985 | } |
986 | $pdf = $this->getBookPdf(); |
987 | $entries = $this->getMapsEntries(); |
988 | |
989 | $index = 0; |
990 | $last_page = 0; |
991 | foreach ($entries as $entry) { |
992 | [$page, $x, $y] = $this->getMapsPageXY($index); |
993 | for ($p = $last_page; $p < $page; $p++) { |
994 | $this->addBookPage($pdf); |
995 | } |
996 | $last_page = $page; |
997 | |
998 | $short = 44; |
999 | $long = 64; |
1000 | $wid = $entry['is_landscape'] ? $long : $short; |
1001 | $hei = $entry['is_landscape'] ? $short : $long; |
1002 | |
1003 | $this->drawPlaceholder($pdf, $entry, $x - $wid / 2, $y - $hei / 2, $wid, $hei); |
1004 | |
1005 | $this->drawEntryInfobox($pdf, $entry, $x - $wid / 2, $y + $hei / 2 + 1, $wid, 25); |
1006 | |
1007 | $index++; |
1008 | } |
1009 | return $pdf->Output('maps.pdf', 'S'); |
1010 | } |
1011 | |
1012 | /** @return array<array<string, mixed>> */ |
1013 | private function getMapsEntries(): array { |
1014 | $entries = []; |
1015 | |
1016 | $db = $this->dbUtils()->getDb(); |
1017 | $result_olz = $db->query("SELECT * FROM panini24 WHERE id >= 100 AND id < 150 ORDER BY line1 ASC"); |
1018 | // @phpstan-ignore-next-line |
1019 | for ($i = 0; $i < $result_olz->num_rows; $i++) { |
1020 | // @phpstan-ignore-next-line |
1021 | $row_olz = $result_olz->fetch_assoc(); |
1022 | $entries[] = $row_olz; |
1023 | } |
1024 | // @phpstan-ignore-next-line |
1025 | return $entries; |
1026 | } |
1027 | |
1028 | /** @return array{0: int, 1: float, 2: float} */ |
1029 | private function getMapsPageXY(int $index): array { |
1030 | $x_step = 46; |
1031 | $x_offset = 13; |
1032 | $y_step = 92; |
1033 | $y_offset = 10.5; |
1034 | $y_box = 26; |
1035 | |
1036 | $col1 = $x_offset + $x_step * 1 / 2; |
1037 | $col2 = $x_offset + $x_step * 3 / 2; |
1038 | $col3 = $x_offset + $x_step * 5 / 2; |
1039 | $col4 = $x_offset + $x_step * 7 / 2; |
1040 | |
1041 | $row1 = $y_offset + $y_step * 1 / 2 - $y_box / 2; |
1042 | $row2 = $y_offset + $y_step * 3 / 2 - $y_box / 2; |
1043 | $row3 = $y_offset + $y_step * 5 / 2 - $y_box / 2; |
1044 | |
1045 | $olz = [ |
1046 | [1, $col1, $row1], |
1047 | // [1, $col2, $row1], |
1048 | [1, $col3, $row1], |
1049 | [1, $col4, $row1], |
1050 | [1, $col1, $row2], |
1051 | // [1, $col2, $row2], |
1052 | // [1, $col3, $row2], |
1053 | [1, $col4, $row2], |
1054 | [1, $col1, $row3], |
1055 | [1, $col2, $row3], |
1056 | [1, $col3, $row3], |
1057 | [1, $col4, $row3], |
1058 | [2, $col1, $row1], |
1059 | [2, $col2, $row1], |
1060 | [2, $col3, $row1], |
1061 | [2, $col4, $row1], |
1062 | [2, $col1, $row2], |
1063 | [2, $col2, $row2], |
1064 | [2, $col3, $row2], |
1065 | [2, $col4, $row2], |
1066 | [2, $col1, $row3], |
1067 | [2, $col2, $row3], |
1068 | [2, $col3, $row3], |
1069 | [2, $col4, $row3], |
1070 | ]; |
1071 | return $olz[$index]; |
1072 | } |
1073 | |
1074 | public function renderBackPages(): string { |
1075 | if (!$this->authUtils()->hasPermission('panini2024')) { |
1076 | throw new NotFoundHttpException(); |
1077 | } |
1078 | $pdf = $this->getBookPdf(); |
1079 | $entries = $this->getBackEntries(); |
1080 | |
1081 | $index = 0; |
1082 | $last_page = 0; |
1083 | foreach ($entries as $entry) { |
1084 | [$page, $x, $y] = $this->getBackPageXY($index); |
1085 | for ($p = $last_page; $p < $page; $p++) { |
1086 | $this->addBookPage($pdf); |
1087 | } |
1088 | $last_page = $page; |
1089 | |
1090 | $short = 44; |
1091 | $long = 64; |
1092 | $wid = $entry['is_landscape'] ? $long : $short; |
1093 | $hei = $entry['is_landscape'] ? $short : $long; |
1094 | |
1095 | $this->drawPlaceholder($pdf, $entry, $x - $wid / 2, $y - $hei / 2, $wid, $hei); |
1096 | |
1097 | $index++; |
1098 | } |
1099 | return $pdf->Output('back.pdf', 'S'); |
1100 | } |
1101 | |
1102 | /** @return array<array<string, mixed>> */ |
1103 | private function getBackEntries(): array { |
1104 | $entries = []; |
1105 | |
1106 | $db = $this->dbUtils()->getDb(); |
1107 | $result_olz = $db->query("SELECT * FROM panini24 WHERE id >= 20 AND id < 40 ORDER BY id ASC"); |
1108 | // @phpstan-ignore-next-line |
1109 | for ($i = 0; $i < $result_olz->num_rows; $i++) { |
1110 | // @phpstan-ignore-next-line |
1111 | $row_olz = $result_olz->fetch_assoc(); |
1112 | $entries[] = $row_olz; |
1113 | } |
1114 | // @phpstan-ignore-next-line |
1115 | return $entries; |
1116 | } |
1117 | |
1118 | /** @return array{0: int, 1: float, 2: float} */ |
1119 | private function getBackPageXY(int $index): array { |
1120 | $a4_wid = 210; |
1121 | $olz = [ |
1122 | [1, $a4_wid * 0.75, 297 - 10.5 - 22], |
1123 | [2, $a4_wid * 0.75, 297 - 10.5 - 22], |
1124 | ]; |
1125 | return $olz[$index]; |
1126 | } |
1127 | |
1128 | public static function fromEnv(): self { |
1129 | return new self(); |
1130 | } |
1131 | } |