PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.2
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.2
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
vigilante / includes / class-qr-svg.php

class-qr-svg.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.11.2, at includes/class-qr-svg.php

917 lines 26.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * QR Code SVG Generator
4 *
5 * Minimal QR code generator for TOTP URIs. Outputs SVG.
6 * Supports byte mode, ECL L, versions 1-10.
7 * No external dependencies, pure PHP.
8 *
9 * @package Vigilante
10 */
11
12 // Prevent direct access
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class Vigilante_QR_SVG
19 *
20 * Generates QR code as inline SVG string
21 */
22 class Vigilante_QR_SVG {
23
24 /**
25 * GF(2^8) exponential table
26 *
27 * @var array
28 */
29 private $gf_exp = array();
30
31 /**
32 * GF(2^8) logarithm table
33 *
34 * @var array
35 */
36 private $gf_log = array();
37
38 /**
39 * QR modules matrix
40 *
41 * @var array
42 */
43 private $modules = array();
44
45 /**
46 * Function pattern mask (true = reserved module)
47 *
48 * @var array
49 */
50 private $reserved = array();
51
52 /**
53 * QR code size (modules per side)
54 *
55 * @var int
56 */
57 private $size = 0;
58
59 /**
60 * Data capacity per version at ECL L (byte mode)
61 *
62 * @var array
63 */
64 private static $capacity = array(
65 1 => 17,
66 2 => 32,
67 3 => 53,
68 4 => 78,
69 5 => 106,
70 6 => 134,
71 7 => 154,
72 8 => 192,
73 9 => 230,
74 10 => 271,
75 );
76
77 /**
78 * Total data codewords per version at ECL L
79 *
80 * @var array
81 */
82 private static $total_codewords = array(
83 1 => 19,
84 2 => 34,
85 3 => 55,
86 4 => 80,
87 5 => 108,
88 6 => 136,
89 7 => 156,
90 8 => 194,
91 9 => 232,
92 10 => 274,
93 );
94
95 /**
96 * EC codewords per block at ECL L
97 *
98 * @var array
99 */
100 private static $ec_codewords = array(
101 1 => 7,
102 2 => 10,
103 3 => 15,
104 4 => 20,
105 5 => 26,
106 6 => 18,
107 7 => 20,
108 8 => 24,
109 9 => 30,
110 10 => 18,
111 );
112
113 /**
114 * Block structure at ECL L: array( [num_blocks_group1, dc_per_block_g1, num_blocks_group2, dc_per_block_g2] )
115 *
116 * @var array
117 */
118 private static $blocks = array(
119 1 => array( 1, 19, 0, 0 ),
120 2 => array( 1, 34, 0, 0 ),
121 3 => array( 1, 55, 0, 0 ),
122 4 => array( 1, 80, 0, 0 ),
123 5 => array( 1, 108, 0, 0 ),
124 6 => array( 2, 68, 0, 0 ),
125 7 => array( 2, 78, 0, 0 ),
126 8 => array( 2, 97, 0, 0 ),
127 9 => array( 2, 116, 0, 0 ),
128 10 => array( 2, 68, 2, 69 ),
129 );
130
131 /**
132 * Alignment pattern center positions per version
133 *
134 * @var array
135 */
136 private static $alignment = array(
137 2 => array( 6, 18 ),
138 3 => array( 6, 22 ),
139 4 => array( 6, 26 ),
140 5 => array( 6, 30 ),
141 6 => array( 6, 34 ),
142 7 => array( 6, 22, 38 ),
143 8 => array( 6, 24, 42 ),
144 9 => array( 6, 26, 46 ),
145 10 => array( 6, 28, 52 ),
146 );
147
148 /**
149 * Constructor - initialize GF(2^8) tables
150 */
151 public function __construct() {
152 $this->init_galois_field();
153 }
154
155 /**
156 * Generate QR code as SVG string
157 *
158 * @param string $data Data to encode.
159 * @param int $px_size Pixel size per module (default 4).
160 * @param int $margin Quiet zone modules (default 4).
161 * @return string SVG markup or empty string on failure.
162 */
163 public function generate( $data, $px_size = 4, $margin = 4 ) {
164 if ( empty( $data ) ) {
165 return '';
166 }
167
168 // Select version
169 $version = $this->select_version( strlen( $data ) );
170 if ( ! $version ) {
171 return '';
172 }
173
174 $this->size = 17 + $version * 4;
175
176 // Initialize matrix
177 $this->modules = array_fill( 0, $this->size, array_fill( 0, $this->size, null ) );
178 $this->reserved = array_fill( 0, $this->size, array_fill( 0, $this->size, false ) );
179
180 // Place function patterns
181 $this->place_finder_patterns();
182 $this->place_alignment_patterns( $version );
183 $this->place_timing_patterns();
184 $this->reserve_format_area();
185
186 if ( $version >= 7 ) {
187 $this->reserve_version_area();
188 }
189
190 // Encode data
191 $encoded = $this->encode_data( $data, $version );
192 if ( empty( $encoded ) ) {
193 return '';
194 }
195
196 // Add error correction
197 $final_data = $this->add_error_correction( $encoded, $version );
198
199 // Place data bits
200 $this->place_data_bits( $final_data );
201
202 // Apply best mask
203 $best_mask = $this->apply_best_mask();
204
205 // Place format information
206 $this->place_format_info( $best_mask );
207
208 // Place version information
209 if ( $version >= 7 ) {
210 $this->place_version_info( $version );
211 }
212
213 // Generate SVG
214 return $this->render_svg( $px_size, $margin );
215 }
216
217 /**
218 * Initialize GF(2^8) exp and log tables with polynomial 0x11D
219 */
220 private function init_galois_field() {
221 $x = 1;
222 for ( $i = 0; $i < 255; $i++ ) {
223 $this->gf_exp[ $i ] = $x;
224 $this->gf_log[ $x ] = $i;
225 $x <<= 1;
226 if ( $x & 0x100 ) {
227 $x ^= 0x11d;
228 }
229 }
230 // Extend exp table for convenience
231 for ( $i = 255; $i < 512; $i++ ) {
232 $this->gf_exp[ $i ] = $this->gf_exp[ $i - 255 ];
233 }
234 }
235
236 /**
237 * Multiply two values in GF(2^8)
238 *
239 * @param int $a First value.
240 * @param int $b Second value.
241 * @return int Product.
242 */
243 private function gf_mul( $a, $b ) {
244 if ( 0 === $a || 0 === $b ) {
245 return 0;
246 }
247 return $this->gf_exp[ $this->gf_log[ $a ] + $this->gf_log[ $b ] ];
248 }
249
250 /**
251 * Generate Reed-Solomon generator polynomial
252 *
253 * @param int $num_ec Number of EC codewords.
254 * @return array Generator polynomial coefficients.
255 */
256 private function rs_generator( $num_ec ) {
257 $gen = array( 1 );
258 for ( $i = 0; $i < $num_ec; $i++ ) {
259 $new_gen = array_fill( 0, count( $gen ) + 1, 0 );
260 for ( $j = 0; $j < count( $gen ); $j++ ) {
261 $new_gen[ $j ] ^= $gen[ $j ];
262 $new_gen[ $j + 1 ] ^= $this->gf_mul( $gen[ $j ], $this->gf_exp[ $i ] );
263 }
264 $gen = $new_gen;
265 }
266 return $gen;
267 }
268
269 /**
270 * Reed-Solomon encode a data block
271 *
272 * @param array $data Data codewords.
273 * @param int $num_ec Number of EC codewords.
274 * @return array EC codewords.
275 */
276 private function rs_encode( $data, $num_ec ) {
277 $gen = $this->rs_generator( $num_ec );
278 $enc = array_merge( $data, array_fill( 0, $num_ec, 0 ) );
279
280 for ( $i = 0; $i < count( $data ); $i++ ) {
281 $coef = $enc[ $i ];
282 if ( 0 !== $coef ) {
283 for ( $j = 0; $j < count( $gen ); $j++ ) {
284 $enc[ $i + $j ] ^= $this->gf_mul( $gen[ $j ], $coef );
285 }
286 }
287 }
288
289 return array_slice( $enc, count( $data ) );
290 }
291
292 /**
293 * Select minimum QR version for data length
294 *
295 * @param int $length Data length in bytes.
296 * @return int|false Version number or false.
297 */
298 private function select_version( $length ) {
299 foreach ( self::$capacity as $v => $cap ) {
300 if ( $length <= $cap ) {
301 return $v;
302 }
303 }
304 return false;
305 }
306
307 /**
308 * Encode data in byte mode
309 *
310 * @param string $data Raw data string.
311 * @param int $version QR version.
312 * @return array Codewords array.
313 */
314 private function encode_data( $data, $version ) {
315 $bits = '';
316
317 // Mode indicator: 0100 (byte mode)
318 $bits .= '0100';
319
320 // Character count indicator
321 $cc_bits = $version <= 9 ? 8 : 16;
322 $bits .= str_pad( decbin( strlen( $data ) ), $cc_bits, '0', STR_PAD_LEFT );
323
324 // Data bytes
325 for ( $i = 0; $i < strlen( $data ); $i++ ) {
326 $bits .= str_pad( decbin( ord( $data[ $i ] ) ), 8, '0', STR_PAD_LEFT );
327 }
328
329 // Terminator (up to 4 bits)
330 // $total_codewords already contains DATA codewords count (EC excluded)
331 $total_dc = self::$total_codewords[ $version ];
332 $max_bits = $total_dc * 8;
333
334 $term_len = max( 0, min( 4, $max_bits - strlen( $bits ) ) );
335 $bits .= str_repeat( '0', $term_len );
336
337 // Pad to byte boundary
338 if ( strlen( $bits ) % 8 !== 0 ) {
339 $bits .= str_repeat( '0', 8 - ( strlen( $bits ) % 8 ) );
340 }
341
342 // Convert to codewords
343 $codewords = array();
344 for ( $i = 0; $i < strlen( $bits ); $i += 8 ) {
345 $codewords[] = intval( substr( $bits, $i, 8 ), 2 );
346 }
347
348 // Pad with alternating 236/17 to fill capacity
349 $pad = array( 236, 17 );
350 $pad_idx = 0;
351 while ( count( $codewords ) < $total_dc ) {
352 $codewords[] = $pad[ $pad_idx % 2 ];
353 $pad_idx++;
354 }
355
356 return $codewords;
357 }
358
359 /**
360 * Get total number of blocks for a version
361 *
362 * @param int $version QR version.
363 * @return int Total blocks.
364 */
365 private function get_total_blocks( $version ) {
366 $b = self::$blocks[ $version ];
367 return $b[0] + $b[2];
368 }
369
370 /**
371 * Add error correction and interleave blocks
372 *
373 * @param array $data Data codewords.
374 * @param int $version QR version.
375 * @return array Final codeword sequence.
376 */
377 private function add_error_correction( $data, $version ) {
378 $b = self::$blocks[ $version ];
379 $ec_per = self::$ec_codewords[ $version ];
380
381 $data_blocks = array();
382 $ec_blocks = array();
383 $offset = 0;
384
385 // Group 1
386 for ( $i = 0; $i < $b[0]; $i++ ) {
387 $block = array_slice( $data, $offset, $b[1] );
388 $data_blocks[] = $block;
389 $ec_blocks[] = $this->rs_encode( $block, $ec_per );
390 $offset += $b[1];
391 }
392
393 // Group 2
394 for ( $i = 0; $i < $b[2]; $i++ ) {
395 $block = array_slice( $data, $offset, $b[3] );
396 $data_blocks[] = $block;
397 $ec_blocks[] = $this->rs_encode( $block, $ec_per );
398 $offset += $b[3];
399 }
400
401 // Interleave data codewords
402 $result = array();
403 $max_data = max( $b[1], $b[3] );
404 for ( $i = 0; $i < $max_data; $i++ ) {
405 foreach ( $data_blocks as $block ) {
406 if ( $i < count( $block ) ) {
407 $result[] = $block[ $i ];
408 }
409 }
410 }
411
412 // Interleave EC codewords
413 for ( $i = 0; $i < $ec_per; $i++ ) {
414 foreach ( $ec_blocks as $block ) {
415 if ( $i < count( $block ) ) {
416 $result[] = $block[ $i ];
417 }
418 }
419 }
420
421 // Add remainder bits (version-dependent)
422 $remainder_bits = array( 0, 0, 7, 7, 7, 7, 0, 0, 0, 0, 0 );
423 // Not added as codewords, but as bits during placement
424
425 return $result;
426 }
427
428 /**
429 * Place finder patterns at three corners
430 */
431 private function place_finder_patterns() {
432 $positions = array(
433 array( 0, 0 ),
434 array( 0, $this->size - 7 ),
435 array( $this->size - 7, 0 ),
436 );
437
438 foreach ( $positions as $pos ) {
439 $r = $pos[0];
440 $c = $pos[1];
441 for ( $dr = 0; $dr < 7; $dr++ ) {
442 for ( $dc = 0; $dc < 7; $dc++ ) {
443 $dark = ( 0 === $dr || 6 === $dr || 0 === $dc || 6 === $dc )
444 || ( $dr >= 2 && $dr <= 4 && $dc >= 2 && $dc <= 4 );
445 $this->set_module( $r + $dr, $c + $dc, $dark, true );
446 }
447 }
448 }
449
450 // Separators
451 for ( $i = 0; $i < 8; $i++ ) {
452 // Top-left
453 $this->set_module( 7, $i, false, true );
454 $this->set_module( $i, 7, false, true );
455 // Top-right
456 $this->set_module( 7, $this->size - 8 + $i, false, true );
457 $this->set_module( $i, $this->size - 8, false, true );
458 // Bottom-left
459 $this->set_module( $this->size - 8, $i, false, true );
460 $this->set_module( $this->size - 8 + $i, 7, false, true );
461 }
462 }
463
464 /**
465 * Place alignment patterns
466 *
467 * @param int $version QR version.
468 */
469 private function place_alignment_patterns( $version ) {
470 if ( ! isset( self::$alignment[ $version ] ) ) {
471 return;
472 }
473
474 $positions = self::$alignment[ $version ];
475 $combos = array();
476
477 foreach ( $positions as $r ) {
478 foreach ( $positions as $c ) {
479 $combos[] = array( $r, $c );
480 }
481 }
482
483 foreach ( $combos as $pos ) {
484 $r = $pos[0];
485 $c = $pos[1];
486
487 // Skip if overlapping finder pattern
488 if ( $this->reserved[ $r ][ $c ] ) {
489 continue;
490 }
491
492 for ( $dr = -2; $dr <= 2; $dr++ ) {
493 for ( $dc = -2; $dc <= 2; $dc++ ) {
494 $dark = ( abs( $dr ) === 2 || abs( $dc ) === 2 || ( 0 === $dr && 0 === $dc ) );
495 $this->set_module( $r + $dr, $c + $dc, $dark, true );
496 }
497 }
498 }
499 }
500
501 /**
502 * Place timing patterns
503 */
504 private function place_timing_patterns() {
505 for ( $i = 8; $i < $this->size - 8; $i++ ) {
506 if ( ! $this->reserved[ 6 ][ $i ] ) {
507 $this->set_module( 6, $i, 0 === $i % 2, true );
508 }
509 if ( ! $this->reserved[ $i ][ 6 ] ) {
510 $this->set_module( $i, 6, 0 === $i % 2, true );
511 }
512 }
513 }
514
515 /**
516 * Reserve format information area
517 */
518 private function reserve_format_area() {
519 // Around top-left finder
520 for ( $i = 0; $i <= 8; $i++ ) {
521 if ( $i !== 6 ) {
522 $this->reserve( 8, $i );
523 }
524 if ( $i !== 6 && $i < 8 ) {
525 $this->reserve( $i, 8 );
526 }
527 }
528 $this->reserve( 8, 8 );
529
530 // Bottom-left
531 for ( $i = $this->size - 7; $i < $this->size; $i++ ) {
532 $this->reserve( $i, 8 );
533 }
534
535 // Top-right
536 for ( $i = $this->size - 8; $i < $this->size; $i++ ) {
537 $this->reserve( 8, $i );
538 }
539
540 // Dark module
541 $this->set_module( $this->size - 8, 8, true, true );
542 }
543
544 /**
545 * Reserve version information area (version >= 7)
546 */
547 private function reserve_version_area() {
548 for ( $i = 0; $i < 6; $i++ ) {
549 for ( $j = $this->size - 11; $j < $this->size - 8; $j++ ) {
550 $this->reserve( $i, $j );
551 $this->reserve( $j, $i );
552 }
553 }
554 }
555
556 /**
557 * Place format information bits
558 *
559 * @param int $mask Mask pattern (0-7).
560 */
561 private function place_format_info( $mask ) {
562 // ECL L = 01, mask pattern 3 bits
563 $format_data = ( 1 << 3 ) | $mask; // ECL L = 01
564 $format_ecc = $this->calc_format_ecc( $format_data );
565 $format_bits = ( $format_data << 10 ) | $format_ecc;
566 $format_bits ^= 0x5412; // XOR with mask pattern 101010000010010
567
568 // Position sequences for format info
569 $positions_a = array(
570 array( 0, 8 ), array( 1, 8 ), array( 2, 8 ), array( 3, 8 ),
571 array( 4, 8 ), array( 5, 8 ), array( 7, 8 ), array( 8, 8 ),
572 array( 8, 7 ), array( 8, 5 ), array( 8, 4 ), array( 8, 3 ),
573 array( 8, 2 ), array( 8, 1 ), array( 8, 0 ),
574 );
575
576 $positions_b = array();
577 for ( $i = $this->size - 1; $i >= $this->size - 7; $i-- ) {
578 $positions_b[] = array( 8, $i );
579 }
580 $positions_b[] = array( 8, $this->size - 8 );
581 for ( $i = $this->size - 7; $i < $this->size; $i++ ) {
582 $positions_b[] = array( $i, 8 );
583 }
584
585 for ( $i = 0; $i < 15; $i++ ) {
586 $bit = ( $format_bits >> ( 14 - $i ) ) & 1;
587 $this->modules[ $positions_a[ $i ][0] ][ $positions_a[ $i ][1] ] = (bool) $bit;
588 $this->modules[ $positions_b[ $i ][0] ][ $positions_b[ $i ][1] ] = (bool) $bit;
589 }
590 }
591
592 /**
593 * Calculate format ECC (BCH(15,5))
594 *
595 * @param int $data 5-bit format data.
596 * @return int 10-bit ECC.
597 */
598 private function calc_format_ecc( $data ) {
599 $g = 0x537; // Generator polynomial
600 $bits = $data << 10;
601 for ( $i = 4; $i >= 0; $i-- ) {
602 if ( $bits & ( 1 << ( $i + 10 ) ) ) {
603 $bits ^= $g << $i;
604 }
605 }
606 return $bits;
607 }
608
609 /**
610 * Place version information bits (version >= 7)
611 *
612 * @param int $version QR version.
613 */
614 private function place_version_info( $version ) {
615 $ver_data = $this->calc_version_ecc( $version );
616
617 for ( $i = 0; $i < 18; $i++ ) {
618 $bit = ( $ver_data >> $i ) & 1;
619 $r = intval( $i / 3 );
620 $c = $this->size - 11 + ( $i % 3 );
621 $this->modules[ $r ][ $c ] = (bool) $bit;
622 $this->modules[ $c ][ $r ] = (bool) $bit;
623 }
624 }
625
626 /**
627 * Calculate version ECC (BCH(18,6))
628 *
629 * @param int $version Version number.
630 * @return int 18-bit version info.
631 */
632 private function calc_version_ecc( $version ) {
633 $g = 0x1F25; // Generator polynomial
634 $bits = $version << 12;
635 $tmp = $bits;
636 for ( $i = 5; $i >= 0; $i-- ) {
637 if ( $tmp & ( 1 << ( $i + 12 ) ) ) {
638 $tmp ^= $g << $i;
639 }
640 }
641 return $bits | $tmp;
642 }
643
644 /**
645 * Place data bits in zigzag pattern
646 *
647 * @param array $codewords Data and EC codewords.
648 */
649 private function place_data_bits( $codewords ) {
650 $bits = '';
651 foreach ( $codewords as $cw ) {
652 $bits .= str_pad( decbin( $cw ), 8, '0', STR_PAD_LEFT );
653 }
654
655 $bit_idx = 0;
656 $col = $this->size - 1;
657
658 while ( $col > 0 ) {
659 // Skip vertical timing pattern column
660 if ( 6 === $col ) {
661 $col--;
662 }
663
664 for ( $row = 0; $row < $this->size; $row++ ) {
665 // Determine actual row based on direction
666 $upward = ( ( ( $this->size - 1 - $col ) >> 1 ) & 1 ) === 0;
667 $actual_row = $upward ? $this->size - 1 - $row : $row;
668
669 for ( $c_offset = 0; $c_offset <= 1; $c_offset++ ) {
670 $actual_col = $col - $c_offset;
671
672 if ( $actual_col < 0 || $actual_col >= $this->size ) {
673 continue;
674 }
675
676 if ( $this->reserved[ $actual_row ][ $actual_col ] ) {
677 continue;
678 }
679
680 if ( $bit_idx < strlen( $bits ) ) {
681 $this->modules[ $actual_row ][ $actual_col ] = '1' === $bits[ $bit_idx ];
682 $bit_idx++;
683 } else {
684 $this->modules[ $actual_row ][ $actual_col ] = false;
685 }
686 }
687 }
688
689 $col -= 2;
690 }
691 }
692
693 /**
694 * Apply best mask pattern
695 *
696 * @return int Best mask index (0-7).
697 */
698 private function apply_best_mask() {
699 $best_mask = 0;
700 $best_penalty = PHP_INT_MAX;
701 $original = $this->modules;
702
703 for ( $mask = 0; $mask < 8; $mask++ ) {
704 $this->modules = $this->deep_copy( $original );
705 $this->apply_mask( $mask );
706 $this->place_format_info( $mask );
707
708 $penalty = $this->calc_penalty();
709
710 if ( $penalty < $best_penalty ) {
711 $best_penalty = $penalty;
712 $best_mask = $mask;
713 }
714 }
715
716 // Apply the best mask
717 $this->modules = $this->deep_copy( $original );
718 $this->apply_mask( $best_mask );
719
720 return $best_mask;
721 }
722
723 /**
724 * Apply a specific mask pattern to data modules
725 *
726 * @param int $mask Mask index (0-7).
727 */
728 private function apply_mask( $mask ) {
729 for ( $r = 0; $r < $this->size; $r++ ) {
730 for ( $c = 0; $c < $this->size; $c++ ) {
731 if ( $this->reserved[ $r ][ $c ] ) {
732 continue;
733 }
734 $invert = false;
735 switch ( $mask ) {
736 case 0:
737 $invert = ( ( $r + $c ) % 2 === 0 );
738 break;
739 case 1:
740 $invert = ( $r % 2 === 0 );
741 break;
742 case 2:
743 $invert = ( $c % 3 === 0 );
744 break;
745 case 3:
746 $invert = ( ( $r + $c ) % 3 === 0 );
747 break;
748 case 4:
749 $invert = ( ( intval( $r / 2 ) + intval( $c / 3 ) ) % 2 === 0 );
750 break;
751 case 5:
752 $invert = ( ( ( $r * $c ) % 2 ) + ( ( $r * $c ) % 3 ) === 0 );
753 break;
754 case 6:
755 $invert = ( ( ( ( $r * $c ) % 2 ) + ( ( $r * $c ) % 3 ) ) % 2 === 0 );
756 break;
757 case 7:
758 $invert = ( ( ( ( $r + $c ) % 2 ) + ( ( $r * $c ) % 3 ) ) % 2 === 0 );
759 break;
760 }
761 if ( $invert ) {
762 $this->modules[ $r ][ $c ] = ! $this->modules[ $r ][ $c ];
763 }
764 }
765 }
766 }
767
768 /**
769 * Calculate penalty score for current matrix
770 *
771 * Simplified: evaluates rule 1 (runs) and rule 3 (finder-like patterns)
772 *
773 * @return int Penalty score.
774 */
775 private function calc_penalty() {
776 $penalty = 0;
777 $n = $this->size;
778
779 // Rule 1: Adjacent modules in row/column of same color (>=5)
780 for ( $r = 0; $r < $n; $r++ ) {
781 $run_len = 1;
782 for ( $c = 1; $c < $n; $c++ ) {
783 if ( $this->modules[ $r ][ $c ] === $this->modules[ $r ][ $c - 1 ] ) {
784 $run_len++;
785 } else {
786 if ( $run_len >= 5 ) {
787 $penalty += $run_len - 2;
788 }
789 $run_len = 1;
790 }
791 }
792 if ( $run_len >= 5 ) {
793 $penalty += $run_len - 2;
794 }
795 }
796
797 for ( $c = 0; $c < $n; $c++ ) {
798 $run_len = 1;
799 for ( $r = 1; $r < $n; $r++ ) {
800 if ( $this->modules[ $r ][ $c ] === $this->modules[ $r - 1 ][ $c ] ) {
801 $run_len++;
802 } else {
803 if ( $run_len >= 5 ) {
804 $penalty += $run_len - 2;
805 }
806 $run_len = 1;
807 }
808 }
809 if ( $run_len >= 5 ) {
810 $penalty += $run_len - 2;
811 }
812 }
813
814 // Rule 2: 2x2 blocks of same color
815 for ( $r = 0; $r < $n - 1; $r++ ) {
816 for ( $c = 0; $c < $n - 1; $c++ ) {
817 $val = $this->modules[ $r ][ $c ];
818 if ( $val === $this->modules[ $r ][ $c + 1 ]
819 && $val === $this->modules[ $r + 1 ][ $c ]
820 && $val === $this->modules[ $r + 1 ][ $c + 1 ] ) {
821 $penalty += 3;
822 }
823 }
824 }
825
826 // Rule 4: Proportion of dark modules
827 $dark = 0;
828 for ( $r = 0; $r < $n; $r++ ) {
829 for ( $c = 0; $c < $n; $c++ ) {
830 if ( $this->modules[ $r ][ $c ] ) {
831 $dark++;
832 }
833 }
834 }
835 $total = $n * $n;
836 $percent = ( $dark * 100 ) / $total;
837 $prev_five = intval( $percent / 5 ) * 5;
838 $next_five = $prev_five + 5;
839 $penalty += min( abs( $prev_five - 50 ), abs( $next_five - 50 ) ) * 2;
840
841 return $penalty;
842 }
843
844 /**
845 * Render the QR matrix as SVG markup
846 *
847 * @param int $px_size Pixel size per module.
848 * @param int $margin Quiet zone in modules.
849 * @return string SVG markup.
850 */
851 private function render_svg( $px_size, $margin ) {
852 $total = ( $this->size + $margin * 2 ) * $px_size;
853
854 $svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' . $total . ' ' . $total . '" width="' . $total . '" height="' . $total . '">';
855 $svg .= '<rect width="100%" height="100%" fill="#ffffff"/>';
856 $svg .= '<path d="';
857
858 for ( $r = 0; $r < $this->size; $r++ ) {
859 for ( $c = 0; $c < $this->size; $c++ ) {
860 if ( $this->modules[ $r ][ $c ] ) {
861 $x = ( $c + $margin ) * $px_size;
862 $y = ( $r + $margin ) * $px_size;
863 $svg .= 'M' . $x . ',' . $y . 'h' . $px_size . 'v' . $px_size . 'h-' . $px_size . 'z';
864 }
865 }
866 }
867
868 $svg .= '" fill="#000000"/>';
869 $svg .= '</svg>';
870
871 return $svg;
872 }
873
874 /**
875 * Set a module value and optionally mark as reserved
876 *
877 * @param int $row Row index.
878 * @param int $col Column index.
879 * @param bool $dark True for dark module.
880 * @param bool $reserved Mark as function pattern.
881 */
882 private function set_module( $row, $col, $dark, $reserved = false ) {
883 if ( $row >= 0 && $row < $this->size && $col >= 0 && $col < $this->size ) {
884 $this->modules[ $row ][ $col ] = (bool) $dark;
885 if ( $reserved ) {
886 $this->reserved[ $row ][ $col ] = true;
887 }
888 }
889 }
890
891 /**
892 * Reserve a module position without setting value
893 *
894 * @param int $row Row index.
895 * @param int $col Column index.
896 */
897 private function reserve( $row, $col ) {
898 if ( $row >= 0 && $row < $this->size && $col >= 0 && $col < $this->size ) {
899 $this->reserved[ $row ][ $col ] = true;
900 }
901 }
902
903 /**
904 * Deep copy a 2D array
905 *
906 * @param array $arr Original array.
907 * @return array Copy.
908 */
909 private function deep_copy( $arr ) {
910 $copy = array();
911 foreach ( $arr as $r => $row ) {
912 $copy[ $r ] = $row; // PHP arrays are copied by value
913 }
914 return $copy;
915 }
916 }
917