';
$out .= '
Select a card to choose the appearance. The dynamic destination and scan statistics will not change.
'; return ob_get_clean(); } public static function data_uri($text, $size=600) { return 'data:image/svg+xml;base64,'.base64_encode(self::svg($text, $size)); } public static function png($text, $size=1200, $logo_attachment_id=0, $shape='classic') { if (!function_exists('imagecreatetruecolor')) { return new WP_Error('net360_no_gd', 'The PHP GD extension is not available.'); } $matrix=self::matrix($text); $n=self::SIZE; $margin=4; $modules=$n+($margin*2); $shape=self::shape($shape); $has_logo=(bool)self::logo_file($logo_attachment_id); $size=max(128,min(3000,(int)$size)); $img=imagecreatetruecolor($size,$size); $white=imagecolorallocate($img,255,255,255); $black=imagecolorallocate($img,17,24,39); imagefilledrectangle($img,0,0,$size-1,$size-1,$white); $scale=$size/$modules; for($y=0;$y<$n;$y++) for($x=0;$x<$n;$x++) { if ($has_logo && self::inside_logo_hole($x + $margin + 0.5, $y + $margin + 0.5, $modules)) continue; if (!$matrix[$y][$x]) continue; $x1=(int)floor(($x+$margin)*$scale); $y1=(int)floor(($y+$margin)*$scale); $x2=(int)ceil(($x+$margin+1)*$scale)-1; $y2=(int)ceil(($y+$margin+1)*$scale)-1; // Keep finder-pattern zones square; stylize only the data modules. $finder = (($x < 8 && $y < 8) || ($x >= $n - 8 && $y < 8) || ($x < 8 && $y >= $n - 8)); if (!$finder && $shape === 'dots') { $cx=(int)round(($x+$margin+.5)*$scale); $cy=(int)round(($y+$margin+.5)*$scale); $r=max(1,(int)floor($scale*.43)); imagefilledellipse($img,$cx,$cy,$r*2,$r*2,$black); } elseif (!$finder && ($shape === 'diamonds' || $shape === 'geometric')) { $cx=($x+$margin+.5)*$scale; $cy=($y+$margin+.5)*$scale; if ($shape === 'diamonds') $pts=array((int)$cx,(int)($y1),(int)$x2,(int)$cy,(int)$cx,(int)$y2,(int)$x1,(int)$cy); else { $pts=array((int)($x1+$scale*.25),(int)$y1,(int)($x1+$scale*.75),(int)$y1,(int)$x2,(int)$cy,(int)($x1+$scale*.75),(int)$y2,(int)($x1+$scale*.25),(int)$y2,(int)$x1,(int)$cy); } if (function_exists('imagefilledpolygon')) { imagefilledpolygon($img, $pts, (int) (count($pts) / 2), $black); } else { imagefilledrectangle($img, $x1, $y1, $x2, $y2, $black); } } elseif (!$finder && ($shape === 'rounded' || $shape === 'rounded_square')) { $inset = $shape === 'rounded' ? (int)floor($scale*.12) : (int)floor($scale*.04); imagefilledrectangle($img,$x1+$inset,$y1+$inset,$x2-$inset,$y2-$inset,$black); } else imagefilledrectangle($img,$x1,$y1,$x2,$y2,$black); } // Overlay an optional small logo in the center, with a white backing. $logo_file = self::logo_file($logo_attachment_id); if ($logo_file && function_exists('imagecreatefromstring')) { $logo_bytes = file_get_contents($logo_file); $logo_src = ($logo_bytes !== false && strlen($logo_bytes) <= 2 * 1024 * 1024) ? @imagecreatefromstring($logo_bytes) : false; if ($logo_src) { $box = (int) round($size * 0.20); // Match the SVG export: rounded true-clearance area and inner spacing. $pad = max(3, (int) round($box * 0.15)); $inner = max(1, $box - 2 * $pad); $lx = (int) floor(($size - $box) / 2); $ly = $lx; $white = imagecolorallocate($img, 255, 255, 255); self::filled_rounded_rect($img, $lx, $ly, $lx + $box - 1, $ly + $box - 1, (int) round($box * 0.16), $white); $sw = imagesx($logo_src); $sh = imagesy($logo_src); $ratio = min($inner / max(1, $sw), $inner / max(1, $sh)); $dw = max(1, (int) round($sw * $ratio)); $dh = max(1, (int) round($sh * $ratio)); imagealphablending($img, true); imagecopyresampled($img, $logo_src, $lx + (int)(($box-$dw)/2), $ly + (int)(($box-$dh)/2), 0, 0, $dw, $dh, $sw, $sh); imagedestroy($logo_src); } } ob_start(); try { imagepng($img); $data = ob_get_clean(); } catch (Throwable $exception) { if (ob_get_level() > 0) ob_end_clean(); imagedestroy($img); return new WP_Error('net360qr_png_generation_failed', 'Could not generate the PNG.'); } imagedestroy($img); return $data; } /** Determine whether a module center falls inside the reserved rounded logo cutout. */ private static function inside_logo_hole($cx, $cy, $view) { $box = $view * 0.20; $left = ($view - $box) / 2; $right = $left + $box; $top = $left; $bottom = $right; $radius = $box * 0.16; if ($cx < $left || $cx > $right || $cy < $top || $cy > $bottom) return false; $qx = max($left + $radius, min($cx, $right - $radius)); $qy = max($top + $radius, min($cy, $bottom - $radius)); $dx = $cx - $qx; $dy = $cy - $qy; return (($dx * $dx) + ($dy * $dy)) <= ($radius * $radius); } /** Draw a solid rounded rectangle with GD using matching rectangles and circular corners. */ private static function filled_rounded_rect($image, $x1, $y1, $x2, $y2, $radius, $color) { $radius = max(1, min((int)$radius, (int)(($x2 - $x1 + 1) / 2), (int)(($y2 - $y1 + 1) / 2))); imagefilledrectangle($image, $x1 + $radius, $y1, $x2 - $radius, $y2, $color); imagefilledrectangle($image, $x1, $y1 + $radius, $x2, $y2 - $radius, $color); imagefilledellipse($image, $x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, $color); imagefilledellipse($image, $x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, $color); imagefilledellipse($image, $x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, $color); imagefilledellipse($image, $x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, $color); } public static function validate_text($text) { $bytes = strlen((string)$text); if ($bytes > self::MAX_BYTES) { return new WP_Error('net360qr_too_long', sprintf('The QR code content exceeds the maximum of %d bytes.', self::MAX_BYTES)); } return true; } public static function shape($value) { $allowed = array('classic','rounded','dots','diamonds','rounded_square','geometric'); $value = sanitize_key((string)$value); return in_array($value, $allowed, true) ? $value : 'classic'; } private static function color($value, $fallback) { $value = strtoupper(trim((string)$value)); return preg_match('/^#[0-9A-F]{6}$/', $value) ? $value : $fallback; } private static function matrix($text) { $text = (string)$text; $valid = self::validate_text($text); if (is_wp_error($valid)) { throw new InvalidArgumentException(esc_html($valid->get_error_message())); } $bytes = array_values(unpack('C*', $text)); $bits = array(0,1,0,0); // Byte mode $len = count($bytes); self::append_bits($bits, $len, 8); foreach ($bytes as $byte) self::append_bits($bits, $byte, 8); $capacity = self::DATA_CODEWORDS * 8; $terminator = min(4, $capacity - count($bits)); for ($i=0; $i<$terminator; $i++) $bits[] = 0; while (count($bits) % 8) $bits[] = 0; $pads = array(0xEC, 0x11); $p = 0; while (count($bits) < $capacity) { self::append_bits($bits, $pads[$p % 2], 8); $p++; } $data = array(); for ($i=0; $i