no message
This commit is contained in:
@@ -0,0 +1,667 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
class ShortLinkQrCodeService
|
||||
{
|
||||
private const DATA_CODEWORDS = [1 => 19, 2 => 34, 3 => 55, 4 => 80, 5 => 108];
|
||||
private const ECC_CODEWORDS = [1 => 7, 2 => 10, 3 => 15, 4 => 20, 5 => 26];
|
||||
private const ALIGNMENT_POSITIONS = [1 => [], 2 => [6, 18], 3 => [6, 22], 4 => [6, 26], 5 => [6, 30]];
|
||||
|
||||
private static array $exp = [];
|
||||
private static array $log = [];
|
||||
|
||||
public static function create(string $content, string $code, string $posterUri = '', array $textData = []): array
|
||||
{
|
||||
$matrix = self::encode($content);
|
||||
$svg = self::toSvg($matrix);
|
||||
$dirUri = 'uploads/qrcode/shortlink';
|
||||
$dir = public_path() . $dirUri;
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$svgUri = $dirUri . '/' . $code . '.svg';
|
||||
$svgPath = public_path() . $svgUri;
|
||||
file_put_contents($svgPath, $svg);
|
||||
|
||||
$qrUri = $svgUri;
|
||||
$posterCompositeUri = '';
|
||||
if (function_exists('imagecreatetruecolor')) {
|
||||
$pngUri = $dirUri . '/' . $code . '.png';
|
||||
self::toPng($matrix, public_path() . $pngUri);
|
||||
$qrUri = $pngUri;
|
||||
|
||||
// 有海报底图时合成完整海报:背景 + 文字层 + 二维码白底
|
||||
if ($posterUri !== '' && self::isLocalImage($posterUri)) {
|
||||
$posterPath = public_path() . ltrim($posterUri, '/');
|
||||
if (is_file($posterPath)) {
|
||||
$compositeUri = $dirUri . '/' . $code . '_poster.png';
|
||||
if (self::composePoster($posterPath, public_path() . $pngUri, public_path() . $compositeUri, $textData)) {
|
||||
$posterCompositeUri = $compositeUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// uri 用于落库 qrcode 字段:合成海报存在则取合成图,否则取纯二维码
|
||||
$uri = $posterCompositeUri !== '' ? $posterCompositeUri : $qrUri;
|
||||
|
||||
return [
|
||||
'uri' => $uri,
|
||||
'url' => FileService::getFileUrl($uri),
|
||||
'qr_uri' => $qrUri,
|
||||
'qr_url' => FileService::getFileUrl($qrUri),
|
||||
'poster_uri' => $posterCompositeUri,
|
||||
'poster_url' => $posterCompositeUri !== '' ? FileService::getFileUrl($posterCompositeUri) : '',
|
||||
'data_url' => 'data:image/svg+xml;base64,' . base64_encode($svg),
|
||||
];
|
||||
}
|
||||
|
||||
private static function isLocalImage(string $uri): bool
|
||||
{
|
||||
$ext = strtolower(pathinfo($uri, PATHINFO_EXTENSION));
|
||||
return in_array($ext, ['jpg', 'jpeg', 'png'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合成海报:背景图 + 文字层 + 二维码(带白底圆角)
|
||||
*/
|
||||
private static function composePoster(string $posterPath, string $qrPath, string $outPath, array $textData = []): bool
|
||||
{
|
||||
$ext = strtolower(pathinfo($posterPath, PATHINFO_EXTENSION));
|
||||
$bg = match ($ext) {
|
||||
'jpg', 'jpeg' => @imagecreatefromjpeg($posterPath),
|
||||
'png' => @imagecreatefrompng($posterPath),
|
||||
default => false,
|
||||
};
|
||||
if (!$bg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$qr = @imagecreatefrompng($qrPath);
|
||||
if (!$qr) {
|
||||
imagedestroy($bg);
|
||||
return false;
|
||||
}
|
||||
|
||||
$bgW = imagesx($bg);
|
||||
$bgH = imagesy($bg);
|
||||
|
||||
// 二维码尺寸:背景宽度的 32%,最小 200,最大 360
|
||||
$qrSize = (int) max(200, min(360, $bgW * 0.32));
|
||||
$padding = 3;
|
||||
$boxSize = $qrSize + $padding * 2;
|
||||
// 右下角,距离右边和底部各 5%
|
||||
$margin = (int) ($bgW * 0.05);
|
||||
$boxX = (int) ($bgW - $boxSize - $margin);
|
||||
$boxY = (int) ($bgH - $boxSize - $margin);
|
||||
|
||||
// 文字层:在二维码上方区域绘制
|
||||
$font = self::resolveFont();
|
||||
if ($font !== null && !empty($textData)) {
|
||||
self::drawTextLayer($bg, $font, $bgW, $boxY, $textData);
|
||||
}
|
||||
|
||||
// 白色底
|
||||
$white = imagecolorallocate($bg, 255, 255, 255);
|
||||
imagefilledrectangle($bg, $boxX, $boxY, $boxX + $boxSize, $boxY + $boxSize, $white);
|
||||
|
||||
// 二维码缩放后贴到底框内
|
||||
$qrW = imagesx($qr);
|
||||
$qrH = imagesy($qr);
|
||||
imagecopyresampled(
|
||||
$bg,
|
||||
$qr,
|
||||
$boxX + $padding,
|
||||
$boxY + $padding,
|
||||
0,
|
||||
0,
|
||||
$qrSize,
|
||||
$qrSize,
|
||||
$qrW,
|
||||
$qrH
|
||||
);
|
||||
|
||||
$ok = imagepng($bg, $outPath);
|
||||
imagedestroy($bg);
|
||||
imagedestroy($qr);
|
||||
return $ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* 探测可用的中文字体路径,找不到返回 null(此时文字层会被跳过)
|
||||
* 注意:不能落到不支持中文的字体(如 DejaVuSans),否则会渲染成豆腐块
|
||||
*/
|
||||
private static function resolveFont(): ?string
|
||||
{
|
||||
$candidates = [
|
||||
public_path() . 'resource/front/cjk.ttf',
|
||||
public_path() . 'resource/front/cjk.otf',
|
||||
public_path() . 'resource/front/cjk.ttc',
|
||||
];
|
||||
foreach ($candidates as $path) {
|
||||
if (is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在海报左上区域绘制文字:标题(加粗+4px)→ 分享内容(+4px)
|
||||
* $boundsBottom 为文字区域可使用的底部 y 坐标(即二维码框顶部)
|
||||
*/
|
||||
private static function drawTextLayer($img, string $font, int $bgW, int $boundsBottom, array $textData): void
|
||||
{
|
||||
$padding = (int) ($bgW * 0.06);
|
||||
$maxW = (int) ($bgW - $padding * 2); // 文字区域:全宽(左右留 padding)
|
||||
$cursorY = (int) ($bgW * 0.06);
|
||||
|
||||
$textColor = imagecolorallocate($img, 255, 255, 255);
|
||||
$shadow = imagecolorallocatealpha($img, 0, 0, 0, 70);
|
||||
|
||||
// 海报标题(居中显示)
|
||||
$posterTitle = (string) ($textData['poster_title'] ?? '');
|
||||
if ($posterTitle !== '') {
|
||||
$size = (int) max(34, $bgW * 0.032 + 18);
|
||||
$cursorY = self::drawBoldCenteredLine($img, $font, $size, $textColor, $shadow, $posterTitle, $bgW, $cursorY);
|
||||
$cursorY += (int) max(70, $size * 2.0);
|
||||
}
|
||||
|
||||
// 分享人
|
||||
$nickname = (string) ($textData['sharer']['nickname'] ?? '');
|
||||
if ($nickname !== '') {
|
||||
$size = (int) max(8, $bgW * 0.022 - 4);
|
||||
$cursorY = self::drawBoldLeftLine($img, $font, $size, $textColor, $shadow, '@' . $nickname, $padding, $cursorY);
|
||||
$cursorY += (int) max(70, $size * 2.4);
|
||||
}
|
||||
|
||||
// 标题:左上角,加粗(双层绘制模拟),字体 +16px
|
||||
$title = (string) ($textData['title'] ?? '');
|
||||
if ($title !== '') {
|
||||
$size = (int) max(32, $bgW * 0.028 + 18);
|
||||
$cursorY = self::drawBoldLeftWrappedLines($img, $font, $size, $textColor, $shadow, $title, $maxW, $padding, $cursorY, 3, $boundsBottom);
|
||||
$cursorY += (int) max(70, $size * 1.6);
|
||||
}
|
||||
|
||||
// 分享内容:在标题下方,字体 +8px,加粗
|
||||
$description = (string) ($textData['description'] ?? '');
|
||||
if ($description !== '' && $cursorY + 60 < $boundsBottom) {
|
||||
$size = (int) max(13, $bgW * 0.02 + 2);
|
||||
self::drawBoldLeftWrappedLines($img, $font, $size, $textColor, $shadow, $description, $maxW, $padding, $cursorY, 4, $boundsBottom);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 左对齐单行文字绘制
|
||||
*/
|
||||
private static function drawLeftLine($img, string $font, int $size, int $color, int $shadow, string $text, int $x, int $y): int
|
||||
{
|
||||
$baseline = $y + $size;
|
||||
imagettftext($img, $size, 0, $x + 2, $baseline + 2, $shadow, $font, $text);
|
||||
imagettftext($img, $size, 0, $x, $baseline, $color, $font, $text);
|
||||
return $baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* 左对齐加粗单行文字(三层绘制模拟加粗)
|
||||
*/
|
||||
private static function drawBoldLeftLine($img, string $font, int $size, int $color, int $shadow, string $text, int $x, int $y): int
|
||||
{
|
||||
$baseline = $y + $size;
|
||||
imagettftext($img, $size, 0, $x + 2, $baseline + 2, $shadow, $font, $text);
|
||||
imagettftext($img, $size, 0, $x, $baseline, $color, $font, $text);
|
||||
imagettftext($img, $size, 0, $x + 1, $baseline, $color, $font, $text);
|
||||
imagettftext($img, $size, 0, $x + 2, $baseline, $color, $font, $text);
|
||||
return $baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* 左对齐换行文字
|
||||
*/
|
||||
private static function drawLeftWrappedLines($img, string $font, int $size, int $color, int $shadow, string $text, int $maxW, int $padding, int $y, int $maxLines, int $maxBottom): int
|
||||
{
|
||||
$lines = self::wrapByPixel($text, $font, $size, $maxW, $maxLines);
|
||||
$lineHeight = (int) ($size * 1.4);
|
||||
foreach ($lines as $line) {
|
||||
if ($y + $lineHeight > $maxBottom) {
|
||||
break;
|
||||
}
|
||||
$y = self::drawLeftLine($img, $font, $size, $color, $shadow, $line, $padding, $y);
|
||||
$y += (int) ($size * 0.4);
|
||||
}
|
||||
return $y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 左对齐加粗换行文字
|
||||
*/
|
||||
private static function drawBoldLeftWrappedLines($img, string $font, int $size, int $color, int $shadow, string $text, int $maxW, int $padding, int $y, int $maxLines, int $maxBottom): int
|
||||
{
|
||||
$lines = self::wrapByPixel($text, $font, $size, $maxW, $maxLines);
|
||||
$lineHeight = (int) ($size * 1.6);
|
||||
foreach ($lines as $line) {
|
||||
if ($y + $lineHeight > $maxBottom) {
|
||||
break;
|
||||
}
|
||||
$y = self::drawBoldLeftLine($img, $font, $size, $color, $shadow, $line, $padding, $y);
|
||||
$y += (int) ($size * 1.6);
|
||||
}
|
||||
return $y;
|
||||
}
|
||||
|
||||
private static function drawCenteredLine($img, string $font, int $size, int $color, int $shadow, string $text, int $bgW, int $y): int
|
||||
{
|
||||
$box = imagettfbbox($size, 0, $font, $text);
|
||||
$w = abs($box[2] - $box[0]);
|
||||
$x = (int) (($bgW - $w) / 2);
|
||||
$baseline = $y + $size;
|
||||
imagettftext($img, $size, 0, $x + 2, $baseline + 2, $shadow, $font, $text);
|
||||
imagettftext($img, $size, 0, $x, $baseline, $color, $font, $text);
|
||||
return $baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* 居中加粗单行文字(三层绘制模拟加粗)
|
||||
*/
|
||||
private static function drawBoldCenteredLine($img, string $font, int $size, int $color, int $shadow, string $text, int $bgW, int $y): int
|
||||
{
|
||||
$box = imagettfbbox($size, 0, $font, $text);
|
||||
$w = abs($box[2] - $box[0]);
|
||||
$x = (int) (($bgW - $w) / 2);
|
||||
$baseline = $y + $size;
|
||||
imagettftext($img, $size, 0, $x + 2, $baseline + 2, $shadow, $font, $text);
|
||||
imagettftext($img, $size, 0, $x, $baseline, $color, $font, $text);
|
||||
imagettftext($img, $size, 0, $x + 1, $baseline, $color, $font, $text);
|
||||
imagettftext($img, $size, 0, $x + 2, $baseline, $color, $font, $text);
|
||||
return $baseline;
|
||||
}
|
||||
|
||||
private static function drawWrappedLines($img, string $font, int $size, int $color, int $shadow, string $text, int $maxW, int $bgW, int $y, int $maxLines, int $maxBottom): int
|
||||
{
|
||||
$lines = self::wrapByPixel($text, $font, $size, $maxW, $maxLines);
|
||||
$lineHeight = (int) ($size * 1.4);
|
||||
foreach ($lines as $line) {
|
||||
if ($y + $lineHeight > $maxBottom) {
|
||||
break;
|
||||
}
|
||||
$y = self::drawCenteredLine($img, $font, $size, $color, $shadow, $line, $bgW, $y);
|
||||
$y += (int) ($size * 0.4);
|
||||
}
|
||||
return $y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按像素宽度对文本断行,超过 $maxLines 时末尾追加省略号
|
||||
*/
|
||||
private static function wrapByPixel(string $text, string $font, int $size, int $maxW, int $maxLines): array
|
||||
{
|
||||
$chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
$lines = [];
|
||||
$current = '';
|
||||
foreach ($chars as $ch) {
|
||||
$candidate = $current . $ch;
|
||||
$box = imagettfbbox($size, 0, $font, $candidate);
|
||||
$w = abs($box[2] - $box[0]);
|
||||
if ($w > $maxW) {
|
||||
if ($current !== '') {
|
||||
$lines[] = $current;
|
||||
}
|
||||
$current = $ch;
|
||||
if (count($lines) >= $maxLines) {
|
||||
$lastIdx = count($lines) - 1;
|
||||
$lines[$lastIdx] = mb_substr($lines[$lastIdx], 0, max(0, mb_strlen($lines[$lastIdx]) - 1)) . '…';
|
||||
return $lines;
|
||||
}
|
||||
} else {
|
||||
$current = $candidate;
|
||||
}
|
||||
}
|
||||
if ($current !== '') {
|
||||
$lines[] = $current;
|
||||
}
|
||||
return array_slice($lines, 0, $maxLines);
|
||||
}
|
||||
|
||||
private static function toPng(array $matrix, string $path): void
|
||||
{
|
||||
$quiet = 1;
|
||||
$scale = 10;
|
||||
$size = count($matrix);
|
||||
$imageSize = ($size + $quiet * 2) * $scale;
|
||||
$image = imagecreatetruecolor($imageSize, $imageSize);
|
||||
$white = imagecolorallocate($image, 255, 255, 255);
|
||||
$black = imagecolorallocate($image, 0, 0, 0);
|
||||
imagefill($image, 0, 0, $white);
|
||||
|
||||
for ($y = 0; $y < $size; $y++) {
|
||||
for ($x = 0; $x < $size; $x++) {
|
||||
if ($matrix[$y][$x]) {
|
||||
imagefilledrectangle(
|
||||
$image,
|
||||
($x + $quiet) * $scale,
|
||||
($y + $quiet) * $scale,
|
||||
($x + $quiet + 1) * $scale - 1,
|
||||
($y + $quiet + 1) * $scale - 1,
|
||||
$black
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imagepng($image, $path);
|
||||
imagedestroy($image);
|
||||
}
|
||||
|
||||
public static function get(string $content, string $code): array
|
||||
{
|
||||
$pngUri = 'uploads/qrcode/shortlink/' . $code . '.png';
|
||||
$svgUri = 'uploads/qrcode/shortlink/' . $code . '.svg';
|
||||
if (is_file(public_path() . $pngUri)) {
|
||||
return [
|
||||
'uri' => $pngUri,
|
||||
'url' => FileService::getFileUrl($pngUri),
|
||||
'data_url' => '',
|
||||
];
|
||||
}
|
||||
if (is_file(public_path() . $svgUri)) {
|
||||
return [
|
||||
'uri' => $svgUri,
|
||||
'url' => FileService::getFileUrl($svgUri),
|
||||
'data_url' => '',
|
||||
];
|
||||
}
|
||||
return self::create($content, $code);
|
||||
}
|
||||
|
||||
private static function encode(string $content): array
|
||||
{
|
||||
$version = self::getVersion(strlen($content));
|
||||
$data = self::buildData($content, $version);
|
||||
$codewords = array_merge($data, self::reedSolomonRemainder($data, self::ECC_CODEWORDS[$version]));
|
||||
$size = 17 + 4 * $version;
|
||||
$modules = array_fill(0, $size, array_fill(0, $size, false));
|
||||
$isFunction = array_fill(0, $size, array_fill(0, $size, false));
|
||||
|
||||
self::drawFunctionPatterns($modules, $isFunction, $version);
|
||||
self::drawCodewords($modules, $isFunction, $codewords);
|
||||
self::applyMask($modules, $isFunction);
|
||||
self::drawFormatBits($modules, $isFunction);
|
||||
|
||||
return $modules;
|
||||
}
|
||||
|
||||
private static function getVersion(int $length): int
|
||||
{
|
||||
foreach (self::DATA_CODEWORDS as $version => $capacity) {
|
||||
if ($length <= $capacity) {
|
||||
return $version;
|
||||
}
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
private static function buildData(string $content, int $version): array
|
||||
{
|
||||
$bits = [];
|
||||
self::appendBits($bits, 0x4, 4);
|
||||
self::appendBits($bits, strlen($content), 8);
|
||||
foreach (unpack('C*', $content) as $byte) {
|
||||
self::appendBits($bits, $byte, 8);
|
||||
}
|
||||
|
||||
$capacity = self::DATA_CODEWORDS[$version] * 8;
|
||||
self::appendBits($bits, 0, min(4, $capacity - count($bits)));
|
||||
while (count($bits) % 8 !== 0) {
|
||||
$bits[] = 0;
|
||||
}
|
||||
|
||||
$data = self::bitsToBytes($bits);
|
||||
for ($pad = 0xec; count($data) < self::DATA_CODEWORDS[$version]; $pad ^= 0xfd) {
|
||||
$data[] = $pad;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private static function appendBits(array &$bits, int $value, int $length): void
|
||||
{
|
||||
for ($i = $length - 1; $i >= 0; $i--) {
|
||||
$bits[] = ($value >> $i) & 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static function bitsToBytes(array $bits): array
|
||||
{
|
||||
$bytes = [];
|
||||
for ($i = 0; $i < count($bits); $i += 8) {
|
||||
$byte = 0;
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$byte = ($byte << 1) | ($bits[$i + $j] ?? 0);
|
||||
}
|
||||
$bytes[] = $byte;
|
||||
}
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
private static function initGalois(): void
|
||||
{
|
||||
if (self::$exp) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$exp = array_fill(0, 512, 0);
|
||||
self::$log = array_fill(0, 256, 0);
|
||||
$x = 1;
|
||||
for ($i = 0; $i < 255; $i++) {
|
||||
self::$exp[$i] = $x;
|
||||
self::$log[$x] = $i;
|
||||
$x <<= 1;
|
||||
if ($x & 0x100) {
|
||||
$x ^= 0x11d;
|
||||
}
|
||||
}
|
||||
for ($i = 255; $i < 512; $i++) {
|
||||
self::$exp[$i] = self::$exp[$i - 255];
|
||||
}
|
||||
}
|
||||
|
||||
private static function multiply(int $x, int $y): int
|
||||
{
|
||||
if ($x === 0 || $y === 0) {
|
||||
return 0;
|
||||
}
|
||||
self::initGalois();
|
||||
return self::$exp[self::$log[$x] + self::$log[$y]];
|
||||
}
|
||||
|
||||
private static function reedSolomonDivisor(int $degree): array
|
||||
{
|
||||
self::initGalois();
|
||||
$result = [1];
|
||||
for ($i = 0; $i < $degree; $i++) {
|
||||
$next = array_fill(0, count($result) + 1, 0);
|
||||
foreach ($result as $j => $coefficient) {
|
||||
$next[$j] ^= self::multiply($coefficient, 1);
|
||||
$next[$j + 1] ^= self::multiply($coefficient, self::$exp[$i]);
|
||||
}
|
||||
$result = $next;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function reedSolomonRemainder(array $data, int $degree): array
|
||||
{
|
||||
$divisor = self::reedSolomonDivisor($degree);
|
||||
$result = array_fill(0, $degree, 0);
|
||||
foreach ($data as $byte) {
|
||||
$factor = $byte ^ $result[0];
|
||||
array_shift($result);
|
||||
$result[] = 0;
|
||||
for ($i = 0; $i < $degree; $i++) {
|
||||
$result[$i] ^= self::multiply($divisor[$i + 1], $factor);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function drawFunctionPatterns(array &$modules, array &$isFunction, int $version): void
|
||||
{
|
||||
$size = count($modules);
|
||||
self::drawFinderPattern($modules, $isFunction, 3, 3);
|
||||
self::drawFinderPattern($modules, $isFunction, $size - 4, 3);
|
||||
self::drawFinderPattern($modules, $isFunction, 3, $size - 4);
|
||||
|
||||
for ($i = 8; $i < $size - 8; $i++) {
|
||||
self::setFunctionModule($modules, $isFunction, 6, $i, $i % 2 === 0);
|
||||
self::setFunctionModule($modules, $isFunction, $i, 6, $i % 2 === 0);
|
||||
}
|
||||
|
||||
foreach (self::ALIGNMENT_POSITIONS[$version] as $x) {
|
||||
foreach (self::ALIGNMENT_POSITIONS[$version] as $y) {
|
||||
if (!$isFunction[$y][$x]) {
|
||||
self::drawAlignmentPattern($modules, $isFunction, $x, $y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i <= 8; $i++) {
|
||||
self::setFunctionModule($modules, $isFunction, 8, $i, false);
|
||||
self::setFunctionModule($modules, $isFunction, $i, 8, false);
|
||||
}
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
self::setFunctionModule($modules, $isFunction, $size - 1 - $i, 8, false);
|
||||
self::setFunctionModule($modules, $isFunction, 8, $size - 1 - $i, false);
|
||||
}
|
||||
self::setFunctionModule($modules, $isFunction, 8, $size - 8, true);
|
||||
}
|
||||
|
||||
private static function drawFinderPattern(array &$modules, array &$isFunction, int $x, int $y): void
|
||||
{
|
||||
$size = count($modules);
|
||||
for ($dy = -4; $dy <= 4; $dy++) {
|
||||
for ($dx = -4; $dx <= 4; $dx++) {
|
||||
$xx = $x + $dx;
|
||||
$yy = $y + $dy;
|
||||
if ($xx < 0 || $xx >= $size || $yy < 0 || $yy >= $size) {
|
||||
continue;
|
||||
}
|
||||
$distance = max(abs($dx), abs($dy));
|
||||
self::setFunctionModule($modules, $isFunction, $xx, $yy, $distance !== 2 && $distance !== 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function drawAlignmentPattern(array &$modules, array &$isFunction, int $x, int $y): void
|
||||
{
|
||||
for ($dy = -2; $dy <= 2; $dy++) {
|
||||
for ($dx = -2; $dx <= 2; $dx++) {
|
||||
self::setFunctionModule($modules, $isFunction, $x + $dx, $y + $dy, max(abs($dx), abs($dy)) !== 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function setFunctionModule(array &$modules, array &$isFunction, int $x, int $y, bool $dark): void
|
||||
{
|
||||
$modules[$y][$x] = $dark;
|
||||
$isFunction[$y][$x] = true;
|
||||
}
|
||||
|
||||
private static function drawCodewords(array &$modules, array $isFunction, array $codewords): void
|
||||
{
|
||||
$size = count($modules);
|
||||
$bitIndex = 0;
|
||||
$totalBits = count($codewords) * 8;
|
||||
$upward = true;
|
||||
for ($right = $size - 1; $right >= 1; $right -= 2) {
|
||||
if ($right === 6) {
|
||||
$right--;
|
||||
}
|
||||
for ($vert = 0; $vert < $size; $vert++) {
|
||||
$y = $upward ? $size - 1 - $vert : $vert;
|
||||
for ($j = 0; $j < 2; $j++) {
|
||||
$x = $right - $j;
|
||||
if ($isFunction[$y][$x]) {
|
||||
continue;
|
||||
}
|
||||
$dark = false;
|
||||
if ($bitIndex < $totalBits) {
|
||||
$dark = (($codewords[intdiv($bitIndex, 8)] >> (7 - ($bitIndex % 8))) & 1) !== 0;
|
||||
}
|
||||
$modules[$y][$x] = $dark;
|
||||
$bitIndex++;
|
||||
}
|
||||
}
|
||||
$upward = !$upward;
|
||||
}
|
||||
}
|
||||
|
||||
private static function applyMask(array &$modules, array $isFunction): void
|
||||
{
|
||||
$size = count($modules);
|
||||
for ($y = 0; $y < $size; $y++) {
|
||||
for ($x = 0; $x < $size; $x++) {
|
||||
if (!$isFunction[$y][$x] && (($x + $y) % 2 === 0)) {
|
||||
$modules[$y][$x] = !$modules[$y][$x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function drawFormatBits(array &$modules, array &$isFunction): void
|
||||
{
|
||||
$size = count($modules);
|
||||
$bits = self::getFormatBits();
|
||||
for ($i = 0; $i <= 5; $i++) {
|
||||
self::setFunctionModule($modules, $isFunction, 8, $i, self::getBit($bits, $i));
|
||||
}
|
||||
self::setFunctionModule($modules, $isFunction, 8, 7, self::getBit($bits, 6));
|
||||
self::setFunctionModule($modules, $isFunction, 8, 8, self::getBit($bits, 7));
|
||||
self::setFunctionModule($modules, $isFunction, 7, 8, self::getBit($bits, 8));
|
||||
for ($i = 9; $i < 15; $i++) {
|
||||
self::setFunctionModule($modules, $isFunction, 14 - $i, 8, self::getBit($bits, $i));
|
||||
}
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
self::setFunctionModule($modules, $isFunction, $size - 1 - $i, 8, self::getBit($bits, $i));
|
||||
}
|
||||
for ($i = 8; $i < 15; $i++) {
|
||||
self::setFunctionModule($modules, $isFunction, 8, $size - 15 + $i, self::getBit($bits, $i));
|
||||
}
|
||||
self::setFunctionModule($modules, $isFunction, 8, $size - 8, true);
|
||||
}
|
||||
|
||||
private static function getFormatBits(): int
|
||||
{
|
||||
$data = 1 << 3;
|
||||
$remainder = $data << 10;
|
||||
for ($i = 14; $i >= 10; $i--) {
|
||||
if ((($remainder >> $i) & 1) !== 0) {
|
||||
$remainder ^= 0x537 << ($i - 10);
|
||||
}
|
||||
}
|
||||
return (($data << 10) | $remainder) ^ 0x5412;
|
||||
}
|
||||
|
||||
private static function getBit(int $value, int $index): bool
|
||||
{
|
||||
return (($value >> $index) & 1) !== 0;
|
||||
}
|
||||
|
||||
private static function toSvg(array $matrix): string
|
||||
{
|
||||
$quiet = 1;
|
||||
$size = count($matrix);
|
||||
$viewBox = $size + $quiet * 2;
|
||||
$path = '';
|
||||
for ($y = 0; $y < $size; $y++) {
|
||||
for ($x = 0; $x < $size; $x++) {
|
||||
if ($matrix[$y][$x]) {
|
||||
$path .= 'M' . ($x + $quiet) . ',' . ($y + $quiet) . 'h1v1h-1z';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' . $viewBox . ' ' . $viewBox . '" width="360" height="360" shape-rendering="crispEdges"><rect width="100%" height="100%" fill="#fff"/><path d="' . $path . '" fill="#000"/></svg>';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user