composer.json000064400000001751150250215260007270 0ustar00{ "name": "bacon/bacon-qr-code", "description": "BaconQrCode is a QR code generator for PHP.", "license" : "BSD-2-Clause", "homepage": "https://github.com/Bacon/BaconQrCode", "require": { "php": "^7.1 || ^8.0", "ext-iconv": "*", "dasprid/enum": "^1.0.3" }, "suggest": { "ext-imagick": "to generate QR code images" }, "authors": [ { "name": "Ben Scholzen 'DASPRiD'", "email": "mail@dasprids.de", "homepage": "https://dasprids.de/", "role": "Developer" } ], "autoload": { "psr-4": { "BaconQrCode\\": "src/" } }, "require-dev": { "phpunit/phpunit": "^7 | ^8 | ^9", "spatie/phpunit-snapshot-assertions": "^4.2.9", "squizlabs/php_codesniffer": "^3.4", "phly/keep-a-changelog": "^2.1" }, "config": { "allow-plugins": { "ocramius/package-versions": true } } } src/Common/FormatInformation.php000064400000013242150250215260012732 0ustar00ecLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x3); $this->dataMask = $formatInfo & 0x7; } /** * Checks how many bits are different between two integers. */ public static function numBitsDiffering(int $a, int $b) : int { $a ^= $b; return ( self::BITS_SET_IN_HALF_BYTE[$a & 0xf] + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 4) & 0xf)] + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 8) & 0xf)] + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 12) & 0xf)] + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 16) & 0xf)] + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 20) & 0xf)] + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 24) & 0xf)] + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 28) & 0xf)] ); } /** * Decodes format information. */ public static function decodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self { $formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2); if (null !== $formatInfo) { return $formatInfo; } // Should return null, but, some QR codes apparently do not mask this info. Try again by actually masking the // pattern first. return self::doDecodeFormatInformation( $maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR, $maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR ); } /** * Internal method for decoding format information. */ private static function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self { $bestDifference = PHP_INT_MAX; $bestFormatInfo = 0; foreach (self::FORMAT_INFO_DECODE_LOOKUP as $decodeInfo) { $targetInfo = $decodeInfo[0]; if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) { // Found an exact match return new self($decodeInfo[1]); } $bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo); if ($bitsDifference < $bestDifference) { $bestFormatInfo = $decodeInfo[1]; $bestDifference = $bitsDifference; } if ($maskedFormatInfo1 !== $maskedFormatInfo2) { // Also try the other option $bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo); if ($bitsDifference < $bestDifference) { $bestFormatInfo = $decodeInfo[1]; $bestDifference = $bitsDifference; } } } // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match. if ($bestDifference <= 3) { return new self($bestFormatInfo); } return null; } /** * Returns the error correction level. */ public function getErrorCorrectionLevel() : ErrorCorrectionLevel { return $this->ecLevel; } /** * Returns the data mask. */ public function getDataMask() : int { return $this->dataMask; } /** * Hashes the code of the EC level. */ public function hashCode() : int { return ($this->ecLevel->getBits() << 3) | $this->dataMask; } /** * Verifies if this instance equals another one. */ public function equals(self $other) : bool { return ( $this->ecLevel === $other->ecLevel && $this->dataMask === $other->dataMask ); } } src/Common/EcBlock.php000064400000001750150250215260010577 0ustar00count = $count; $this->dataCodewords = $dataCodewords; } /** * Returns how many times the block is used. */ public function getCount() : int { return $this->count; } /** * Returns the number of data codewords. */ public function getDataCodewords() : int { return $this->dataCodewords; } } src/Common/ReedSolomonCodec.php000064400000035071150250215260012464 0ustar00 8) { throw new InvalidArgumentException('Symbol size must be between 0 and 8'); } if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) { throw new InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize)); } if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) { throw new InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize)); } if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) { throw new InvalidArgumentException( 'Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots) ); } $this->symbolSize = $symbolSize; $this->blockSize = (1 << $symbolSize) - 1; $this->padding = $padding; $this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false); $this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false); // Generate galous field lookup table $this->indexOf[0] = $this->blockSize; $this->alphaTo[$this->blockSize] = 0; $sr = 1; for ($i = 0; $i < $this->blockSize; ++$i) { $this->indexOf[$sr] = $i; $this->alphaTo[$i] = $sr; $sr <<= 1; if ($sr & (1 << $symbolSize)) { $sr ^= $gfPoly; } $sr &= $this->blockSize; } if (1 !== $sr) { throw new RuntimeException('Field generator polynomial is not primitive'); } // Form RS code generator polynomial from its roots $this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false); $this->firstRoot = $firstRoot; $this->primitive = $primitive; $this->numRoots = $numRoots; // Find prim-th root of 1, used in decoding for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize) { } $this->iPrimitive = intdiv($iPrimitive, $primitive); $this->generatorPoly[0] = 1; for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; ++$i, $root += $primitive) { $this->generatorPoly[$i + 1] = 1; for ($j = $i; $j > 0; $j--) { if ($this->generatorPoly[$j] !== 0) { $this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[ $this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root) ]; } else { $this->generatorPoly[$j] = $this->generatorPoly[$j - 1]; } } $this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)]; } // Convert generator poly to index form for quicker encoding for ($i = 0; $i <= $numRoots; ++$i) { $this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]]; } } /** * Encodes data and writes result back into parity array. */ public function encode(SplFixedArray $data, SplFixedArray $parity) : void { for ($i = 0; $i < $this->numRoots; ++$i) { $parity[$i] = 0; } $iterations = $this->blockSize - $this->numRoots - $this->padding; for ($i = 0; $i < $iterations; ++$i) { $feedback = $this->indexOf[$data[$i] ^ $parity[0]]; if ($feedback !== $this->blockSize) { // Feedback term is non-zero $feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback); for ($j = 1; $j < $this->numRoots; ++$j) { $parity[$j] = $parity[$j] ^ $this->alphaTo[ $this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j]) ]; } } for ($j = 0; $j < $this->numRoots - 1; ++$j) { $parity[$j] = $parity[$j + 1]; } if ($feedback !== $this->blockSize) { $parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])]; } else { $parity[$this->numRoots - 1] = 0; } } } /** * Decodes received data. */ public function decode(SplFixedArray $data, SplFixedArray $erasures = null) : ?int { // This speeds up the initialization a bit. $numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false); $numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false); $lambda = clone $numRootsPlusOne; $b = clone $numRootsPlusOne; $t = clone $numRootsPlusOne; $omega = clone $numRootsPlusOne; $root = clone $numRoots; $loc = clone $numRoots; $numErasures = (null !== $erasures ? count($erasures) : 0); // Form the Syndromes; i.e., evaluate data(x) at roots of g(x) $syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false); for ($i = 1; $i < $this->blockSize - $this->padding; ++$i) { for ($j = 0; $j < $this->numRoots; ++$j) { if ($syndromes[$j] === 0) { $syndromes[$j] = $data[$i]; } else { $syndromes[$j] = $data[$i] ^ $this->alphaTo[ $this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive) ]; } } } // Convert syndromes to index form, checking for nonzero conditions $syndromeError = 0; for ($i = 0; $i < $this->numRoots; ++$i) { $syndromeError |= $syndromes[$i]; $syndromes[$i] = $this->indexOf[$syndromes[$i]]; } if (! $syndromeError) { // If syndrome is zero, data[] is a codeword and there are no errors to correct, so return data[] // unmodified. return 0; } $lambda[0] = 1; if ($numErasures > 0) { // Init lambda to be the erasure locator polynomial $lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))]; for ($i = 1; $i < $numErasures; ++$i) { $u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i])); for ($j = $i + 1; $j > 0; --$j) { $tmp = $this->indexOf[$lambda[$j - 1]]; if ($tmp !== $this->blockSize) { $lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)]; } } } } for ($i = 0; $i <= $this->numRoots; ++$i) { $b[$i] = $this->indexOf[$lambda[$i]]; } // Begin Berlekamp-Massey algorithm to determine error+erasure locator polynomial $r = $numErasures; $el = $numErasures; while (++$r <= $this->numRoots) { // Compute discrepancy at the r-th step in poly form $discrepancyR = 0; for ($i = 0; $i < $r; ++$i) { if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) { $discrepancyR ^= $this->alphaTo[ $this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1]) ]; } } $discrepancyR = $this->indexOf[$discrepancyR]; if ($discrepancyR === $this->blockSize) { $tmp = $b->toArray(); array_unshift($tmp, $this->blockSize); array_pop($tmp); $b = SplFixedArray::fromArray($tmp, false); continue; } $t[0] = $lambda[0]; for ($i = 0; $i < $this->numRoots; ++$i) { if ($b[$i] !== $this->blockSize) { $t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])]; } else { $t[$i + 1] = $lambda[$i + 1]; } } if (2 * $el <= $r + $numErasures - 1) { $el = $r + $numErasures - $el; for ($i = 0; $i <= $this->numRoots; ++$i) { $b[$i] = ( $lambda[$i] === 0 ? $this->blockSize : $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize) ); } } else { $tmp = $b->toArray(); array_unshift($tmp, $this->blockSize); array_pop($tmp); $b = SplFixedArray::fromArray($tmp, false); } $lambda = clone $t; } // Convert lambda to index form and compute deg(lambda(x)) $degLambda = 0; for ($i = 0; $i <= $this->numRoots; ++$i) { $lambda[$i] = $this->indexOf[$lambda[$i]]; if ($lambda[$i] !== $this->blockSize) { $degLambda = $i; } } // Find roots of the error+erasure locator polynomial by Chien search. $reg = clone $lambda; $reg[0] = 0; $count = 0; $i = 1; for ($k = $this->iPrimitive - 1; $i <= $this->blockSize; ++$i, $k = $this->modNn($k + $this->iPrimitive)) { $q = 1; for ($j = $degLambda; $j > 0; $j--) { if ($reg[$j] !== $this->blockSize) { $reg[$j] = $this->modNn($reg[$j] + $j); $q ^= $this->alphaTo[$reg[$j]]; } } if ($q !== 0) { // Not a root continue; } // Store root (index-form) and error location number $root[$count] = $i; $loc[$count] = $k; if (++$count === $degLambda) { break; } } if ($degLambda !== $count) { // deg(lambda) unequal to number of roots: uncorrectable error detected return null; } // Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo x**numRoots). In index form. Also find // deg(omega). $degOmega = $degLambda - 1; for ($i = 0; $i <= $degOmega; ++$i) { $tmp = 0; for ($j = $i; $j >= 0; --$j) { if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) { $tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])]; } } $omega[$i] = $this->indexOf[$tmp]; } // Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = inv(X(l))**(firstRoot-1) and // den = lambda_pr(inv(X(l))) all in poly form. for ($j = $count - 1; $j >= 0; --$j) { $num1 = 0; for ($i = $degOmega; $i >= 0; $i--) { if ($omega[$i] !== $this->blockSize) { $num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])]; } } $num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)]; $den = 0; // lambda[i+1] for i even is the formal derivativelambda_pr of lambda[i] for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) { if ($lambda[$i + 1] !== $this->blockSize) { $den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])]; } } // Apply error to data if ($num1 !== 0 && $loc[$j] >= $this->padding) { $data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ ( $this->alphaTo[ $this->modNn( $this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den] ) ] ); } } if (null !== $erasures) { if (count($erasures) < $count) { $erasures->setSize($count); } for ($i = 0; $i < $count; $i++) { $erasures[$i] = $loc[$i]; } } return $count; } /** * Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow divide. */ private function modNn(int $x) : int { while ($x >= $this->blockSize) { $x -= $this->blockSize; $x = ($x >> $this->symbolSize) + ($x & $this->blockSize); } return $x; } } src/Common/EcBlocks.php000064400000003212150250215260010755 0ustar00ecCodewordsPerBlock = $ecCodewordsPerBlock; $this->ecBlocks = $ecBlocks; } /** * Returns the number of EC codewords per block. */ public function getEcCodewordsPerBlock() : int { return $this->ecCodewordsPerBlock; } /** * Returns the total number of EC block appearances. */ public function getNumBlocks() : int { $total = 0; foreach ($this->ecBlocks as $ecBlock) { $total += $ecBlock->getCount(); } return $total; } /** * Returns the total count of EC codewords. */ public function getTotalEcCodewords() : int { return $this->ecCodewordsPerBlock * $this->getNumBlocks(); } /** * Returns the EC blocks included in this collection. * * @return EcBlock[] */ public function getEcBlocks() : array { return $this->ecBlocks; } } src/Common/Mode.php000064400000003774150250215260010171 0ustar00characterCountBitsForVersions = $characterCountBitsForVersions; $this->bits = $bits; } /** * Returns the number of bits used in a specific QR code version. */ public function getCharacterCountBits(Version $version) : int { $number = $version->getVersionNumber(); if ($number <= 9) { $offset = 0; } elseif ($number <= 26) { $offset = 1; } else { $offset = 2; } return $this->characterCountBitsForVersions[$offset]; } /** * Returns the four bits used to encode this mode. */ public function getBits() : int { return $this->bits; } } src/Common/CharacterSetEci.php000064400000012270150250215260012265 0ustar00|null */ private static $valueToEci; /** * @var array|null */ private static $nameToEci; public function __construct(array $values, string ...$otherEncodingNames) { $this->values = $values; $this->otherEncodingNames = $otherEncodingNames; } /** * Returns the primary value. */ public function getValue() : int { return $this->values[0]; } /** * Gets character set ECI by value. * * Returns the representing ECI of a given value, or null if it is legal but unsupported. * * @throws InvalidArgumentException if value is not between 0 and 900 */ public static function getCharacterSetEciByValue(int $value) : ?self { if ($value < 0 || $value >= 900) { throw new InvalidArgumentException('Value must be between 0 and 900'); } $valueToEci = self::valueToEci(); if (! array_key_exists($value, $valueToEci)) { return null; } return $valueToEci[$value]; } /** * Returns character set ECI by name. * * Returns the representing ECI of a given name, or null if it is legal but unsupported */ public static function getCharacterSetEciByName(string $name) : ?self { $nameToEci = self::nameToEci(); $name = strtolower($name); if (! array_key_exists($name, $nameToEci)) { return null; } return $nameToEci[$name]; } private static function valueToEci() : array { if (null !== self::$valueToEci) { return self::$valueToEci; } self::$valueToEci = []; foreach (self::values() as $eci) { foreach ($eci->values as $value) { self::$valueToEci[$value] = $eci; } } return self::$valueToEci; } private static function nameToEci() : array { if (null !== self::$nameToEci) { return self::$nameToEci; } self::$nameToEci = []; foreach (self::values() as $eci) { self::$nameToEci[strtolower($eci->name())] = $eci; foreach ($eci->otherEncodingNames as $name) { self::$nameToEci[strtolower($name)] = $eci; } } return self::$nameToEci; } } src/Common/Version.php000064400000052560150250215260010727 0ustar00|null */ private static $versions; /** * @param int[] $alignmentPatternCenters */ private function __construct( int $versionNumber, array $alignmentPatternCenters, EcBlocks ...$ecBlocks ) { $this->versionNumber = $versionNumber; $this->alignmentPatternCenters = $alignmentPatternCenters; $this->ecBlocks = $ecBlocks; $totalCodewords = 0; $ecCodewords = $ecBlocks[0]->getEcCodewordsPerBlock(); foreach ($ecBlocks[0]->getEcBlocks() as $ecBlock) { $totalCodewords += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords); } $this->totalCodewords = $totalCodewords; } /** * Returns the version number. */ public function getVersionNumber() : int { return $this->versionNumber; } /** * Returns the alignment pattern centers. * * @return int[] */ public function getAlignmentPatternCenters() : array { return $this->alignmentPatternCenters; } /** * Returns the total number of codewords. */ public function getTotalCodewords() : int { return $this->totalCodewords; } /** * Calculates the dimension for the current version. */ public function getDimensionForVersion() : int { return 17 + 4 * $this->versionNumber; } /** * Returns the number of EC blocks for a specific EC level. */ public function getEcBlocksForLevel(ErrorCorrectionLevel $ecLevel) : EcBlocks { return $this->ecBlocks[$ecLevel->ordinal()]; } /** * Gets a provisional version number for a specific dimension. * * @throws InvalidArgumentException if dimension is not 1 mod 4 */ public static function getProvisionalVersionForDimension(int $dimension) : self { if (1 !== $dimension % 4) { throw new InvalidArgumentException('Dimension is not 1 mod 4'); } return self::getVersionForNumber(intdiv($dimension - 17, 4)); } /** * Gets a version instance for a specific version number. * * @throws InvalidArgumentException if version number is out of range */ public static function getVersionForNumber(int $versionNumber) : self { if ($versionNumber < 1 || $versionNumber > 40) { throw new InvalidArgumentException('Version number must be between 1 and 40'); } return self::versions()[$versionNumber - 1]; } /** * Decodes version information from an integer and returns the version. */ public static function decodeVersionInformation(int $versionBits) : ?self { $bestDifference = PHP_INT_MAX; $bestVersion = 0; foreach (self::VERSION_DECODE_INFO as $i => $targetVersion) { if ($targetVersion === $versionBits) { return self::getVersionForNumber($i + 7); } $bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion); if ($bitsDifference < $bestDifference) { $bestVersion = $i + 7; $bestDifference = $bitsDifference; } } if ($bestDifference <= 3) { return self::getVersionForNumber($bestVersion); } return null; } /** * Builds the function pattern for the current version. */ public function buildFunctionPattern() : BitMatrix { $dimension = $this->getDimensionForVersion(); $bitMatrix = new BitMatrix($dimension); // Top left finder pattern + separator + format $bitMatrix->setRegion(0, 0, 9, 9); // Top right finder pattern + separator + format $bitMatrix->setRegion($dimension - 8, 0, 8, 9); // Bottom left finder pattern + separator + format $bitMatrix->setRegion(0, $dimension - 8, 9, 8); // Alignment patterns $max = count($this->alignmentPatternCenters); for ($x = 0; $x < $max; ++$x) { $i = $this->alignmentPatternCenters[$x] - 2; for ($y = 0; $y < $max; ++$y) { if (($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)) { // No alignment patterns near the three finder paterns continue; } $bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5); } } // Vertical timing pattern $bitMatrix->setRegion(6, 9, 1, $dimension - 17); // Horizontal timing pattern $bitMatrix->setRegion(9, 6, $dimension - 17, 1); if ($this->versionNumber > 6) { // Version info, top right $bitMatrix->setRegion($dimension - 11, 0, 3, 6); // Version info, bottom left $bitMatrix->setRegion(0, $dimension - 11, 6, 3); } return $bitMatrix; } /** * Returns a string representation for the version. */ public function __toString() : string { return (string) $this->versionNumber; } /** * Build and cache a specific version. * * See ISO 18004:2006 6.5.1 Table 9. * * @return array */ private static function versions() : array { if (null !== self::$versions) { return self::$versions; } return self::$versions = [ new self( 1, [], new EcBlocks(7, new EcBlock(1, 19)), new EcBlocks(10, new EcBlock(1, 16)), new EcBlocks(13, new EcBlock(1, 13)), new EcBlocks(17, new EcBlock(1, 9)) ), new self( 2, [6, 18], new EcBlocks(10, new EcBlock(1, 34)), new EcBlocks(16, new EcBlock(1, 28)), new EcBlocks(22, new EcBlock(1, 22)), new EcBlocks(28, new EcBlock(1, 16)) ), new self( 3, [6, 22], new EcBlocks(15, new EcBlock(1, 55)), new EcBlocks(26, new EcBlock(1, 44)), new EcBlocks(18, new EcBlock(2, 17)), new EcBlocks(22, new EcBlock(2, 13)) ), new self( 4, [6, 26], new EcBlocks(20, new EcBlock(1, 80)), new EcBlocks(18, new EcBlock(2, 32)), new EcBlocks(26, new EcBlock(3, 24)), new EcBlocks(16, new EcBlock(4, 9)) ), new self( 5, [6, 30], new EcBlocks(26, new EcBlock(1, 108)), new EcBlocks(24, new EcBlock(2, 43)), new EcBlocks(18, new EcBlock(2, 15), new EcBlock(2, 16)), new EcBlocks(22, new EcBlock(2, 11), new EcBlock(2, 12)) ), new self( 6, [6, 34], new EcBlocks(18, new EcBlock(2, 68)), new EcBlocks(16, new EcBlock(4, 27)), new EcBlocks(24, new EcBlock(4, 19)), new EcBlocks(28, new EcBlock(4, 15)) ), new self( 7, [6, 22, 38], new EcBlocks(20, new EcBlock(2, 78)), new EcBlocks(18, new EcBlock(4, 31)), new EcBlocks(18, new EcBlock(2, 14), new EcBlock(4, 15)), new EcBlocks(26, new EcBlock(4, 13), new EcBlock(1, 14)) ), new self( 8, [6, 24, 42], new EcBlocks(24, new EcBlock(2, 97)), new EcBlocks(22, new EcBlock(2, 38), new EcBlock(2, 39)), new EcBlocks(22, new EcBlock(4, 18), new EcBlock(2, 19)), new EcBlocks(26, new EcBlock(4, 14), new EcBlock(2, 15)) ), new self( 9, [6, 26, 46], new EcBlocks(30, new EcBlock(2, 116)), new EcBlocks(22, new EcBlock(3, 36), new EcBlock(2, 37)), new EcBlocks(20, new EcBlock(4, 16), new EcBlock(4, 17)), new EcBlocks(24, new EcBlock(4, 12), new EcBlock(4, 13)) ), new self( 10, [6, 28, 50], new EcBlocks(18, new EcBlock(2, 68), new EcBlock(2, 69)), new EcBlocks(26, new EcBlock(4, 43), new EcBlock(1, 44)), new EcBlocks(24, new EcBlock(6, 19), new EcBlock(2, 20)), new EcBlocks(28, new EcBlock(6, 15), new EcBlock(2, 16)) ), new self( 11, [6, 30, 54], new EcBlocks(20, new EcBlock(4, 81)), new EcBlocks(30, new EcBlock(1, 50), new EcBlock(4, 51)), new EcBlocks(28, new EcBlock(4, 22), new EcBlock(4, 23)), new EcBlocks(24, new EcBlock(3, 12), new EcBlock(8, 13)) ), new self( 12, [6, 32, 58], new EcBlocks(24, new EcBlock(2, 92), new EcBlock(2, 93)), new EcBlocks(22, new EcBlock(6, 36), new EcBlock(2, 37)), new EcBlocks(26, new EcBlock(4, 20), new EcBlock(6, 21)), new EcBlocks(28, new EcBlock(7, 14), new EcBlock(4, 15)) ), new self( 13, [6, 34, 62], new EcBlocks(26, new EcBlock(4, 107)), new EcBlocks(22, new EcBlock(8, 37), new EcBlock(1, 38)), new EcBlocks(24, new EcBlock(8, 20), new EcBlock(4, 21)), new EcBlocks(22, new EcBlock(12, 11), new EcBlock(4, 12)) ), new self( 14, [6, 26, 46, 66], new EcBlocks(30, new EcBlock(3, 115), new EcBlock(1, 116)), new EcBlocks(24, new EcBlock(4, 40), new EcBlock(5, 41)), new EcBlocks(20, new EcBlock(11, 16), new EcBlock(5, 17)), new EcBlocks(24, new EcBlock(11, 12), new EcBlock(5, 13)) ), new self( 15, [6, 26, 48, 70], new EcBlocks(22, new EcBlock(5, 87), new EcBlock(1, 88)), new EcBlocks(24, new EcBlock(5, 41), new EcBlock(5, 42)), new EcBlocks(30, new EcBlock(5, 24), new EcBlock(7, 25)), new EcBlocks(24, new EcBlock(11, 12), new EcBlock(7, 13)) ), new self( 16, [6, 26, 50, 74], new EcBlocks(24, new EcBlock(5, 98), new EcBlock(1, 99)), new EcBlocks(28, new EcBlock(7, 45), new EcBlock(3, 46)), new EcBlocks(24, new EcBlock(15, 19), new EcBlock(2, 20)), new EcBlocks(30, new EcBlock(3, 15), new EcBlock(13, 16)) ), new self( 17, [6, 30, 54, 78], new EcBlocks(28, new EcBlock(1, 107), new EcBlock(5, 108)), new EcBlocks(28, new EcBlock(10, 46), new EcBlock(1, 47)), new EcBlocks(28, new EcBlock(1, 22), new EcBlock(15, 23)), new EcBlocks(28, new EcBlock(2, 14), new EcBlock(17, 15)) ), new self( 18, [6, 30, 56, 82], new EcBlocks(30, new EcBlock(5, 120), new EcBlock(1, 121)), new EcBlocks(26, new EcBlock(9, 43), new EcBlock(4, 44)), new EcBlocks(28, new EcBlock(17, 22), new EcBlock(1, 23)), new EcBlocks(28, new EcBlock(2, 14), new EcBlock(19, 15)) ), new self( 19, [6, 30, 58, 86], new EcBlocks(28, new EcBlock(3, 113), new EcBlock(4, 114)), new EcBlocks(26, new EcBlock(3, 44), new EcBlock(11, 45)), new EcBlocks(26, new EcBlock(17, 21), new EcBlock(4, 22)), new EcBlocks(26, new EcBlock(9, 13), new EcBlock(16, 14)) ), new self( 20, [6, 34, 62, 90], new EcBlocks(28, new EcBlock(3, 107), new EcBlock(5, 108)), new EcBlocks(26, new EcBlock(3, 41), new EcBlock(13, 42)), new EcBlocks(30, new EcBlock(15, 24), new EcBlock(5, 25)), new EcBlocks(28, new EcBlock(15, 15), new EcBlock(10, 16)) ), new self( 21, [6, 28, 50, 72, 94], new EcBlocks(28, new EcBlock(4, 116), new EcBlock(4, 117)), new EcBlocks(26, new EcBlock(17, 42)), new EcBlocks(28, new EcBlock(17, 22), new EcBlock(6, 23)), new EcBlocks(30, new EcBlock(19, 16), new EcBlock(6, 17)) ), new self( 22, [6, 26, 50, 74, 98], new EcBlocks(28, new EcBlock(2, 111), new EcBlock(7, 112)), new EcBlocks(28, new EcBlock(17, 46)), new EcBlocks(30, new EcBlock(7, 24), new EcBlock(16, 25)), new EcBlocks(24, new EcBlock(34, 13)) ), new self( 23, [6, 30, 54, 78, 102], new EcBlocks(30, new EcBlock(4, 121), new EcBlock(5, 122)), new EcBlocks(28, new EcBlock(4, 47), new EcBlock(14, 48)), new EcBlocks(30, new EcBlock(11, 24), new EcBlock(14, 25)), new EcBlocks(30, new EcBlock(16, 15), new EcBlock(14, 16)) ), new self( 24, [6, 28, 54, 80, 106], new EcBlocks(30, new EcBlock(6, 117), new EcBlock(4, 118)), new EcBlocks(28, new EcBlock(6, 45), new EcBlock(14, 46)), new EcBlocks(30, new EcBlock(11, 24), new EcBlock(16, 25)), new EcBlocks(30, new EcBlock(30, 16), new EcBlock(2, 17)) ), new self( 25, [6, 32, 58, 84, 110], new EcBlocks(26, new EcBlock(8, 106), new EcBlock(4, 107)), new EcBlocks(28, new EcBlock(8, 47), new EcBlock(13, 48)), new EcBlocks(30, new EcBlock(7, 24), new EcBlock(22, 25)), new EcBlocks(30, new EcBlock(22, 15), new EcBlock(13, 16)) ), new self( 26, [6, 30, 58, 86, 114], new EcBlocks(28, new EcBlock(10, 114), new EcBlock(2, 115)), new EcBlocks(28, new EcBlock(19, 46), new EcBlock(4, 47)), new EcBlocks(28, new EcBlock(28, 22), new EcBlock(6, 23)), new EcBlocks(30, new EcBlock(33, 16), new EcBlock(4, 17)) ), new self( 27, [6, 34, 62, 90, 118], new EcBlocks(30, new EcBlock(8, 122), new EcBlock(4, 123)), new EcBlocks(28, new EcBlock(22, 45), new EcBlock(3, 46)), new EcBlocks(30, new EcBlock(8, 23), new EcBlock(26, 24)), new EcBlocks(30, new EcBlock(12, 15), new EcBlock(28, 16)) ), new self( 28, [6, 26, 50, 74, 98, 122], new EcBlocks(30, new EcBlock(3, 117), new EcBlock(10, 118)), new EcBlocks(28, new EcBlock(3, 45), new EcBlock(23, 46)), new EcBlocks(30, new EcBlock(4, 24), new EcBlock(31, 25)), new EcBlocks(30, new EcBlock(11, 15), new EcBlock(31, 16)) ), new self( 29, [6, 30, 54, 78, 102, 126], new EcBlocks(30, new EcBlock(7, 116), new EcBlock(7, 117)), new EcBlocks(28, new EcBlock(21, 45), new EcBlock(7, 46)), new EcBlocks(30, new EcBlock(1, 23), new EcBlock(37, 24)), new EcBlocks(30, new EcBlock(19, 15), new EcBlock(26, 16)) ), new self( 30, [6, 26, 52, 78, 104, 130], new EcBlocks(30, new EcBlock(5, 115), new EcBlock(10, 116)), new EcBlocks(28, new EcBlock(19, 47), new EcBlock(10, 48)), new EcBlocks(30, new EcBlock(15, 24), new EcBlock(25, 25)), new EcBlocks(30, new EcBlock(23, 15), new EcBlock(25, 16)) ), new self( 31, [6, 30, 56, 82, 108, 134], new EcBlocks(30, new EcBlock(13, 115), new EcBlock(3, 116)), new EcBlocks(28, new EcBlock(2, 46), new EcBlock(29, 47)), new EcBlocks(30, new EcBlock(42, 24), new EcBlock(1, 25)), new EcBlocks(30, new EcBlock(23, 15), new EcBlock(28, 16)) ), new self( 32, [6, 34, 60, 86, 112, 138], new EcBlocks(30, new EcBlock(17, 115)), new EcBlocks(28, new EcBlock(10, 46), new EcBlock(23, 47)), new EcBlocks(30, new EcBlock(10, 24), new EcBlock(35, 25)), new EcBlocks(30, new EcBlock(19, 15), new EcBlock(35, 16)) ), new self( 33, [6, 30, 58, 86, 114, 142], new EcBlocks(30, new EcBlock(17, 115), new EcBlock(1, 116)), new EcBlocks(28, new EcBlock(14, 46), new EcBlock(21, 47)), new EcBlocks(30, new EcBlock(29, 24), new EcBlock(19, 25)), new EcBlocks(30, new EcBlock(11, 15), new EcBlock(46, 16)) ), new self( 34, [6, 34, 62, 90, 118, 146], new EcBlocks(30, new EcBlock(13, 115), new EcBlock(6, 116)), new EcBlocks(28, new EcBlock(14, 46), new EcBlock(23, 47)), new EcBlocks(30, new EcBlock(44, 24), new EcBlock(7, 25)), new EcBlocks(30, new EcBlock(59, 16), new EcBlock(1, 17)) ), new self( 35, [6, 30, 54, 78, 102, 126, 150], new EcBlocks(30, new EcBlock(12, 121), new EcBlock(7, 122)), new EcBlocks(28, new EcBlock(12, 47), new EcBlock(26, 48)), new EcBlocks(30, new EcBlock(39, 24), new EcBlock(14, 25)), new EcBlocks(30, new EcBlock(22, 15), new EcBlock(41, 16)) ), new self( 36, [6, 24, 50, 76, 102, 128, 154], new EcBlocks(30, new EcBlock(6, 121), new EcBlock(14, 122)), new EcBlocks(28, new EcBlock(6, 47), new EcBlock(34, 48)), new EcBlocks(30, new EcBlock(46, 24), new EcBlock(10, 25)), new EcBlocks(30, new EcBlock(2, 15), new EcBlock(64, 16)) ), new self( 37, [6, 28, 54, 80, 106, 132, 158], new EcBlocks(30, new EcBlock(17, 122), new EcBlock(4, 123)), new EcBlocks(28, new EcBlock(29, 46), new EcBlock(14, 47)), new EcBlocks(30, new EcBlock(49, 24), new EcBlock(10, 25)), new EcBlocks(30, new EcBlock(24, 15), new EcBlock(46, 16)) ), new self( 38, [6, 32, 58, 84, 110, 136, 162], new EcBlocks(30, new EcBlock(4, 122), new EcBlock(18, 123)), new EcBlocks(28, new EcBlock(13, 46), new EcBlock(32, 47)), new EcBlocks(30, new EcBlock(48, 24), new EcBlock(14, 25)), new EcBlocks(30, new EcBlock(42, 15), new EcBlock(32, 16)) ), new self( 39, [6, 26, 54, 82, 110, 138, 166], new EcBlocks(30, new EcBlock(20, 117), new EcBlock(4, 118)), new EcBlocks(28, new EcBlock(40, 47), new EcBlock(7, 48)), new EcBlocks(30, new EcBlock(43, 24), new EcBlock(22, 25)), new EcBlocks(30, new EcBlock(10, 15), new EcBlock(67, 16)) ), new self( 40, [6, 30, 58, 86, 114, 142, 170], new EcBlocks(30, new EcBlock(19, 118), new EcBlock(6, 119)), new EcBlocks(28, new EcBlock(18, 47), new EcBlock(31, 48)), new EcBlocks(30, new EcBlock(34, 24), new EcBlock(34, 25)), new EcBlocks(30, new EcBlock(20, 15), new EcBlock(61, 16)) ), ]; } } src/Common/BitArray.php000064400000021323150250215260011010 0ustar00 */ private $bits; /** * Size of the bit array in bits. * * @var int */ private $size; /** * Creates a new bit array with a given size. */ public function __construct(int $size = 0) { $this->size = $size; $this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0)); } /** * Gets the size in bits. */ public function getSize() : int { return $this->size; } /** * Gets the size in bytes. */ public function getSizeInBytes() : int { return ($this->size + 7) >> 3; } /** * Ensures that the array has a minimum capacity. */ public function ensureCapacity(int $size) : void { if ($size > count($this->bits) << 5) { $this->bits->setSize(($size + 31) >> 5); } } /** * Gets a specific bit. */ public function get(int $i) : bool { return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f))); } /** * Sets a specific bit. */ public function set(int $i) : void { $this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f); } /** * Flips a specific bit. */ public function flip(int $i) : void { $this->bits[$i >> 5] ^= 1 << ($i & 0x1f); } /** * Gets the next set bit position from a given position. */ public function getNextSet(int $from) : int { if ($from >= $this->size) { return $this->size; } $bitsOffset = $from >> 5; $currentBits = $this->bits[$bitsOffset]; $bitsLength = count($this->bits); $currentBits &= ~((1 << ($from & 0x1f)) - 1); while (0 === $currentBits) { if (++$bitsOffset === $bitsLength) { return $this->size; } $currentBits = $this->bits[$bitsOffset]; } $result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits); return $result > $this->size ? $this->size : $result; } /** * Gets the next unset bit position from a given position. */ public function getNextUnset(int $from) : int { if ($from >= $this->size) { return $this->size; } $bitsOffset = $from >> 5; $currentBits = ~$this->bits[$bitsOffset]; $bitsLength = count($this->bits); $currentBits &= ~((1 << ($from & 0x1f)) - 1); while (0 === $currentBits) { if (++$bitsOffset === $bitsLength) { return $this->size; } $currentBits = ~$this->bits[$bitsOffset]; } $result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits); return $result > $this->size ? $this->size : $result; } /** * Sets a bulk of bits. */ public function setBulk(int $i, int $newBits) : void { $this->bits[$i >> 5] = $newBits; } /** * Sets a range of bits. * * @throws InvalidArgumentException if end is smaller than start */ public function setRange(int $start, int $end) : void { if ($end < $start) { throw new InvalidArgumentException('End must be greater or equal to start'); } if ($end === $start) { return; } --$end; $firstInt = $start >> 5; $lastInt = $end >> 5; for ($i = $firstInt; $i <= $lastInt; ++$i) { $firstBit = $i > $firstInt ? 0 : $start & 0x1f; $lastBit = $i < $lastInt ? 31 : $end & 0x1f; if (0 === $firstBit && 31 === $lastBit) { $mask = 0x7fffffff; } else { $mask = 0; for ($j = $firstBit; $j < $lastBit; ++$j) { $mask |= 1 << $j; } } $this->bits[$i] = $this->bits[$i] | $mask; } } /** * Clears the bit array, unsetting every bit. */ public function clear() : void { $bitsLength = count($this->bits); for ($i = 0; $i < $bitsLength; ++$i) { $this->bits[$i] = 0; } } /** * Checks if a range of bits is set or not set. * @throws InvalidArgumentException if end is smaller than start */ public function isRange(int $start, int $end, bool $value) : bool { if ($end < $start) { throw new InvalidArgumentException('End must be greater or equal to start'); } if ($end === $start) { return true; } --$end; $firstInt = $start >> 5; $lastInt = $end >> 5; for ($i = $firstInt; $i <= $lastInt; ++$i) { $firstBit = $i > $firstInt ? 0 : $start & 0x1f; $lastBit = $i < $lastInt ? 31 : $end & 0x1f; if (0 === $firstBit && 31 === $lastBit) { $mask = 0x7fffffff; } else { $mask = 0; for ($j = $firstBit; $j <= $lastBit; ++$j) { $mask |= 1 << $j; } } if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) { return false; } } return true; } /** * Appends a bit to the array. */ public function appendBit(bool $bit) : void { $this->ensureCapacity($this->size + 1); if ($bit) { $this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f)); } ++$this->size; } /** * Appends a number of bits (up to 32) to the array. * @throws InvalidArgumentException if num bits is not between 0 and 32 */ public function appendBits(int $value, int $numBits) : void { if ($numBits < 0 || $numBits > 32) { throw new InvalidArgumentException('Num bits must be between 0 and 32'); } $this->ensureCapacity($this->size + $numBits); for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) { $this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1); } } /** * Appends another bit array to this array. */ public function appendBitArray(self $other) : void { $otherSize = $other->getSize(); $this->ensureCapacity($this->size + $other->getSize()); for ($i = 0; $i < $otherSize; ++$i) { $this->appendBit($other->get($i)); } } /** * Makes an exclusive-or comparision on the current bit array. * * @throws InvalidArgumentException if sizes don't match */ public function xorBits(self $other) : void { $bitsLength = count($this->bits); $otherBits = $other->getBitArray(); if ($bitsLength !== count($otherBits)) { throw new InvalidArgumentException('Sizes don\'t match'); } for ($i = 0; $i < $bitsLength; ++$i) { $this->bits[$i] = $this->bits[$i] ^ $otherBits[$i]; } } /** * Converts the bit array to a byte array. * * @return SplFixedArray */ public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray { $bytes = new SplFixedArray($numBytes); for ($i = 0; $i < $numBytes; ++$i) { $byte = 0; for ($j = 0; $j < 8; ++$j) { if ($this->get($bitOffset)) { $byte |= 1 << (7 - $j); } ++$bitOffset; } $bytes[$i] = $byte; } return $bytes; } /** * Gets the internal bit array. * * @return SplFixedArray */ public function getBitArray() : SplFixedArray { return $this->bits; } /** * Reverses the array. */ public function reverse() : void { $newBits = new SplFixedArray(count($this->bits)); for ($i = 0; $i < $this->size; ++$i) { if ($this->get($this->size - $i - 1)) { $newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f)); } } $this->bits = $newBits; } /** * Returns a string representation of the bit array. */ public function __toString() : string { $result = ''; for ($i = 0; $i < $this->size; ++$i) { if (0 === ($i & 0x07)) { $result .= ' '; } $result .= $this->get($i) ? 'X' : '.'; } return $result; } } src/Common/BitMatrix.php000064400000017070150250215260011202 0ustar00 */ private $bits; /** * @throws InvalidArgumentException if a dimension is smaller than zero */ public function __construct(int $width, int $height = null) { if (null === $height) { $height = $width; } if ($width < 1 || $height < 1) { throw new InvalidArgumentException('Both dimensions must be greater than zero'); } $this->width = $width; $this->height = $height; $this->rowSize = ($width + 31) >> 5; $this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0)); } /** * Gets the requested bit, where true means black. */ public function get(int $x, int $y) : bool { $offset = $y * $this->rowSize + ($x >> 5); return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1); } /** * Sets the given bit to true. */ public function set(int $x, int $y) : void { $offset = $y * $this->rowSize + ($x >> 5); $this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f)); } /** * Flips the given bit. */ public function flip(int $x, int $y) : void { $offset = $y * $this->rowSize + ($x >> 5); $this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f)); } /** * Clears all bits (set to false). */ public function clear() : void { $max = count($this->bits); for ($i = 0; $i < $max; ++$i) { $this->bits[$i] = 0; } } /** * Sets a square region of the bit matrix to true. * * @throws InvalidArgumentException if left or top are negative * @throws InvalidArgumentException if width or height are smaller than 1 * @throws InvalidArgumentException if region does not fit into the matix */ public function setRegion(int $left, int $top, int $width, int $height) : void { if ($top < 0 || $left < 0) { throw new InvalidArgumentException('Left and top must be non-negative'); } if ($height < 1 || $width < 1) { throw new InvalidArgumentException('Width and height must be at least 1'); } $right = $left + $width; $bottom = $top + $height; if ($bottom > $this->height || $right > $this->width) { throw new InvalidArgumentException('The region must fit inside the matrix'); } for ($y = $top; $y < $bottom; ++$y) { $offset = $y * $this->rowSize; for ($x = $left; $x < $right; ++$x) { $index = $offset + ($x >> 5); $this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f)); } } } /** * A fast method to retrieve one row of data from the matrix as a BitArray. */ public function getRow(int $y, BitArray $row = null) : BitArray { if (null === $row || $row->getSize() < $this->width) { $row = new BitArray($this->width); } $offset = $y * $this->rowSize; for ($x = 0; $x < $this->rowSize; ++$x) { $row->setBulk($x << 5, $this->bits[$offset + $x]); } return $row; } /** * Sets a row of data from a BitArray. */ public function setRow(int $y, BitArray $row) : void { $bits = $row->getBitArray(); for ($i = 0; $i < $this->rowSize; ++$i) { $this->bits[$y * $this->rowSize + $i] = $bits[$i]; } } /** * This is useful in detecting the enclosing rectangle of a 'pure' barcode. * * @return int[]|null */ public function getEnclosingRectangle() : ?array { $left = $this->width; $top = $this->height; $right = -1; $bottom = -1; for ($y = 0; $y < $this->height; ++$y) { for ($x32 = 0; $x32 < $this->rowSize; ++$x32) { $bits = $this->bits[$y * $this->rowSize + $x32]; if (0 !== $bits) { if ($y < $top) { $top = $y; } if ($y > $bottom) { $bottom = $y; } if ($x32 * 32 < $left) { $bit = 0; while (($bits << (31 - $bit)) === 0) { $bit++; } if (($x32 * 32 + $bit) < $left) { $left = $x32 * 32 + $bit; } } } if ($x32 * 32 + 31 > $right) { $bit = 31; while (0 === BitUtils::unsignedRightShift($bits, $bit)) { --$bit; } if (($x32 * 32 + $bit) > $right) { $right = $x32 * 32 + $bit; } } } } $width = $right - $left; $height = $bottom - $top; if ($width < 0 || $height < 0) { return null; } return [$left, $top, $width, $height]; } /** * Gets the most top left set bit. * * This is useful in detecting a corner of a 'pure' barcode. * * @return int[]|null */ public function getTopLeftOnBit() : ?array { $bitsOffset = 0; while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) { ++$bitsOffset; } if (count($this->bits) === $bitsOffset) { return null; } $x = intdiv($bitsOffset, $this->rowSize); $y = ($bitsOffset % $this->rowSize) << 5; $bits = $this->bits[$bitsOffset]; $bit = 0; while (0 === ($bits << (31 - $bit))) { ++$bit; } $x += $bit; return [$x, $y]; } /** * Gets the most bottom right set bit. * * This is useful in detecting a corner of a 'pure' barcode. * * @return int[]|null */ public function getBottomRightOnBit() : ?array { $bitsOffset = count($this->bits) - 1; while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) { --$bitsOffset; } if ($bitsOffset < 0) { return null; } $x = intdiv($bitsOffset, $this->rowSize); $y = ($bitsOffset % $this->rowSize) << 5; $bits = $this->bits[$bitsOffset]; $bit = 0; while (0 === BitUtils::unsignedRightShift($bits, $bit)) { --$bit; } $x += $bit; return [$x, $y]; } /** * Gets the width of the matrix, */ public function getWidth() : int { return $this->width; } /** * Gets the height of the matrix. */ public function getHeight() : int { return $this->height; } } src/Common/ErrorCorrectionLevel.php000064400000002475150250215260013413 0ustar00bits = $bits; } /** * @throws OutOfBoundsException if number of bits is invalid */ public static function forBits(int $bits) : self { switch ($bits) { case 0: return self::M(); case 1: return self::L(); case 2: return self::H(); case 3: return self::Q(); } throw new OutOfBoundsException('Invalid number of bits'); } /** * Returns the two bits used to encode this error correction level. */ public function getBits() : int { return $this->bits; } } src/Common/BitUtils.php000064400000001612150250215260011031 0ustar00>>" in other * languages. */ public static function unsignedRightShift(int $a, int $b) : int { return ( $a >= 0 ? $a >> $b : (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1)) ); } /** * Gets the number of trailing zeros. */ public static function numberOfTrailingZeros(int $i) : int { $lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1'); return $lastPos === false ? 32 : 31 - $lastPos; } } src/Renderer/Eye/EyeInterface.php000064400000001124150250215260012673 0ustar00module = $module; } public function getExternalPath() : Path { $matrix = new ByteMatrix(7, 7); for ($x = 0; $x < 7; ++$x) { $matrix->set($x, 0, 1); $matrix->set($x, 6, 1); } for ($y = 1; $y < 6; ++$y) { $matrix->set(0, $y, 1); $matrix->set(6, $y, 1); } return $this->module->createPath($matrix)->translate(-3.5, -3.5); } public function getInternalPath() : Path { $matrix = new ByteMatrix(3, 3); for ($x = 0; $x < 3; ++$x) { for ($y = 0; $y < 3; ++$y) { $matrix->set($x, $y, 1); } } return $this->module->createPath($matrix)->translate(-1.5, -1.5); } } src/Renderer/Eye/CompositeEye.php000064400000001366150250215260012745 0ustar00externalEye = $externalEye; $this->internalEye = $internalEye; } public function getExternalPath() : Path { return $this->externalEye->getExternalPath(); } public function getInternalPath() : Path { return $this->internalEye->getInternalPath(); } } src/Renderer/Eye/SquareEye.php000064400000002061150250215260012234 0ustar00move(-3.5, -3.5) ->line(3.5, -3.5) ->line(3.5, 3.5) ->line(-3.5, 3.5) ->close() ->move(-2.5, -2.5) ->line(-2.5, 2.5) ->line(2.5, 2.5) ->line(2.5, -2.5) ->close() ; } public function getInternalPath() : Path { return (new Path()) ->move(-1.5, -1.5) ->line(1.5, -1.5) ->line(1.5, 1.5) ->line(-1.5, 1.5) ->close() ; } } src/Renderer/Eye/SimpleCircleEye.php000064400000002307150250215260013352 0ustar00move(-3.5, -3.5) ->line(3.5, -3.5) ->line(3.5, 3.5) ->line(-3.5, 3.5) ->close() ->move(-2.5, -2.5) ->line(-2.5, 2.5) ->line(2.5, 2.5) ->line(2.5, -2.5) ->close() ; } public function getInternalPath() : Path { return (new Path()) ->move(1.5, 0) ->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5) ->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.) ->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5) ->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.) ->close() ; } } src/Renderer/Color/Alpha.php000064400000002076150250215260011720 0ustar00 100) { throw new Exception\InvalidArgumentException('Alpha must be between 0 and 100'); } $this->alpha = $alpha; $this->baseColor = $baseColor; } public function getAlpha() : int { return $this->alpha; } public function getBaseColor() : ColorInterface { return $this->baseColor; } public function toRgb() : Rgb { return $this->baseColor->toRgb(); } public function toCmyk() : Cmyk { return $this->baseColor->toCmyk(); } public function toGray() : Gray { return $this->baseColor->toGray(); } } src/Renderer/Color/Gray.php000064400000001637150250215260011577 0ustar00 100) { throw new Exception\InvalidArgumentException('Gray must be between 0 and 100'); } $this->gray = (int) $gray; } public function getGray() : int { return $this->gray; } public function toRgb() : Rgb { return new Rgb((int) ($this->gray * 2.55), (int) ($this->gray * 2.55), (int) ($this->gray * 2.55)); } public function toCmyk() : Cmyk { return new Cmyk(0, 0, 0, 100 - $this->gray); } public function toGray() : Gray { return $this; } } src/Renderer/Color/ColorInterface.php000064400000000555150250215260013572 0ustar00 255) { throw new Exception\InvalidArgumentException('Red must be between 0 and 255'); } if ($green < 0 || $green > 255) { throw new Exception\InvalidArgumentException('Green must be between 0 and 255'); } if ($blue < 0 || $blue > 255) { throw new Exception\InvalidArgumentException('Blue must be between 0 and 255'); } $this->red = $red; $this->green = $green; $this->blue = $blue; } public function getRed() : int { return $this->red; } public function getGreen() : int { return $this->green; } public function getBlue() : int { return $this->blue; } public function toRgb() : Rgb { return $this; } public function toCmyk() : Cmyk { $c = 1 - ($this->red / 255); $m = 1 - ($this->green / 255); $y = 1 - ($this->blue / 255); $k = min($c, $m, $y); return new Cmyk( (int) (100 * ($c - $k) / (1 - $k)), (int) (100 * ($m - $k) / (1 - $k)), (int) (100 * ($y - $k) / (1 - $k)), (int) (100 * $k) ); } public function toGray() : Gray { return new Gray((int) (($this->red * 0.21 + $this->green * 0.71 + $this->blue * 0.07) / 2.55)); } } src/Renderer/Color/Cmyk.php000064400000004412150250215260011572 0ustar00 100) { throw new Exception\InvalidArgumentException('Cyan must be between 0 and 100'); } if ($magenta < 0 || $magenta > 100) { throw new Exception\InvalidArgumentException('Magenta must be between 0 and 100'); } if ($yellow < 0 || $yellow > 100) { throw new Exception\InvalidArgumentException('Yellow must be between 0 and 100'); } if ($black < 0 || $black > 100) { throw new Exception\InvalidArgumentException('Black must be between 0 and 100'); } $this->cyan = $cyan; $this->magenta = $magenta; $this->yellow = $yellow; $this->black = $black; } public function getCyan() : int { return $this->cyan; } public function getMagenta() : int { return $this->magenta; } public function getYellow() : int { return $this->yellow; } public function getBlack() : int { return $this->black; } public function toRgb() : Rgb { $k = $this->black / 100; $c = (-$k * $this->cyan + $k * 100 + $this->cyan) / 100; $m = (-$k * $this->magenta + $k * 100 + $this->magenta) / 100; $y = (-$k * $this->yellow + $k * 100 + $this->yellow) / 100; return new Rgb( (int) (-$c * 255 + 255), (int) (-$m * 255 + 255), (int) (-$y * 255 + 255) ); } public function toCmyk() : Cmyk { return $this; } public function toGray() : Gray { return $this->toRgb()->toGray(); } } src/Renderer/PlainTextRenderer.php000064400000004216150250215260013212 0ustar00margin = $margin; } /** * @throws InvalidArgumentException if matrix width doesn't match height */ public function render(QrCode $qrCode) : string { $matrix = $qrCode->getMatrix(); $matrixSize = $matrix->getWidth(); if ($matrixSize !== $matrix->getHeight()) { throw new InvalidArgumentException('Matrix must have the same width and height'); } $rows = $matrix->getArray()->toArray(); if (0 !== $matrixSize % 2) { $rows[] = array_fill(0, $matrixSize, 0); } $horizontalMargin = str_repeat(self::EMPTY_BLOCK, $this->margin); $result = str_repeat("\n", (int) ceil($this->margin / 2)); for ($i = 0; $i < $matrixSize; $i += 2) { $result .= $horizontalMargin; $upperRow = $rows[$i]; $lowerRow = $rows[$i + 1]; for ($j = 0; $j < $matrixSize; ++$j) { $upperBit = $upperRow[$j]; $lowerBit = $lowerRow[$j]; if ($upperBit) { $result .= $lowerBit ? self::FULL_BLOCK : self::UPPER_HALF_BLOCK; } else { $result .= $lowerBit ? self::LOWER_HALF_BLOCK : self::EMPTY_BLOCK; } } $result .= $horizontalMargin . "\n"; } $result .= str_repeat("\n", (int) ceil($this->margin / 2)); return $result; } } src/Renderer/Path/Line.php000064400000001220150250215260011366 0ustar00x = $x; $this->y = $y; } public function getX() : float { return $this->x; } public function getY() : float { return $this->y; } /** * @return self */ public function translate(float $x, float $y) : OperationInterface { return new self($this->x + $x, $this->y + $y); } } src/Renderer/Path/Move.php000064400000001220150250215260011405 0ustar00x = $x; $this->y = $y; } public function getX() : float { return $this->x; } public function getY() : float { return $this->y; } /** * @return self */ public function translate(float $x, float $y) : OperationInterface { return new self($this->x + $x, $this->y + $y); } } src/Renderer/Path/Curve.php000064400000002675150250215260011602 0ustar00x1 = $x1; $this->y1 = $y1; $this->x2 = $x2; $this->y2 = $y2; $this->x3 = $x3; $this->y3 = $y3; } public function getX1() : float { return $this->x1; } public function getY1() : float { return $this->y1; } public function getX2() : float { return $this->x2; } public function getY2() : float { return $this->y2; } public function getX3() : float { return $this->x3; } public function getY3() : float { return $this->y3; } /** * @return self */ public function translate(float $x, float $y) : OperationInterface { return new self( $this->x1 + $x, $this->y1 + $y, $this->x2 + $x, $this->y2 + $y, $this->x3 + $x, $this->y3 + $y ); } } src/Renderer/Path/Close.php000064400000000770150250215260011555 0ustar00xRadius = abs($xRadius); $this->yRadius = abs($yRadius); $this->xAxisAngle = $xAxisAngle % 360; $this->largeArc = $largeArc; $this->sweep = $sweep; $this->x = $x; $this->y = $y; } public function getXRadius() : float { return $this->xRadius; } public function getYRadius() : float { return $this->yRadius; } public function getXAxisAngle() : float { return $this->xAxisAngle; } public function isLargeArc() : bool { return $this->largeArc; } public function isSweep() : bool { return $this->sweep; } public function getX() : float { return $this->x; } public function getY() : float { return $this->y; } /** * @return self */ public function translate(float $x, float $y) : OperationInterface { return new self( $this->xRadius, $this->yRadius, $this->xAxisAngle, $this->largeArc, $this->sweep, $this->x + $x, $this->y + $y ); } /** * Converts the elliptic arc to multiple curves. * * Since not all image back ends support elliptic arcs, this method allows to convert the arc into multiple curves * resembling the same result. * * @see https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/ * @return array */ public function toCurves(float $fromX, float $fromY) : array { if (sqrt(($fromX - $this->x) ** 2 + ($fromY - $this->y) ** 2) < self::ZERO_TOLERANCE) { return []; } if ($this->xRadius < self::ZERO_TOLERANCE || $this->yRadius < self::ZERO_TOLERANCE) { return [new Line($this->x, $this->y)]; } return $this->createCurves($fromX, $fromY); } /** * @return Curve[] */ private function createCurves(float $fromX, $fromY) : array { $xAngle = deg2rad($this->xAxisAngle); list($centerX, $centerY, $radiusX, $radiusY, $startAngle, $deltaAngle) = $this->calculateCenterPointParameters($fromX, $fromY, $xAngle); $s = $startAngle; $e = $s + $deltaAngle; $sign = ($e < $s) ? -1 : 1; $remain = abs($e - $s); $p1 = self::point($centerX, $centerY, $radiusX, $radiusY, $xAngle, $s); $curves = []; while ($remain > self::ZERO_TOLERANCE) { $step = min($remain, pi() / 2); $signStep = $step * $sign; $p2 = self::point($centerX, $centerY, $radiusX, $radiusY, $xAngle, $s + $signStep); $alphaT = tan($signStep / 2); $alpha = sin($signStep) * (sqrt(4 + 3 * $alphaT ** 2) - 1) / 3; $d1 = self::derivative($radiusX, $radiusY, $xAngle, $s); $d2 = self::derivative($radiusX, $radiusY, $xAngle, $s + $signStep); $curves[] = new Curve( $p1[0] + $alpha * $d1[0], $p1[1] + $alpha * $d1[1], $p2[0] - $alpha * $d2[0], $p2[1] - $alpha * $d2[1], $p2[0], $p2[1] ); $s += $signStep; $remain -= $step; $p1 = $p2; } return $curves; } /** * @return float[] */ private function calculateCenterPointParameters(float $fromX, float $fromY, float $xAngle) { $rX = $this->xRadius; $rY = $this->yRadius; // F.6.5.1 $dx2 = ($fromX - $this->x) / 2; $dy2 = ($fromY - $this->y) / 2; $x1p = cos($xAngle) * $dx2 + sin($xAngle) * $dy2; $y1p = -sin($xAngle) * $dx2 + cos($xAngle) * $dy2; // F.6.5.2 $rxs = $rX ** 2; $rys = $rY ** 2; $x1ps = $x1p ** 2; $y1ps = $y1p ** 2; $cr = $x1ps / $rxs + $y1ps / $rys; if ($cr > 1) { $s = sqrt($cr); $rX *= $s; $rY *= $s; $rxs = $rX ** 2; $rys = $rY ** 2; } $dq = ($rxs * $y1ps + $rys * $x1ps); $pq = ($rxs * $rys - $dq) / $dq; $q = sqrt(max(0, $pq)); if ($this->largeArc === $this->sweep) { $q = -$q; } $cxp = $q * $rX * $y1p / $rY; $cyp = -$q * $rY * $x1p / $rX; // F.6.5.3 $cx = cos($xAngle) * $cxp - sin($xAngle) * $cyp + ($fromX + $this->x) / 2; $cy = sin($xAngle) * $cxp + cos($xAngle) * $cyp + ($fromY + $this->y) / 2; // F.6.5.5 $theta = self::angle(1, 0, ($x1p - $cxp) / $rX, ($y1p - $cyp) / $rY); // F.6.5.6 $delta = self::angle(($x1p - $cxp) / $rX, ($y1p - $cyp) / $rY, (-$x1p - $cxp) / $rX, (-$y1p - $cyp) / $rY); $delta = fmod($delta, pi() * 2); if (! $this->sweep) { $delta -= 2 * pi(); } return [$cx, $cy, $rX, $rY, $theta, $delta]; } private static function angle(float $ux, float $uy, float $vx, float $vy) : float { // F.6.5.4 $dot = $ux * $vx + $uy * $vy; $length = sqrt($ux ** 2 + $uy ** 2) * sqrt($vx ** 2 + $vy ** 2); $angle = acos(min(1, max(-1, $dot / $length))); if (($ux * $vy - $uy * $vx) < 0) { return -$angle; } return $angle; } /** * @return float[] */ private static function point( float $centerX, float $centerY, float $radiusX, float $radiusY, float $xAngle, float $angle ) : array { return [ $centerX + $radiusX * cos($xAngle) * cos($angle) - $radiusY * sin($xAngle) * sin($angle), $centerY + $radiusX * sin($xAngle) * cos($angle) + $radiusY * cos($xAngle) * sin($angle), ]; } /** * @return float[] */ private static function derivative(float $radiusX, float $radiusY, float $xAngle, float $angle) : array { return [ -$radiusX * cos($xAngle) * sin($angle) - $radiusY * sin($xAngle) * cos($angle), -$radiusX * sin($xAngle) * sin($angle) + $radiusY * cos($xAngle) * cos($angle), ]; } } src/Renderer/Path/OperationInterface.php000064400000000342150250215260014264 0ustar00operations[] = new Move($x, $y); return $path; } /** * Draws a line from the current position to another position. */ public function line(float $x, float $y) : self { $path = clone $this; $path->operations[] = new Line($x, $y); return $path; } /** * Draws an elliptic arc from the current position to another position. */ public function ellipticArc( float $xRadius, float $yRadius, float $xAxisRotation, bool $largeArc, bool $sweep, float $x, float $y ) : self { $path = clone $this; $path->operations[] = new EllipticArc($xRadius, $yRadius, $xAxisRotation, $largeArc, $sweep, $x, $y); return $path; } /** * Draws a curve from the current position to another position. */ public function curve(float $x1, float $y1, float $x2, float $y2, float $x3, float $y3) : self { $path = clone $this; $path->operations[] = new Curve($x1, $y1, $x2, $y2, $x3, $y3); return $path; } /** * Closes a sub-path. */ public function close() : self { $path = clone $this; $path->operations[] = Close::instance(); return $path; } /** * Appends another path to this one. */ public function append(self $other) : self { $path = clone $this; $path->operations = array_merge($this->operations, $other->operations); return $path; } public function translate(float $x, float $y) : self { $path = new self(); foreach ($this->operations as $operation) { $path->operations[] = $operation->translate($x, $y); } return $path; } /** * @return OperationInterface[]|Traversable */ public function getIterator() : Traversable { foreach ($this->operations as $operation) { yield $operation; } } } src/Renderer/Image/ImageBackEndInterface.php000064400000004724150250215260014714 0ustar00values = [1, 0, 0, 1, 0, 0]; } public function multiply(self $other) : self { $matrix = new self(); $matrix->values[0] = $this->values[0] * $other->values[0] + $this->values[2] * $other->values[1]; $matrix->values[1] = $this->values[1] * $other->values[0] + $this->values[3] * $other->values[1]; $matrix->values[2] = $this->values[0] * $other->values[2] + $this->values[2] * $other->values[3]; $matrix->values[3] = $this->values[1] * $other->values[2] + $this->values[3] * $other->values[3]; $matrix->values[4] = $this->values[0] * $other->values[4] + $this->values[2] * $other->values[5] + $this->values[4]; $matrix->values[5] = $this->values[1] * $other->values[4] + $this->values[3] * $other->values[5] + $this->values[5]; return $matrix; } public static function scale(float $size) : self { $matrix = new self(); $matrix->values = [$size, 0, 0, $size, 0, 0]; return $matrix; } public static function translate(float $x, float $y) : self { $matrix = new self(); $matrix->values = [1, 0, 0, 1, $x, $y]; return $matrix; } public static function rotate(int $degrees) : self { $matrix = new self(); $rad = deg2rad($degrees); $matrix->values = [cos($rad), sin($rad), -sin($rad), cos($rad), 0, 0]; return $matrix; } /** * Applies this matrix onto a point and returns the resulting viewport point. * * @return float[] */ public function apply(float $x, float $y) : array { return [ $x * $this->values[0] + $y * $this->values[2] + $this->values[4], $x * $this->values[1] + $y * $this->values[3] + $this->values[5], ]; } } src/Renderer/Image/EpsImageBackEnd.php000064400000027273150250215260013547 0ustar00eps = "%!PS-Adobe-3.0 EPSF-3.0\n" . "%%Creator: BaconQrCode\n" . sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size) . "%%BeginProlog\n" . "save\n" . "50 dict begin\n" . "/q { gsave } bind def\n" . "/Q { grestore } bind def\n" . "/s { scale } bind def\n" . "/t { translate } bind def\n" . "/r { rotate } bind def\n" . "/n { newpath } bind def\n" . "/m { moveto } bind def\n" . "/l { lineto } bind def\n" . "/c { curveto } bind def\n" . "/z { closepath } bind def\n" . "/f { eofill } bind def\n" . "/rgb { setrgbcolor } bind def\n" . "/cmyk { setcmykcolor } bind def\n" . "/gray { setgray } bind def\n" . "%%EndProlog\n" . "1 -1 s\n" . sprintf("0 -%d t\n", $size); if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) { return; } $this->eps .= wordwrap( '0 0 m' . sprintf(' %s 0 l', (string) $size) . sprintf(' %s %s l', (string) $size, (string) $size) . sprintf(' 0 %s l', (string) $size) . ' z' . ' ' .$this->getColorSetString($backgroundColor) . " f\n", 75, "\n " ); } public function scale(float $size) : void { if (null === $this->eps) { throw new RuntimeException('No image has been started'); } $this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION)); } public function translate(float $x, float $y) : void { if (null === $this->eps) { throw new RuntimeException('No image has been started'); } $this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION)); } public function rotate(int $degrees) : void { if (null === $this->eps) { throw new RuntimeException('No image has been started'); } $this->eps .= sprintf("%d r\n", $degrees); } public function push() : void { if (null === $this->eps) { throw new RuntimeException('No image has been started'); } $this->eps .= "q\n"; } public function pop() : void { if (null === $this->eps) { throw new RuntimeException('No image has been started'); } $this->eps .= "Q\n"; } public function drawPathWithColor(Path $path, ColorInterface $color) : void { if (null === $this->eps) { throw new RuntimeException('No image has been started'); } $fromX = 0; $fromY = 0; $this->eps .= wordwrap( 'n ' . $this->drawPathOperations($path, $fromX, $fromY) . ' ' . $this->getColorSetString($color) . " f\n", 75, "\n " ); } public function drawPathWithGradient( Path $path, Gradient $gradient, float $x, float $y, float $width, float $height ) : void { if (null === $this->eps) { throw new RuntimeException('No image has been started'); } $fromX = 0; $fromY = 0; $this->eps .= wordwrap( 'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n", 75, "\n " ); $this->createGradientFill($gradient, $x, $y, $width, $height); } public function done() : string { if (null === $this->eps) { throw new RuntimeException('No image has been started'); } $this->eps .= "%%TRAILER\nend restore\n%%EOF"; $blob = $this->eps; $this->eps = null; return $blob; } private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string { $pathData = []; foreach ($ops as $op) { switch (true) { case $op instanceof Move: $fromX = $toX = round($op->getX(), self::PRECISION); $fromY = $toY = round($op->getY(), self::PRECISION); $pathData[] = sprintf('%s %s m', $toX, $toY); break; case $op instanceof Line: $fromX = $toX = round($op->getX(), self::PRECISION); $fromY = $toY = round($op->getY(), self::PRECISION); $pathData[] = sprintf('%s %s l', $toX, $toY); break; case $op instanceof EllipticArc: $pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY); break; case $op instanceof Curve: $x1 = round($op->getX1(), self::PRECISION); $y1 = round($op->getY1(), self::PRECISION); $x2 = round($op->getX2(), self::PRECISION); $y2 = round($op->getY2(), self::PRECISION); $fromX = $x3 = round($op->getX3(), self::PRECISION); $fromY = $y3 = round($op->getY3(), self::PRECISION); $pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3); break; case $op instanceof Close: $pathData[] = 'z'; break; default: throw new RuntimeException('Unexpected draw operation: ' . get_class($op)); } } return implode(' ', $pathData); } private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void { $startColor = $gradient->getStartColor(); $endColor = $gradient->getEndColor(); if ($startColor instanceof Alpha) { $startColor = $startColor->getBaseColor(); } $startColorType = get_class($startColor); if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) { $startColorType = Cmyk::class; $startColor = $startColor->toCmyk(); } if (get_class($endColor) !== $startColorType) { switch ($startColorType) { case Cmyk::class: $endColor = $endColor->toCmyk(); break; case Rgb::class: $endColor = $endColor->toRgb(); break; case Gray::class: $endColor = $endColor->toGray(); break; } } $this->eps .= "eoclip\n<<\n"; if ($gradient->getType() === GradientType::RADIAL()) { $this->eps .= " /ShadingType 3\n"; } else { $this->eps .= " /ShadingType 2\n"; } $this->eps .= " /Extend [ true true ]\n" . " /AntiAlias true\n"; switch ($startColorType) { case Cmyk::class: $this->eps .= " /ColorSpace /DeviceCMYK\n"; break; case Rgb::class: $this->eps .= " /ColorSpace /DeviceRGB\n"; break; case Gray::class: $this->eps .= " /ColorSpace /DeviceGray\n"; break; } switch ($gradient->getType()) { case GradientType::HORIZONTAL(): $this->eps .= sprintf( " /Coords [ %s %s %s %s ]\n", round($x, self::PRECISION), round($y, self::PRECISION), round($x + $width, self::PRECISION), round($y, self::PRECISION) ); break; case GradientType::VERTICAL(): $this->eps .= sprintf( " /Coords [ %s %s %s %s ]\n", round($x, self::PRECISION), round($y, self::PRECISION), round($x, self::PRECISION), round($y + $height, self::PRECISION) ); break; case GradientType::DIAGONAL(): $this->eps .= sprintf( " /Coords [ %s %s %s %s ]\n", round($x, self::PRECISION), round($y, self::PRECISION), round($x + $width, self::PRECISION), round($y + $height, self::PRECISION) ); break; case GradientType::INVERSE_DIAGONAL(): $this->eps .= sprintf( " /Coords [ %s %s %s %s ]\n", round($x, self::PRECISION), round($y + $height, self::PRECISION), round($x + $width, self::PRECISION), round($y, self::PRECISION) ); break; case GradientType::RADIAL(): $centerX = ($x + $width) / 2; $centerY = ($y + $height) / 2; $this->eps .= sprintf( " /Coords [ %s %s 0 %s %s %s ]\n", round($centerX, self::PRECISION), round($centerY, self::PRECISION), round($centerX, self::PRECISION), round($centerY, self::PRECISION), round(max($width, $height) / 2, self::PRECISION) ); break; } $this->eps .= " /Function\n" . " <<\n" . " /FunctionType 2\n" . " /Domain [ 0 1 ]\n" . sprintf(" /C0 [ %s ]\n", $this->getColorString($startColor)) . sprintf(" /C1 [ %s ]\n", $this->getColorString($endColor)) . " /N 1\n" . " >>\n>>\nshfill\nQ\n"; } private function getColorSetString(ColorInterface $color) : string { if ($color instanceof Rgb) { return $this->getColorString($color) . ' rgb'; } if ($color instanceof Cmyk) { return $this->getColorString($color) . ' cmyk'; } if ($color instanceof Gray) { return $this->getColorString($color) . ' gray'; } return $this->getColorSetString($color->toCmyk()); } private function getColorString(ColorInterface $color) : string { if ($color instanceof Rgb) { return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255); } if ($color instanceof Cmyk) { return sprintf( '%s %s %s %s', $color->getCyan() / 100, $color->getMagenta() / 100, $color->getYellow() / 100, $color->getBlack() / 100 ); } if ($color instanceof Gray) { return sprintf('%s', $color->getGray() / 100); } return $this->getColorString($color->toCmyk()); } } src/Renderer/Image/ImagickImageBackEnd.php000064400000024163150250215260014357 0ustar00imageFormat = $imageFormat; $this->compressionQuality = $compressionQuality; } public function new(int $size, ColorInterface $backgroundColor) : void { $this->image = new Imagick(); $this->image->newImage($size, $size, $this->getColorPixel($backgroundColor)); $this->image->setImageFormat($this->imageFormat); $this->image->setCompressionQuality($this->compressionQuality); $this->draw = new ImagickDraw(); $this->gradientCount = 0; $this->matrices = [new TransformationMatrix()]; $this->matrixIndex = 0; } public function scale(float $size) : void { if (null === $this->draw) { throw new RuntimeException('No image has been started'); } $this->draw->scale($size, $size); $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex] ->multiply(TransformationMatrix::scale($size)); } public function translate(float $x, float $y) : void { if (null === $this->draw) { throw new RuntimeException('No image has been started'); } $this->draw->translate($x, $y); $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex] ->multiply(TransformationMatrix::translate($x, $y)); } public function rotate(int $degrees) : void { if (null === $this->draw) { throw new RuntimeException('No image has been started'); } $this->draw->rotate($degrees); $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex] ->multiply(TransformationMatrix::rotate($degrees)); } public function push() : void { if (null === $this->draw) { throw new RuntimeException('No image has been started'); } $this->draw->push(); $this->matrices[++$this->matrixIndex] = $this->matrices[$this->matrixIndex - 1]; } public function pop() : void { if (null === $this->draw) { throw new RuntimeException('No image has been started'); } $this->draw->pop(); unset($this->matrices[$this->matrixIndex--]); } public function drawPathWithColor(Path $path, ColorInterface $color) : void { if (null === $this->draw) { throw new RuntimeException('No image has been started'); } $this->draw->setFillColor($this->getColorPixel($color)); $this->drawPath($path); } public function drawPathWithGradient( Path $path, Gradient $gradient, float $x, float $y, float $width, float $height ) : void { if (null === $this->draw) { throw new RuntimeException('No image has been started'); } $this->draw->setFillPatternURL('#' . $this->createGradientFill($gradient, $x, $y, $width, $height)); $this->drawPath($path); } public function done() : string { if (null === $this->draw) { throw new RuntimeException('No image has been started'); } $this->image->drawImage($this->draw); $blob = $this->image->getImageBlob(); $this->draw->clear(); $this->image->clear(); $this->draw = null; $this->image = null; $this->gradientCount = null; return $blob; } private function drawPath(Path $path) : void { $this->draw->pathStart(); foreach ($path as $op) { switch (true) { case $op instanceof Move: $this->draw->pathMoveToAbsolute($op->getX(), $op->getY()); break; case $op instanceof Line: $this->draw->pathLineToAbsolute($op->getX(), $op->getY()); break; case $op instanceof EllipticArc: $this->draw->pathEllipticArcAbsolute( $op->getXRadius(), $op->getYRadius(), $op->getXAxisAngle(), $op->isLargeArc(), $op->isSweep(), $op->getX(), $op->getY() ); break; case $op instanceof Curve: $this->draw->pathCurveToAbsolute( $op->getX1(), $op->getY1(), $op->getX2(), $op->getY2(), $op->getX3(), $op->getY3() ); break; case $op instanceof Close: $this->draw->pathClose(); break; default: throw new RuntimeException('Unexpected draw operation: ' . get_class($op)); } } $this->draw->pathFinish(); } private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string { list($width, $height) = $this->matrices[$this->matrixIndex]->apply($width, $height); $startColor = $this->getColorPixel($gradient->getStartColor())->getColorAsString(); $endColor = $this->getColorPixel($gradient->getEndColor())->getColorAsString(); $gradientImage = new Imagick(); switch ($gradient->getType()) { case GradientType::HORIZONTAL(): $gradientImage->newPseudoImage((int) $height, (int) $width, sprintf( 'gradient:%s-%s', $startColor, $endColor )); $gradientImage->rotateImage('transparent', -90); break; case GradientType::VERTICAL(): $gradientImage->newPseudoImage((int) $width, (int) $height, sprintf( 'gradient:%s-%s', $startColor, $endColor )); break; case GradientType::DIAGONAL(): case GradientType::INVERSE_DIAGONAL(): $gradientImage->newPseudoImage((int) ($width * sqrt(2)), (int) ($height * sqrt(2)), sprintf( 'gradient:%s-%s', $startColor, $endColor )); if (GradientType::DIAGONAL() === $gradient->getType()) { $gradientImage->rotateImage('transparent', -45); } else { $gradientImage->rotateImage('transparent', -135); } $rotatedWidth = $gradientImage->getImageWidth(); $rotatedHeight = $gradientImage->getImageHeight(); $gradientImage->setImagePage($rotatedWidth, $rotatedHeight, 0, 0); $gradientImage->cropImage( intdiv($rotatedWidth, 2) - 2, intdiv($rotatedHeight, 2) - 2, intdiv($rotatedWidth, 4) + 1, intdiv($rotatedWidth, 4) + 1 ); break; case GradientType::RADIAL(): $gradientImage->newPseudoImage((int) $width, (int) $height, sprintf( 'radial-gradient:%s-%s', $startColor, $endColor )); break; } $id = sprintf('g%d', ++$this->gradientCount); $this->draw->pushPattern($id, 0, 0, $width, $height); $this->draw->composite(Imagick::COMPOSITE_COPY, 0, 0, $width, $height, $gradientImage); $this->draw->popPattern(); return $id; } private function getColorPixel(ColorInterface $color) : ImagickPixel { $alpha = 100; if ($color instanceof Alpha) { $alpha = $color->getAlpha(); $color = $color->getBaseColor(); } if ($color instanceof Rgb) { return new ImagickPixel(sprintf( 'rgba(%d, %d, %d, %F)', $color->getRed(), $color->getGreen(), $color->getBlue(), $alpha / 100 )); } if ($color instanceof Cmyk) { return new ImagickPixel(sprintf( 'cmyka(%d, %d, %d, %d, %F)', $color->getCyan(), $color->getMagenta(), $color->getYellow(), $color->getBlack(), $alpha / 100 )); } if ($color instanceof Gray) { return new ImagickPixel(sprintf( 'graya(%d%%, %F)', $color->getGray(), $alpha / 100 )); } return $this->getColorPixel(new Alpha($alpha, $color->toRgb())); } } src/Renderer/Image/SvgImageBackEnd.php000064400000030502150250215260013544 0ustar00xmlWriter = new XMLWriter(); $this->xmlWriter->openMemory(); $this->xmlWriter->startDocument('1.0', 'UTF-8'); $this->xmlWriter->startElement('svg'); $this->xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/2000/svg'); $this->xmlWriter->writeAttribute('version', '1.1'); $this->xmlWriter->writeAttribute('width', (string) $size); $this->xmlWriter->writeAttribute('height', (string) $size); $this->xmlWriter->writeAttribute('viewBox', '0 0 '. $size . ' ' . $size); $this->gradientCount = 0; $this->currentStack = 0; $this->stack[0] = 0; $alpha = 1; if ($backgroundColor instanceof Alpha) { $alpha = $backgroundColor->getAlpha() / 100; } if (0 === $alpha) { return; } $this->xmlWriter->startElement('rect'); $this->xmlWriter->writeAttribute('x', '0'); $this->xmlWriter->writeAttribute('y', '0'); $this->xmlWriter->writeAttribute('width', (string) $size); $this->xmlWriter->writeAttribute('height', (string) $size); $this->xmlWriter->writeAttribute('fill', $this->getColorString($backgroundColor)); if ($alpha < 1) { $this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha); } $this->xmlWriter->endElement(); } public function scale(float $size) : void { if (null === $this->xmlWriter) { throw new RuntimeException('No image has been started'); } $this->xmlWriter->startElement('g'); $this->xmlWriter->writeAttribute( 'transform', sprintf('scale(%s)', round($size, self::PRECISION)) ); ++$this->stack[$this->currentStack]; } public function translate(float $x, float $y) : void { if (null === $this->xmlWriter) { throw new RuntimeException('No image has been started'); } $this->xmlWriter->startElement('g'); $this->xmlWriter->writeAttribute( 'transform', sprintf('translate(%s,%s)', round($x, self::PRECISION), round($y, self::PRECISION)) ); ++$this->stack[$this->currentStack]; } public function rotate(int $degrees) : void { if (null === $this->xmlWriter) { throw new RuntimeException('No image has been started'); } $this->xmlWriter->startElement('g'); $this->xmlWriter->writeAttribute('transform', sprintf('rotate(%d)', $degrees)); ++$this->stack[$this->currentStack]; } public function push() : void { if (null === $this->xmlWriter) { throw new RuntimeException('No image has been started'); } $this->xmlWriter->startElement('g'); $this->stack[] = 1; ++$this->currentStack; } public function pop() : void { if (null === $this->xmlWriter) { throw new RuntimeException('No image has been started'); } for ($i = 0; $i < $this->stack[$this->currentStack]; ++$i) { $this->xmlWriter->endElement(); } array_pop($this->stack); --$this->currentStack; } public function drawPathWithColor(Path $path, ColorInterface $color) : void { if (null === $this->xmlWriter) { throw new RuntimeException('No image has been started'); } $alpha = 1; if ($color instanceof Alpha) { $alpha = $color->getAlpha() / 100; } $this->startPathElement($path); $this->xmlWriter->writeAttribute('fill', $this->getColorString($color)); if ($alpha < 1) { $this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha); } $this->xmlWriter->endElement(); } public function drawPathWithGradient( Path $path, Gradient $gradient, float $x, float $y, float $width, float $height ) : void { if (null === $this->xmlWriter) { throw new RuntimeException('No image has been started'); } $gradientId = $this->createGradientFill($gradient, $x, $y, $width, $height); $this->startPathElement($path); $this->xmlWriter->writeAttribute('fill', 'url(#' . $gradientId . ')'); $this->xmlWriter->endElement(); } public function done() : string { if (null === $this->xmlWriter) { throw new RuntimeException('No image has been started'); } foreach ($this->stack as $openElements) { for ($i = $openElements; $i > 0; --$i) { $this->xmlWriter->endElement(); } } $this->xmlWriter->endDocument(); $blob = $this->xmlWriter->outputMemory(true); $this->xmlWriter = null; $this->stack = null; $this->currentStack = null; $this->gradientCount = null; return $blob; } private function startPathElement(Path $path) : void { $pathData = []; foreach ($path as $op) { switch (true) { case $op instanceof Move: $pathData[] = sprintf( 'M%s %s', round($op->getX(), self::PRECISION), round($op->getY(), self::PRECISION) ); break; case $op instanceof Line: $pathData[] = sprintf( 'L%s %s', round($op->getX(), self::PRECISION), round($op->getY(), self::PRECISION) ); break; case $op instanceof EllipticArc: $pathData[] = sprintf( 'A%s %s %s %u %u %s %s', round($op->getXRadius(), self::PRECISION), round($op->getYRadius(), self::PRECISION), round($op->getXAxisAngle(), self::PRECISION), $op->isLargeArc(), $op->isSweep(), round($op->getX(), self::PRECISION), round($op->getY(), self::PRECISION) ); break; case $op instanceof Curve: $pathData[] = sprintf( 'C%s %s %s %s %s %s', round($op->getX1(), self::PRECISION), round($op->getY1(), self::PRECISION), round($op->getX2(), self::PRECISION), round($op->getY2(), self::PRECISION), round($op->getX3(), self::PRECISION), round($op->getY3(), self::PRECISION) ); break; case $op instanceof Close: $pathData[] = 'Z'; break; default: throw new RuntimeException('Unexpected draw operation: ' . get_class($op)); } } $this->xmlWriter->startElement('path'); $this->xmlWriter->writeAttribute('fill-rule', 'evenodd'); $this->xmlWriter->writeAttribute('d', implode('', $pathData)); } private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string { $this->xmlWriter->startElement('defs'); $startColor = $gradient->getStartColor(); $endColor = $gradient->getEndColor(); if ($gradient->getType() === GradientType::RADIAL()) { $this->xmlWriter->startElement('radialGradient'); } else { $this->xmlWriter->startElement('linearGradient'); } $this->xmlWriter->writeAttribute('gradientUnits', 'userSpaceOnUse'); switch ($gradient->getType()) { case GradientType::HORIZONTAL(): $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION)); $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION)); $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION)); $this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION)); break; case GradientType::VERTICAL(): $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION)); $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION)); $this->xmlWriter->writeAttribute('x2', (string) round($x, self::PRECISION)); $this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION)); break; case GradientType::DIAGONAL(): $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION)); $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION)); $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION)); $this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION)); break; case GradientType::INVERSE_DIAGONAL(): $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION)); $this->xmlWriter->writeAttribute('y1', (string) round($y + $height, self::PRECISION)); $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION)); $this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION)); break; case GradientType::RADIAL(): $this->xmlWriter->writeAttribute('cx', (string) round(($x + $width) / 2, self::PRECISION)); $this->xmlWriter->writeAttribute('cy', (string) round(($y + $height) / 2, self::PRECISION)); $this->xmlWriter->writeAttribute('r', (string) round(max($width, $height) / 2, self::PRECISION)); break; } $id = sprintf('g%d', ++$this->gradientCount); $this->xmlWriter->writeAttribute('id', $id); $this->xmlWriter->startElement('stop'); $this->xmlWriter->writeAttribute('offset', '0%'); $this->xmlWriter->writeAttribute('stop-color', $this->getColorString($startColor)); if ($startColor instanceof Alpha) { $this->xmlWriter->writeAttribute('stop-opacity', $startColor->getAlpha()); } $this->xmlWriter->endElement(); $this->xmlWriter->startElement('stop'); $this->xmlWriter->writeAttribute('offset', '100%'); $this->xmlWriter->writeAttribute('stop-color', $this->getColorString($endColor)); if ($endColor instanceof Alpha) { $this->xmlWriter->writeAttribute('stop-opacity', $endColor->getAlpha()); } $this->xmlWriter->endElement(); $this->xmlWriter->endElement(); $this->xmlWriter->endElement(); return $id; } private function getColorString(ColorInterface $color) : string { $color = $color->toRgb(); return sprintf( '#%02x%02x%02x', $color->getRed(), $color->getGreen(), $color->getBlue() ); } } src/Renderer/Module/EdgeIterator/Edge.php000064400000003771150250215260014267 0ustar00 */ private $points = []; /** * @var array|null */ private $simplifiedPoints; /** * @var int */ private $minX = PHP_INT_MAX; /** * @var int */ private $minY = PHP_INT_MAX; /** * @var int */ private $maxX = -1; /** * @var int */ private $maxY = -1; public function __construct(bool $positive) { $this->positive = $positive; } public function addPoint(int $x, int $y) : void { $this->points[] = [$x, $y]; $this->minX = min($this->minX, $x); $this->minY = min($this->minY, $y); $this->maxX = max($this->maxX, $x); $this->maxY = max($this->maxY, $y); } public function isPositive() : bool { return $this->positive; } /** * @return array */ public function getPoints() : array { return $this->points; } public function getMaxX() : int { return $this->maxX; } public function getSimplifiedPoints() : array { if (null !== $this->simplifiedPoints) { return $this->simplifiedPoints; } $points = []; $length = count($this->points); for ($i = 0; $i < $length; ++$i) { $previousPoint = $this->points[(0 === $i ? $length : $i) - 1]; $nextPoint = $this->points[($length - 1 === $i ? -1 : $i) + 1]; $currentPoint = $this->points[$i]; if (($previousPoint[0] === $currentPoint[0] && $currentPoint[0] === $nextPoint[0]) || ($previousPoint[1] === $currentPoint[1] && $currentPoint[1] === $nextPoint[1]) ) { continue; } $points[] = $currentPoint; } return $this->simplifiedPoints = $points; } } src/Renderer/Module/EdgeIterator/EdgeIterator.php000064400000007136150250215260016000 0ustar00bytes = iterator_to_array($matrix->getBytes()); $this->size = count($this->bytes); $this->width = $matrix->getWidth(); $this->height = $matrix->getHeight(); } /** * @return Edge[] */ public function getIterator() : Traversable { $originalBytes = $this->bytes; $point = $this->findNext(0, 0); while (null !== $point) { $edge = $this->findEdge($point[0], $point[1]); $this->xorEdge($edge); yield $edge; $point = $this->findNext($point[0], $point[1]); } $this->bytes = $originalBytes; } /** * @return int[]|null */ private function findNext(int $x, int $y) : ?array { $i = $this->width * $y + $x; while ($i < $this->size && 1 !== $this->bytes[$i]) { ++$i; } if ($i < $this->size) { return $this->pointOf($i); } return null; } private function findEdge(int $x, int $y) : Edge { $edge = new Edge($this->isSet($x, $y)); $startX = $x; $startY = $y; $dirX = 0; $dirY = 1; while (true) { $edge->addPoint($x, $y); $x += $dirX; $y += $dirY; if ($x === $startX && $y === $startY) { break; } $left = $this->isSet($x + ($dirX + $dirY - 1 ) / 2, $y + ($dirY - $dirX - 1) / 2); $right = $this->isSet($x + ($dirX - $dirY - 1) / 2, $y + ($dirY + $dirX - 1) / 2); if ($right && ! $left) { $tmp = $dirX; $dirX = -$dirY; $dirY = $tmp; } elseif ($right) { $tmp = $dirX; $dirX = -$dirY; $dirY = $tmp; } elseif (! $left) { $tmp = $dirX; $dirX = $dirY; $dirY = -$tmp; } } return $edge; } private function xorEdge(Edge $path) : void { $points = $path->getPoints(); $y1 = $points[0][1]; $length = count($points); $maxX = $path->getMaxX(); for ($i = 1; $i < $length; ++$i) { $y = $points[$i][1]; if ($y === $y1) { continue; } $x = $points[$i][0]; $minY = min($y1, $y); for ($j = $x; $j < $maxX; ++$j) { $this->flip($j, $minY); } $y1 = $y; } } private function isSet(int $x, int $y) : bool { return ( $x >= 0 && $x < $this->width && $y >= 0 && $y < $this->height ) && 1 === $this->bytes[$this->width * $y + $x]; } /** * @return int[] */ private function pointOf(int $i) : array { $y = intdiv($i, $this->width); return [$i - $y * $this->width, $y]; } private function flip(int $x, int $y) : void { $this->bytes[$this->width * $y + $x] = ( $this->isSet($x, $y) ? 0 : 1 ); } } src/Renderer/Module/DotsModule.php000064400000003434150250215260013120 0ustar00 1) { throw new InvalidArgumentException('Size must between 0 (exclusive) and 1 (inclusive)'); } $this->size = $size; } public function createPath(ByteMatrix $matrix) : Path { $width = $matrix->getWidth(); $height = $matrix->getHeight(); $path = new Path(); $halfSize = $this->size / 2; $margin = (1 - $this->size) / 2; for ($y = 0; $y < $height; ++$y) { for ($x = 0; $x < $width; ++$x) { if (! $matrix->get($x, $y)) { continue; } $pathX = $x + $margin; $pathY = $y + $margin; $path = $path ->move($pathX + $this->size, $pathY + $halfSize) ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY + $this->size) ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX, $pathY + $halfSize) ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY) ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $this->size, $pathY + $halfSize) ->close() ; } } return $path; } } src/Renderer/Module/SquareModule.php000064400000002046150250215260013445 0ustar00getSimplifiedPoints(); $length = count($points); $path = $path->move($points[0][0], $points[0][1]); for ($i = 1; $i < $length; ++$i) { $path = $path->line($points[$i][0], $points[$i][1]); } $path = $path->close(); } return $path; } } src/Renderer/Module/RoundnessModule.php000064400000010574150250215260014172 0ustar00 1) { throw new InvalidArgumentException('Intensity must between 0 (exclusive) and 1 (inclusive)'); } $this->intensity = $intensity / 2; } public function createPath(ByteMatrix $matrix) : Path { $path = new Path(); foreach (new EdgeIterator($matrix) as $edge) { $points = $edge->getSimplifiedPoints(); $length = count($points); $currentPoint = $points[0]; $nextPoint = $points[1]; $horizontal = ($currentPoint[1] === $nextPoint[1]); if ($horizontal) { $right = $nextPoint[0] > $currentPoint[0]; $path = $path->move( $currentPoint[0] + ($right ? $this->intensity : -$this->intensity), $currentPoint[1] ); } else { $up = $nextPoint[0] < $currentPoint[0]; $path = $path->move( $currentPoint[0], $currentPoint[1] + ($up ? -$this->intensity : $this->intensity) ); } for ($i = 1; $i <= $length; ++$i) { if ($i === $length) { $previousPoint = $points[$length - 1]; $currentPoint = $points[0]; $nextPoint = $points[1]; } else { $previousPoint = $points[(0 === $i ? $length : $i) - 1]; $currentPoint = $points[$i]; $nextPoint = $points[($length - 1 === $i ? -1 : $i) + 1]; } $horizontal = ($previousPoint[1] === $currentPoint[1]); if ($horizontal) { $right = $previousPoint[0] < $currentPoint[0]; $up = $nextPoint[1] < $currentPoint[1]; $sweep = ($up xor $right); if ($this->intensity < 0.5 || ($right && $previousPoint[0] !== $currentPoint[0] - 1) || (! $right && $previousPoint[0] - 1 !== $currentPoint[0]) ) { $path = $path->line( $currentPoint[0] + ($right ? -$this->intensity : $this->intensity), $currentPoint[1] ); } $path = $path->ellipticArc( $this->intensity, $this->intensity, 0, false, $sweep, $currentPoint[0], $currentPoint[1] + ($up ? -$this->intensity : $this->intensity) ); } else { $up = $previousPoint[1] > $currentPoint[1]; $right = $nextPoint[0] > $currentPoint[0]; $sweep = ! ($up xor $right); if ($this->intensity < 0.5 || ($up && $previousPoint[1] !== $currentPoint[1] + 1) || (! $up && $previousPoint[0] + 1 !== $currentPoint[0]) ) { $path = $path->line( $currentPoint[0], $currentPoint[1] + ($up ? $this->intensity : -$this->intensity) ); } $path = $path->ellipticArc( $this->intensity, $this->intensity, 0, false, $sweep, $currentPoint[0] + ($right ? $this->intensity : -$this->intensity), $currentPoint[1] ); } } $path = $path->close(); } return $path; } } src/Renderer/Module/ModuleInterface.php000064400000000753150250215260014110 0ustar00rendererStyle = $rendererStyle; $this->imageBackEnd = $imageBackEnd; } /** * @throws InvalidArgumentException if matrix width doesn't match height */ public function render(QrCode $qrCode) : string { $size = $this->rendererStyle->getSize(); $margin = $this->rendererStyle->getMargin(); $matrix = $qrCode->getMatrix(); $matrixSize = $matrix->getWidth(); if ($matrixSize !== $matrix->getHeight()) { throw new InvalidArgumentException('Matrix must have the same width and height'); } $totalSize = $matrixSize + ($margin * 2); $moduleSize = $size / $totalSize; $fill = $this->rendererStyle->getFill(); $this->imageBackEnd->new($size, $fill->getBackgroundColor()); $this->imageBackEnd->scale((float) $moduleSize); $this->imageBackEnd->translate((float) $margin, (float) $margin); $module = $this->rendererStyle->getModule(); $moduleMatrix = clone $matrix; MatrixUtil::removePositionDetectionPatterns($moduleMatrix); $modulePath = $this->drawEyes($matrixSize, $module->createPath($moduleMatrix)); if ($fill->hasGradientFill()) { $this->imageBackEnd->drawPathWithGradient( $modulePath, $fill->getForegroundGradient(), 0, 0, $matrixSize, $matrixSize ); } else { $this->imageBackEnd->drawPathWithColor($modulePath, $fill->getForegroundColor()); } return $this->imageBackEnd->done(); } private function drawEyes(int $matrixSize, Path $modulePath) : Path { $fill = $this->rendererStyle->getFill(); $eye = $this->rendererStyle->getEye(); $externalPath = $eye->getExternalPath(); $internalPath = $eye->getInternalPath(); $modulePath = $this->drawEye( $externalPath, $internalPath, $fill->getTopLeftEyeFill(), 3.5, 3.5, 0, $modulePath ); $modulePath = $this->drawEye( $externalPath, $internalPath, $fill->getTopRightEyeFill(), $matrixSize - 3.5, 3.5, 90, $modulePath ); $modulePath = $this->drawEye( $externalPath, $internalPath, $fill->getBottomLeftEyeFill(), 3.5, $matrixSize - 3.5, -90, $modulePath ); return $modulePath; } private function drawEye( Path $externalPath, Path $internalPath, EyeFill $fill, float $xTranslation, float $yTranslation, int $rotation, Path $modulePath ) : Path { if ($fill->inheritsBothColors()) { return $modulePath ->append($externalPath->translate($xTranslation, $yTranslation)) ->append($internalPath->translate($xTranslation, $yTranslation)); } $this->imageBackEnd->push(); $this->imageBackEnd->translate($xTranslation, $yTranslation); if (0 !== $rotation) { $this->imageBackEnd->rotate($rotation); } if ($fill->inheritsExternalColor()) { $modulePath = $modulePath->append($externalPath->translate($xTranslation, $yTranslation)); } else { $this->imageBackEnd->drawPathWithColor($externalPath, $fill->getExternalColor()); } if ($fill->inheritsInternalColor()) { $modulePath = $modulePath->append($internalPath->translate($xTranslation, $yTranslation)); } else { $this->imageBackEnd->drawPathWithColor($internalPath, $fill->getInternalColor()); } $this->imageBackEnd->pop(); return $modulePath; } } src/Renderer/RendererStyle/EyeFill.php000064400000003301150250215260013725 0ustar00externalColor = $externalColor; $this->internalColor = $internalColor; } public static function uniform(ColorInterface $color) : self { return new self($color, $color); } public static function inherit() : self { return self::$inherit ?: self::$inherit = new self(null, null); } public function inheritsBothColors() : bool { return null === $this->externalColor && null === $this->internalColor; } public function inheritsExternalColor() : bool { return null === $this->externalColor; } public function inheritsInternalColor() : bool { return null === $this->internalColor; } public function getExternalColor() : ColorInterface { if (null === $this->externalColor) { throw new RuntimeException('External eye color inherits foreground color'); } return $this->externalColor; } public function getInternalColor() : ColorInterface { if (null === $this->internalColor) { throw new RuntimeException('Internal eye color inherits foreground color'); } return $this->internalColor; } } src/Renderer/RendererStyle/RendererStyle.php000064400000003257150250215260015175 0ustar00margin = $margin; $this->size = $size; $this->module = $module ?: SquareModule::instance(); $this->eye = $eye ?: new ModuleEye($this->module); $this->fill = $fill ?: Fill::default(); } public function withSize(int $size) : self { $style = clone $this; $style->size = $size; return $style; } public function withMargin(int $margin) : self { $style = clone $this; $style->margin = $margin; return $style; } public function getSize() : int { return $this->size; } public function getMargin() : int { return $this->margin; } public function getModule() : ModuleInterface { return $this->module; } public function getEye() : EyeInterface { return $this->eye; } public function getFill() : Fill { return $this->fill; } } src/Renderer/RendererStyle/Gradient.php000064400000001532150250215260014135 0ustar00startColor = $startColor; $this->endColor = $endColor; $this->type = $type; } public function getStartColor() : ColorInterface { return $this->startColor; } public function getEndColor() : ColorInterface { return $this->endColor; } public function getType() : GradientType { return $this->type; } } src/Renderer/RendererStyle/GradientType.php000064400000001036150250215260014776 0ustar00backgroundColor = $backgroundColor; $this->foregroundColor = $foregroundColor; $this->foregroundGradient = $foregroundGradient; $this->topLeftEyeFill = $topLeftEyeFill; $this->topRightEyeFill = $topRightEyeFill; $this->bottomLeftEyeFill = $bottomLeftEyeFill; } public static function default() : self { return self::$default ?: self::$default = self::uniformColor(new Gray(100), new Gray(0)); } public static function withForegroundColor( ColorInterface $backgroundColor, ColorInterface $foregroundColor, EyeFill $topLeftEyeFill, EyeFill $topRightEyeFill, EyeFill $bottomLeftEyeFill ) : self { return new self( $backgroundColor, $foregroundColor, null, $topLeftEyeFill, $topRightEyeFill, $bottomLeftEyeFill ); } public static function withForegroundGradient( ColorInterface $backgroundColor, Gradient $foregroundGradient, EyeFill $topLeftEyeFill, EyeFill $topRightEyeFill, EyeFill $bottomLeftEyeFill ) : self { return new self( $backgroundColor, null, $foregroundGradient, $topLeftEyeFill, $topRightEyeFill, $bottomLeftEyeFill ); } public static function uniformColor(ColorInterface $backgroundColor, ColorInterface $foregroundColor) : self { return new self( $backgroundColor, $foregroundColor, null, EyeFill::inherit(), EyeFill::inherit(), EyeFill::inherit() ); } public static function uniformGradient(ColorInterface $backgroundColor, Gradient $foregroundGradient) : self { return new self( $backgroundColor, null, $foregroundGradient, EyeFill::inherit(), EyeFill::inherit(), EyeFill::inherit() ); } public function hasGradientFill() : bool { return null !== $this->foregroundGradient; } public function getBackgroundColor() : ColorInterface { return $this->backgroundColor; } public function getForegroundColor() : ColorInterface { if (null === $this->foregroundColor) { throw new RuntimeException('Fill uses a gradient, thus no foreground color is available'); } return $this->foregroundColor; } public function getForegroundGradient() : Gradient { if (null === $this->foregroundGradient) { throw new RuntimeException('Fill uses a single color, thus no foreground gradient is available'); } return $this->foregroundGradient; } public function getTopLeftEyeFill() : EyeFill { return $this->topLeftEyeFill; } public function getTopRightEyeFill() : EyeFill { return $this->topRightEyeFill; } public function getBottomLeftEyeFill() : EyeFill { return $this->bottomLeftEyeFill; } } src/Renderer/RendererInterface.php000064400000000271150250215260013177 0ustar00renderer = $renderer; } /** * Writes QR code and returns it as string. * * Content is a string which *should* be encoded in UTF-8, in case there are * non ASCII-characters present. * * @throws InvalidArgumentException if the content is empty */ public function writeString( string $content, string $encoding = Encoder::DEFAULT_BYTE_MODE_ECODING, ?ErrorCorrectionLevel $ecLevel = null, ?Version $forcedVersion = null ) : string { if (strlen($content) === 0) { throw new InvalidArgumentException('Found empty contents'); } if (null === $ecLevel) { $ecLevel = ErrorCorrectionLevel::L(); } return $this->renderer->render(Encoder::encode($content, $ecLevel, $encoding, $forcedVersion)); } /** * Writes QR code to a file. * * @see Writer::writeString() */ public function writeFile( string $content, string $filename, string $encoding = Encoder::DEFAULT_BYTE_MODE_ECODING, ?ErrorCorrectionLevel $ecLevel = null, ?Version $forcedVersion = null ) : void { file_put_contents($filename, $this->writeString($content, $encoding, $ecLevel, $forcedVersion)); } } src/Encoder/BlockPair.php000064400000002126150250215260011270 0ustar00 */ private $dataBytes; /** * Error correction bytes in the block. * * @var SplFixedArray */ private $errorCorrectionBytes; /** * Creates a new block pair. * * @param SplFixedArray $data * @param SplFixedArray $errorCorrection */ public function __construct(SplFixedArray $data, SplFixedArray $errorCorrection) { $this->dataBytes = $data; $this->errorCorrectionBytes = $errorCorrection; } /** * Gets the data bytes. * * @return SplFixedArray */ public function getDataBytes() : SplFixedArray { return $this->dataBytes; } /** * Gets the error correction bytes. * * @return SplFixedArray */ public function getErrorCorrectionBytes() : SplFixedArray { return $this->errorCorrectionBytes; } } src/Encoder/MaskUtil.php000064400000020272150250215260011155 0ustar00getArray(); $width = $matrix->getWidth(); $height = $matrix->getHeight(); for ($y = 0; $y < $height - 1; ++$y) { for ($x = 0; $x < $width - 1; ++$x) { $value = $array[$y][$x]; if ($value === $array[$y][$x + 1] && $value === $array[$y + 1][$x] && $value === $array[$y + 1][$x + 1] ) { ++$penalty; } } } return self::N2 * $penalty; } /** * Applies mask penalty rule 3 and returns the penalty. * * Finds consecutive cells of 00001011101 or 10111010000, and gives penalty * to them. If we find patterns like 000010111010000, we give penalties * twice (i.e. 40 * 2). */ public static function applyMaskPenaltyRule3(ByteMatrix $matrix) : int { $penalty = 0; $array = $matrix->getArray(); $width = $matrix->getWidth(); $height = $matrix->getHeight(); for ($y = 0; $y < $height; ++$y) { for ($x = 0; $x < $width; ++$x) { if ($x + 6 < $width && 1 === $array[$y][$x] && 0 === $array[$y][$x + 1] && 1 === $array[$y][$x + 2] && 1 === $array[$y][$x + 3] && 1 === $array[$y][$x + 4] && 0 === $array[$y][$x + 5] && 1 === $array[$y][$x + 6] && ( ( $x + 10 < $width && 0 === $array[$y][$x + 7] && 0 === $array[$y][$x + 8] && 0 === $array[$y][$x + 9] && 0 === $array[$y][$x + 10] ) || ( $x - 4 >= 0 && 0 === $array[$y][$x - 1] && 0 === $array[$y][$x - 2] && 0 === $array[$y][$x - 3] && 0 === $array[$y][$x - 4] ) ) ) { $penalty += self::N3; } if ($y + 6 < $height && 1 === $array[$y][$x] && 0 === $array[$y + 1][$x] && 1 === $array[$y + 2][$x] && 1 === $array[$y + 3][$x] && 1 === $array[$y + 4][$x] && 0 === $array[$y + 5][$x] && 1 === $array[$y + 6][$x] && ( ( $y + 10 < $height && 0 === $array[$y + 7][$x] && 0 === $array[$y + 8][$x] && 0 === $array[$y + 9][$x] && 0 === $array[$y + 10][$x] ) || ( $y - 4 >= 0 && 0 === $array[$y - 1][$x] && 0 === $array[$y - 2][$x] && 0 === $array[$y - 3][$x] && 0 === $array[$y - 4][$x] ) ) ) { $penalty += self::N3; } } } return $penalty; } /** * Applies mask penalty rule 4 and returns the penalty. * * Calculates the ratio of dark cells and gives penalty if the ratio is far * from 50%. It gives 10 penalty for 5% distance. */ public static function applyMaskPenaltyRule4(ByteMatrix $matrix) : int { $numDarkCells = 0; $array = $matrix->getArray(); $width = $matrix->getWidth(); $height = $matrix->getHeight(); for ($y = 0; $y < $height; ++$y) { $arrayY = $array[$y]; for ($x = 0; $x < $width; ++$x) { if (1 === $arrayY[$x]) { ++$numDarkCells; } } } $numTotalCells = $height * $width; $darkRatio = $numDarkCells / $numTotalCells; $fixedPercentVariances = (int) (abs($darkRatio - 0.5) * 20); return $fixedPercentVariances * self::N4; } /** * Returns the mask bit for "getMaskPattern" at "x" and "y". * * See 8.8 of JISX0510:2004 for mask pattern conditions. * * @throws InvalidArgumentException if an invalid mask pattern was supplied */ public static function getDataMaskBit(int $maskPattern, int $x, int $y) : bool { switch ($maskPattern) { case 0: $intermediate = ($y + $x) & 0x1; break; case 1: $intermediate = $y & 0x1; break; case 2: $intermediate = $x % 3; break; case 3: $intermediate = ($y + $x) % 3; break; case 4: $intermediate = (BitUtils::unsignedRightShift($y, 1) + (int) ($x / 3)) & 0x1; break; case 5: $temp = $y * $x; $intermediate = ($temp & 0x1) + ($temp % 3); break; case 6: $temp = $y * $x; $intermediate = (($temp & 0x1) + ($temp % 3)) & 0x1; break; case 7: $temp = $y * $x; $intermediate = (($temp % 3) + (($y + $x) & 0x1)) & 0x1; break; default: throw new InvalidArgumentException('Invalid mask pattern: ' . $maskPattern); } return 0 == $intermediate; } /** * Helper function for applyMaskPenaltyRule1. * * We need this for doing this calculation in both vertical and horizontal * orders respectively. */ private static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, bool $isHorizontal) : int { $penalty = 0; $iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth(); $jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight(); $array = $matrix->getArray(); for ($i = 0; $i < $iLimit; ++$i) { $numSameBitCells = 0; $prevBit = -1; for ($j = 0; $j < $jLimit; $j++) { $bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i]; if ($bit === $prevBit) { ++$numSameBitCells; } else { if ($numSameBitCells >= 5) { $penalty += self::N1 + ($numSameBitCells - 5); } $numSameBitCells = 1; $prevBit = $bit; } } if ($numSameBitCells >= 5) { $penalty += self::N1 + ($numSameBitCells - 5); } } return $penalty; } } src/Encoder/MatrixUtil.php000064400000040752150250215260011533 0ustar00clear(-1); } /** * Builds a complete matrix. */ public static function buildMatrix( BitArray $dataBits, ErrorCorrectionLevel $level, Version $version, int $maskPattern, ByteMatrix $matrix ) : void { self::clearMatrix($matrix); self::embedBasicPatterns($version, $matrix); self::embedTypeInfo($level, $maskPattern, $matrix); self::maybeEmbedVersionInfo($version, $matrix); self::embedDataBits($dataBits, $maskPattern, $matrix); } /** * Removes the position detection patterns from a matrix. * * This can be useful if you need to render those patterns separately. */ public static function removePositionDetectionPatterns(ByteMatrix $matrix) : void { $pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]); self::removePositionDetectionPattern(0, 0, $matrix); self::removePositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix); self::removePositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix); } /** * Embeds type information into a matrix. */ private static function embedTypeInfo(ErrorCorrectionLevel $level, int $maskPattern, ByteMatrix $matrix) : void { $typeInfoBits = new BitArray(); self::makeTypeInfoBits($level, $maskPattern, $typeInfoBits); $typeInfoBitsSize = $typeInfoBits->getSize(); for ($i = 0; $i < $typeInfoBitsSize; ++$i) { $bit = $typeInfoBits->get($typeInfoBitsSize - 1 - $i); $x1 = self::TYPE_INFO_COORDINATES[$i][0]; $y1 = self::TYPE_INFO_COORDINATES[$i][1]; $matrix->set($x1, $y1, (int) $bit); if ($i < 8) { $x2 = $matrix->getWidth() - $i - 1; $y2 = 8; } else { $x2 = 8; $y2 = $matrix->getHeight() - 7 + ($i - 8); } $matrix->set($x2, $y2, (int) $bit); } } /** * Generates type information bits and appends them to a bit array. * * @throws RuntimeException if bit array resulted in invalid size */ private static function makeTypeInfoBits(ErrorCorrectionLevel $level, int $maskPattern, BitArray $bits) : void { $typeInfo = ($level->getBits() << 3) | $maskPattern; $bits->appendBits($typeInfo, 5); $bchCode = self::calculateBchCode($typeInfo, self::TYPE_INFO_POLY); $bits->appendBits($bchCode, 10); $maskBits = new BitArray(); $maskBits->appendBits(self::TYPE_INFO_MASK_PATTERN, 15); $bits->xorBits($maskBits); if (15 !== $bits->getSize()) { throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize()); } } /** * Embeds version information if required. */ private static function maybeEmbedVersionInfo(Version $version, ByteMatrix $matrix) : void { if ($version->getVersionNumber() < 7) { return; } $versionInfoBits = new BitArray(); self::makeVersionInfoBits($version, $versionInfoBits); $bitIndex = 6 * 3 - 1; for ($i = 0; $i < 6; ++$i) { for ($j = 0; $j < 3; ++$j) { $bit = $versionInfoBits->get($bitIndex); --$bitIndex; $matrix->set($i, $matrix->getHeight() - 11 + $j, (int) $bit); $matrix->set($matrix->getHeight() - 11 + $j, $i, (int) $bit); } } } /** * Generates version information bits and appends them to a bit array. * * @throws RuntimeException if bit array resulted in invalid size */ private static function makeVersionInfoBits(Version $version, BitArray $bits) : void { $bits->appendBits($version->getVersionNumber(), 6); $bchCode = self::calculateBchCode($version->getVersionNumber(), self::VERSION_INFO_POLY); $bits->appendBits($bchCode, 12); if (18 !== $bits->getSize()) { throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize()); } } /** * Calculates the BCH code for a value and a polynomial. */ private static function calculateBchCode(int $value, int $poly) : int { $msbSetInPoly = self::findMsbSet($poly); $value <<= $msbSetInPoly - 1; while (self::findMsbSet($value) >= $msbSetInPoly) { $value ^= $poly << (self::findMsbSet($value) - $msbSetInPoly); } return $value; } /** * Finds and MSB set. */ private static function findMsbSet(int $value) : int { $numDigits = 0; while (0 !== $value) { $value >>= 1; ++$numDigits; } return $numDigits; } /** * Embeds basic patterns into a matrix. */ private static function embedBasicPatterns(Version $version, ByteMatrix $matrix) : void { self::embedPositionDetectionPatternsAndSeparators($matrix); self::embedDarkDotAtLeftBottomCorner($matrix); self::maybeEmbedPositionAdjustmentPatterns($version, $matrix); self::embedTimingPatterns($matrix); } /** * Embeds position detection patterns and separators into a byte matrix. */ private static function embedPositionDetectionPatternsAndSeparators(ByteMatrix $matrix) : void { $pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]); self::embedPositionDetectionPattern(0, 0, $matrix); self::embedPositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix); self::embedPositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix); $hspWidth = 8; self::embedHorizontalSeparationPattern(0, $hspWidth - 1, $matrix); self::embedHorizontalSeparationPattern($matrix->getWidth() - $hspWidth, $hspWidth - 1, $matrix); self::embedHorizontalSeparationPattern(0, $matrix->getWidth() - $hspWidth, $matrix); $vspSize = 7; self::embedVerticalSeparationPattern($vspSize, 0, $matrix); self::embedVerticalSeparationPattern($matrix->getHeight() - $vspSize - 1, 0, $matrix); self::embedVerticalSeparationPattern($vspSize, $matrix->getHeight() - $vspSize, $matrix); } /** * Embeds a single position detection pattern into a byte matrix. */ private static function embedPositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void { for ($y = 0; $y < 7; ++$y) { for ($x = 0; $x < 7; ++$x) { $matrix->set($xStart + $x, $yStart + $y, self::POSITION_DETECTION_PATTERN[$y][$x]); } } } private static function removePositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void { for ($y = 0; $y < 7; ++$y) { for ($x = 0; $x < 7; ++$x) { $matrix->set($xStart + $x, $yStart + $y, 0); } } } /** * Embeds a single horizontal separation pattern. * * @throws RuntimeException if a byte was already set */ private static function embedHorizontalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void { for ($x = 0; $x < 8; $x++) { if (-1 !== $matrix->get($xStart + $x, $yStart)) { throw new RuntimeException('Byte already set'); } $matrix->set($xStart + $x, $yStart, 0); } } /** * Embeds a single vertical separation pattern. * * @throws RuntimeException if a byte was already set */ private static function embedVerticalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void { for ($y = 0; $y < 7; $y++) { if (-1 !== $matrix->get($xStart, $yStart + $y)) { throw new RuntimeException('Byte already set'); } $matrix->set($xStart, $yStart + $y, 0); } } /** * Embeds a dot at the left bottom corner. * * @throws RuntimeException if a byte was already set to 0 */ private static function embedDarkDotAtLeftBottomCorner(ByteMatrix $matrix) : void { if (0 === $matrix->get(8, $matrix->getHeight() - 8)) { throw new RuntimeException('Byte already set to 0'); } $matrix->set(8, $matrix->getHeight() - 8, 1); } /** * Embeds position adjustment patterns if required. */ private static function maybeEmbedPositionAdjustmentPatterns(Version $version, ByteMatrix $matrix) : void { if ($version->getVersionNumber() < 2) { return; } $index = $version->getVersionNumber() - 1; $coordinates = self::POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[$index]; $numCoordinates = count($coordinates); for ($i = 0; $i < $numCoordinates; ++$i) { for ($j = 0; $j < $numCoordinates; ++$j) { $y = $coordinates[$i]; $x = $coordinates[$j]; if (null === $x || null === $y) { continue; } if (-1 === $matrix->get($x, $y)) { self::embedPositionAdjustmentPattern($x - 2, $y - 2, $matrix); } } } } /** * Embeds a single position adjustment pattern. */ private static function embedPositionAdjustmentPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void { for ($y = 0; $y < 5; $y++) { for ($x = 0; $x < 5; $x++) { $matrix->set($xStart + $x, $yStart + $y, self::POSITION_ADJUSTMENT_PATTERN[$y][$x]); } } } /** * Embeds timing patterns into a matrix. */ private static function embedTimingPatterns(ByteMatrix $matrix) : void { $matrixWidth = $matrix->getWidth(); for ($i = 8; $i < $matrixWidth - 8; ++$i) { $bit = ($i + 1) % 2; if (-1 === $matrix->get($i, 6)) { $matrix->set($i, 6, $bit); } if (-1 === $matrix->get(6, $i)) { $matrix->set(6, $i, $bit); } } } /** * Embeds "dataBits" using "getMaskPattern". * * For debugging purposes, it skips masking process if "getMaskPattern" is -1. See 8.7 of JISX0510:2004 (p.38) for * how to embed data bits. * * @throws WriterException if not all bits could be consumed */ private static function embedDataBits(BitArray $dataBits, int $maskPattern, ByteMatrix $matrix) : void { $bitIndex = 0; $direction = -1; // Start from the right bottom cell. $x = $matrix->getWidth() - 1; $y = $matrix->getHeight() - 1; while ($x > 0) { // Skip vertical timing pattern. if (6 === $x) { --$x; } while ($y >= 0 && $y < $matrix->getHeight()) { for ($i = 0; $i < 2; $i++) { $xx = $x - $i; // Skip the cell if it's not empty. if (-1 !== $matrix->get($xx, $y)) { continue; } if ($bitIndex < $dataBits->getSize()) { $bit = $dataBits->get($bitIndex); ++$bitIndex; } else { // Padding bit. If there is no bit left, we'll fill the // left cells with 0, as described in 8.4.9 of // JISX0510:2004 (p. 24). $bit = false; } // Skip masking if maskPattern is -1. if (-1 !== $maskPattern && MaskUtil::getDataMaskBit($maskPattern, $xx, $y)) { $bit = ! $bit; } $matrix->set($xx, $y, (int) $bit); } $y += $direction; } $direction = -$direction; $y += $direction; $x -= 2; } // All bits should be consumed if ($dataBits->getSize() !== $bitIndex) { throw new WriterException('Not all bits consumed (' . $bitIndex . ' out of ' . $dataBits->getSize() .')'); } } } src/Encoder/ByteMatrix.php000064400000005727150250215260011524 0ustar00> */ private $bytes; /** * Width of the matrix. * * @var int */ private $width; /** * Height of the matrix. * * @var int */ private $height; public function __construct(int $width, int $height) { $this->height = $height; $this->width = $width; $this->bytes = new SplFixedArray($height); for ($y = 0; $y < $height; ++$y) { $this->bytes[$y] = SplFixedArray::fromArray(array_fill(0, $width, 0)); } } /** * Gets the width of the matrix. */ public function getWidth() : int { return $this->width; } /** * Gets the height of the matrix. */ public function getHeight() : int { return $this->height; } /** * Gets the internal representation of the matrix. * * @return SplFixedArray> */ public function getArray() : SplFixedArray { return $this->bytes; } /** * @return Traversable */ public function getBytes() : Traversable { foreach ($this->bytes as $row) { foreach ($row as $byte) { yield $byte; } } } /** * Gets the byte for a specific position. */ public function get(int $x, int $y) : int { return $this->bytes[$y][$x]; } /** * Sets the byte for a specific position. */ public function set(int $x, int $y, int $value) : void { $this->bytes[$y][$x] = $value; } /** * Clears the matrix with a specific value. */ public function clear(int $value) : void { for ($y = 0; $y < $this->height; ++$y) { for ($x = 0; $x < $this->width; ++$x) { $this->bytes[$y][$x] = $value; } } } public function __clone() { $this->bytes = clone $this->bytes; foreach ($this->bytes as $index => $row) { $this->bytes[$index] = clone $row; } } /** * Returns a string representation of the matrix. */ public function __toString() : string { $result = ''; for ($y = 0; $y < $this->height; $y++) { for ($x = 0; $x < $this->width; $x++) { switch ($this->bytes[$y][$x]) { case 0: $result .= ' 0'; break; case 1: $result .= ' 1'; break; default: $result .= ' '; break; } } $result .= "\n"; } return $result; } } src/Encoder/Encoder.php000064400000052726150250215260011014 0ustar00getSize() + $mode->getCharacterCountBits(Version::getVersionForNumber(1)) + $dataBits->getSize(); $provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel); // Use that guess to calculate the right version. I am still not sure // this works in 100% of cases. $bitsNeeded = $headerBits->getSize() + $mode->getCharacterCountBits($provisionalVersion) + $dataBits->getSize(); $version = self::chooseVersion($bitsNeeded, $ecLevel); if (null !== $forcedVersion) { // Forced version check if ($version->getVersionNumber() <= $forcedVersion->getVersionNumber()) { // Calculated minimum version is same or equal as forced version $version = $forcedVersion; } else { throw new WriterException( 'Invalid version! Calculated version: ' . $version->getVersionNumber() . ', requested version: ' . $forcedVersion->getVersionNumber() ); } } $headerAndDataBits = new BitArray(); $headerAndDataBits->appendBitArray($headerBits); // Find "length" of main segment and write it. $numLetters = (Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : strlen($content)); self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits); // Put data together into the overall payload. $headerAndDataBits->appendBitArray($dataBits); $ecBlocks = $version->getEcBlocksForLevel($ecLevel); $numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords(); // Terminate the bits properly. self::terminateBits($numDataBytes, $headerAndDataBits); // Interleave data bits with error correction code. $finalBits = self::interleaveWithEcBytes( $headerAndDataBits, $version->getTotalCodewords(), $numDataBytes, $ecBlocks->getNumBlocks() ); // Choose the mask pattern. $dimension = $version->getDimensionForVersion(); $matrix = new ByteMatrix($dimension, $dimension); $maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix); // Build the matrix. MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix); return new QrCode($mode, $ecLevel, $version, $maskPattern, $matrix); } /** * Gets the alphanumeric code for a byte. */ private static function getAlphanumericCode(int $code) : int { if (isset(self::ALPHANUMERIC_TABLE[$code])) { return self::ALPHANUMERIC_TABLE[$code]; } return -1; } /** * Chooses the best mode for a given content. */ private static function chooseMode(string $content, string $encoding = null) : Mode { if (null !== $encoding && 0 === strcasecmp($encoding, 'SHIFT-JIS')) { return self::isOnlyDoubleByteKanji($content) ? Mode::KANJI() : Mode::BYTE(); } $hasNumeric = false; $hasAlphanumeric = false; $contentLength = strlen($content); for ($i = 0; $i < $contentLength; ++$i) { $char = $content[$i]; if (ctype_digit($char)) { $hasNumeric = true; } elseif (-1 !== self::getAlphanumericCode(ord($char))) { $hasAlphanumeric = true; } else { return Mode::BYTE(); } } if ($hasAlphanumeric) { return Mode::ALPHANUMERIC(); } elseif ($hasNumeric) { return Mode::NUMERIC(); } return Mode::BYTE(); } /** * Calculates the mask penalty for a matrix. */ private static function calculateMaskPenalty(ByteMatrix $matrix) : int { return ( MaskUtil::applyMaskPenaltyRule1($matrix) + MaskUtil::applyMaskPenaltyRule2($matrix) + MaskUtil::applyMaskPenaltyRule3($matrix) + MaskUtil::applyMaskPenaltyRule4($matrix) ); } /** * Checks if content only consists of double-byte kanji characters. */ private static function isOnlyDoubleByteKanji(string $content) : bool { $bytes = @iconv('utf-8', 'SHIFT-JIS', $content); if (false === $bytes) { return false; } $length = strlen($bytes); if (0 !== $length % 2) { return false; } for ($i = 0; $i < $length; $i += 2) { $byte = $bytes[$i] & 0xff; if (($byte < 0x81 || $byte > 0x9f) && $byte < 0xe0 || $byte > 0xeb) { return false; } } return true; } /** * Chooses the best mask pattern for a matrix. */ private static function chooseMaskPattern( BitArray $bits, ErrorCorrectionLevel $ecLevel, Version $version, ByteMatrix $matrix ) : int { $minPenalty = PHP_INT_MAX; $bestMaskPattern = -1; for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; ++$maskPattern) { MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix); $penalty = self::calculateMaskPenalty($matrix); if ($penalty < $minPenalty) { $minPenalty = $penalty; $bestMaskPattern = $maskPattern; } } return $bestMaskPattern; } /** * Chooses the best version for the input. * * @throws WriterException if data is too big */ private static function chooseVersion(int $numInputBits, ErrorCorrectionLevel $ecLevel) : Version { for ($versionNum = 1; $versionNum <= 40; ++$versionNum) { $version = Version::getVersionForNumber($versionNum); $numBytes = $version->getTotalCodewords(); $ecBlocks = $version->getEcBlocksForLevel($ecLevel); $numEcBytes = $ecBlocks->getTotalEcCodewords(); $numDataBytes = $numBytes - $numEcBytes; $totalInputBytes = intdiv($numInputBits + 8, 8); if ($numDataBytes >= $totalInputBytes) { return $version; } } throw new WriterException('Data too big'); } /** * Terminates the bits in a bit array. * * @throws WriterException if data bits cannot fit in the QR code * @throws WriterException if bits size does not equal the capacity */ private static function terminateBits(int $numDataBytes, BitArray $bits) : void { $capacity = $numDataBytes << 3; if ($bits->getSize() > $capacity) { throw new WriterException('Data bits cannot fit in the QR code'); } for ($i = 0; $i < 4 && $bits->getSize() < $capacity; ++$i) { $bits->appendBit(false); } $numBitsInLastByte = $bits->getSize() & 0x7; if ($numBitsInLastByte > 0) { for ($i = $numBitsInLastByte; $i < 8; ++$i) { $bits->appendBit(false); } } $numPaddingBytes = $numDataBytes - $bits->getSizeInBytes(); for ($i = 0; $i < $numPaddingBytes; ++$i) { $bits->appendBits(0 === ($i & 0x1) ? 0xec : 0x11, 8); } if ($bits->getSize() !== $capacity) { throw new WriterException('Bits size does not equal capacity'); } } /** * Gets number of data- and EC bytes for a block ID. * * @return int[] * @throws WriterException if block ID is too large * @throws WriterException if EC bytes mismatch * @throws WriterException if RS blocks mismatch * @throws WriterException if total bytes mismatch */ private static function getNumDataBytesAndNumEcBytesForBlockId( int $numTotalBytes, int $numDataBytes, int $numRsBlocks, int $blockId ) : array { if ($blockId >= $numRsBlocks) { throw new WriterException('Block ID too large'); } $numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks; $numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2; $numTotalBytesInGroup1 = intdiv($numTotalBytes, $numRsBlocks); $numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1; $numDataBytesInGroup1 = intdiv($numDataBytes, $numRsBlocks); $numDataBytesInGroup2 = $numDataBytesInGroup1 + 1; $numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1; $numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2; if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) { throw new WriterException('EC bytes mismatch'); } if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) { throw new WriterException('RS blocks mismatch'); } if ($numTotalBytes !== (($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1) + (($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2) ) { throw new WriterException('Total bytes mismatch'); } if ($blockId < $numRsBlocksInGroup1) { return [$numDataBytesInGroup1, $numEcBytesInGroup1]; } else { return [$numDataBytesInGroup2, $numEcBytesInGroup2]; } } /** * Interleaves data with EC bytes. * * @throws WriterException if number of bits and data bytes does not match * @throws WriterException if data bytes does not match offset * @throws WriterException if an interleaving error occurs */ private static function interleaveWithEcBytes( BitArray $bits, int $numTotalBytes, int $numDataBytes, int $numRsBlocks ) : BitArray { if ($bits->getSizeInBytes() !== $numDataBytes) { throw new WriterException('Number of bits and data bytes does not match'); } $dataBytesOffset = 0; $maxNumDataBytes = 0; $maxNumEcBytes = 0; $blocks = new SplFixedArray($numRsBlocks); for ($i = 0; $i < $numRsBlocks; ++$i) { list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId( $numTotalBytes, $numDataBytes, $numRsBlocks, $i ); $size = $numDataBytesInBlock; $dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size); $ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock); $blocks[$i] = new BlockPair($dataBytes, $ecBytes); $maxNumDataBytes = max($maxNumDataBytes, $size); $maxNumEcBytes = max($maxNumEcBytes, count($ecBytes)); $dataBytesOffset += $numDataBytesInBlock; } if ($numDataBytes !== $dataBytesOffset) { throw new WriterException('Data bytes does not match offset'); } $result = new BitArray(); for ($i = 0; $i < $maxNumDataBytes; ++$i) { foreach ($blocks as $block) { $dataBytes = $block->getDataBytes(); if ($i < count($dataBytes)) { $result->appendBits($dataBytes[$i], 8); } } } for ($i = 0; $i < $maxNumEcBytes; ++$i) { foreach ($blocks as $block) { $ecBytes = $block->getErrorCorrectionBytes(); if ($i < count($ecBytes)) { $result->appendBits($ecBytes[$i], 8); } } } if ($numTotalBytes !== $result->getSizeInBytes()) { throw new WriterException( 'Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ' ); } return $result; } /** * Generates EC bytes for given data. * * @param SplFixedArray $dataBytes * @return SplFixedArray */ private static function generateEcBytes(SplFixedArray $dataBytes, int $numEcBytesInBlock) : SplFixedArray { $numDataBytes = count($dataBytes); $toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock); for ($i = 0; $i < $numDataBytes; $i++) { $toEncode[$i] = $dataBytes[$i] & 0xff; } $ecBytes = new SplFixedArray($numEcBytesInBlock); $codec = self::getCodec($numDataBytes, $numEcBytesInBlock); $codec->encode($toEncode, $ecBytes); return $ecBytes; } /** * Gets an RS codec and caches it. */ private static function getCodec(int $numDataBytes, int $numEcBytesInBlock) : ReedSolomonCodec { $cacheId = $numDataBytes . '-' . $numEcBytesInBlock; if (isset(self::$codecs[$cacheId])) { return self::$codecs[$cacheId]; } return self::$codecs[$cacheId] = new ReedSolomonCodec( 8, 0x11d, 0, 1, $numEcBytesInBlock, 255 - $numDataBytes - $numEcBytesInBlock ); } /** * Appends mode information to a bit array. */ private static function appendModeInfo(Mode $mode, BitArray $bits) : void { $bits->appendBits($mode->getBits(), 4); } /** * Appends length information to a bit array. * * @throws WriterException if num letters is bigger than expected */ private static function appendLengthInfo(int $numLetters, Version $version, Mode $mode, BitArray $bits) : void { $numBits = $mode->getCharacterCountBits($version); if ($numLetters >= (1 << $numBits)) { throw new WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1)); } $bits->appendBits($numLetters, $numBits); } /** * Appends bytes to a bit array in a specific mode. * * @throws WriterException if an invalid mode was supplied */ private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void { switch ($mode) { case Mode::NUMERIC(): self::appendNumericBytes($content, $bits); break; case Mode::ALPHANUMERIC(): self::appendAlphanumericBytes($content, $bits); break; case Mode::BYTE(): self::append8BitBytes($content, $bits, $encoding); break; case Mode::KANJI(): self::appendKanjiBytes($content, $bits); break; default: throw new WriterException('Invalid mode: ' . $mode); } } /** * Appends numeric bytes to a bit array. */ private static function appendNumericBytes(string $content, BitArray $bits) : void { $length = strlen($content); $i = 0; while ($i < $length) { $num1 = (int) $content[$i]; if ($i + 2 < $length) { // Encode three numeric letters in ten bits. $num2 = (int) $content[$i + 1]; $num3 = (int) $content[$i + 2]; $bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10); $i += 3; } elseif ($i + 1 < $length) { // Encode two numeric letters in seven bits. $num2 = (int) $content[$i + 1]; $bits->appendBits($num1 * 10 + $num2, 7); $i += 2; } else { // Encode one numeric letter in four bits. $bits->appendBits($num1, 4); ++$i; } } } /** * Appends alpha-numeric bytes to a bit array. * * @throws WriterException if an invalid alphanumeric code was found */ private static function appendAlphanumericBytes(string $content, BitArray $bits) : void { $length = strlen($content); $i = 0; while ($i < $length) { $code1 = self::getAlphanumericCode(ord($content[$i])); if (-1 === $code1) { throw new WriterException('Invalid alphanumeric code'); } if ($i + 1 < $length) { $code2 = self::getAlphanumericCode(ord($content[$i + 1])); if (-1 === $code2) { throw new WriterException('Invalid alphanumeric code'); } // Encode two alphanumeric letters in 11 bits. $bits->appendBits($code1 * 45 + $code2, 11); $i += 2; } else { // Encode one alphanumeric letter in six bits. $bits->appendBits($code1, 6); ++$i; } } } /** * Appends regular 8-bit bytes to a bit array. * * @throws WriterException if content cannot be encoded to target encoding */ private static function append8BitBytes(string $content, BitArray $bits, string $encoding) : void { $bytes = @iconv('utf-8', $encoding, $content); if (false === $bytes) { throw new WriterException('Could not encode content to ' . $encoding); } $length = strlen($bytes); for ($i = 0; $i < $length; $i++) { $bits->appendBits(ord($bytes[$i]), 8); } } /** * Appends KANJI bytes to a bit array. * * @throws WriterException if content does not seem to be encoded in SHIFT-JIS * @throws WriterException if an invalid byte sequence occurs */ private static function appendKanjiBytes(string $content, BitArray $bits) : void { if (strlen($content) % 2 > 0) { // We just do a simple length check here. The for loop will check // individual characters. throw new WriterException('Content does not seem to be encoded in SHIFT-JIS'); } $length = strlen($content); for ($i = 0; $i < $length; $i += 2) { $byte1 = ord($content[$i]) & 0xff; $byte2 = ord($content[$i + 1]) & 0xff; $code = ($byte1 << 8) | $byte2; if ($code >= 0x8140 && $code <= 0x9ffc) { $subtracted = $code - 0x8140; } elseif ($code >= 0xe040 && $code <= 0xebbf) { $subtracted = $code - 0xc140; } else { throw new WriterException('Invalid byte sequence'); } $encoded = (($subtracted >> 8) * 0xc0) + ($subtracted & 0xff); $bits->appendBits($encoded, 13); } } /** * Appends ECI information to a bit array. */ private static function appendEci(CharacterSetEci $eci, BitArray $bits) : void { $mode = Mode::ECI(); $bits->appendBits($mode->getBits(), 4); $bits->appendBits($eci->getValue(), 8); } } src/Encoder/QrCode.php000064400000005324150250215260010602 0ustar00mode = $mode; $this->errorCorrectionLevel = $errorCorrectionLevel; $this->version = $version; $this->maskPattern = $maskPattern; $this->matrix = $matrix; } /** * Gets the mode. */ public function getMode() : Mode { return $this->mode; } /** * Gets the EC level. */ public function getErrorCorrectionLevel() : ErrorCorrectionLevel { return $this->errorCorrectionLevel; } /** * Gets the version. */ public function getVersion() : Version { return $this->version; } /** * Gets the mask pattern. */ public function getMaskPattern() : int { return $this->maskPattern; } /** * Gets the matrix. * * @return ByteMatrix */ public function getMatrix() { return $this->matrix; } /** * Validates whether a mask pattern is valid. */ public static function isValidMaskPattern(int $maskPattern) : bool { return $maskPattern > 0 && $maskPattern < self::NUM_MASK_PATTERNS; } /** * Returns a string representation of the QR code. */ public function __toString() : string { $result = "<<\n" . ' mode: ' . $this->mode . "\n" . ' ecLevel: ' . $this->errorCorrectionLevel . "\n" . ' version: ' . $this->version . "\n" . ' maskPattern: ' . $this->maskPattern . "\n"; if ($this->matrix === null) { $result .= " matrix: null\n"; } else { $result .= " matrix:\n"; $result .= $this->matrix; } $result .= ">>\n"; return $result; } } README.md000064400000003236150250215260006025 0ustar00# QR Code generator [![PHP CI](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml/badge.svg)](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/Bacon/BaconQrCode/branch/master/graph/badge.svg?token=rD0HcAiEEx)](https://codecov.io/gh/Bacon/BaconQrCode) [![Latest Stable Version](https://poser.pugx.org/bacon/bacon-qr-code/v/stable)](https://packagist.org/packages/bacon/bacon-qr-code) [![Total Downloads](https://poser.pugx.org/bacon/bacon-qr-code/downloads)](https://packagist.org/packages/bacon/bacon-qr-code) [![License](https://poser.pugx.org/bacon/bacon-qr-code/license)](https://packagist.org/packages/bacon/bacon-qr-code) ## Introduction BaconQrCode is a port of QR code portion of the ZXing library. It currently only features the encoder part, but could later receive the decoder part as well. As the Reed Solomon codec implementation of the ZXing library performs quite slow in PHP, it was exchanged with the implementation by Phil Karn. ## Example usage ```php use BaconQrCode\Renderer\ImageRenderer; use BaconQrCode\Renderer\Image\ImagickImageBackEnd; use BaconQrCode\Renderer\RendererStyle\RendererStyle; use BaconQrCode\Writer; $renderer = new ImageRenderer( new RendererStyle(400), new ImagickImageBackEnd() ); $writer = new Writer($renderer); $writer->writeFile('Hello World!', 'qrcode.png'); ``` ## Available image renderer back ends BaconQrCode comes with multiple back ends for rendering images. Currently included are the following: - `ImagickImageBackEnd`: renders raster images using the Imagick library - `SvgImageBackEnd`: renders SVG files using XMLWriter - `EpsImageBackEnd`: renders EPS files LICENSE000064400000002434150250215260005552 0ustar00Copyright (c) 2017, Ben Scholzen 'DASPRiD' All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. test/Common/ErrorCorrectionLevelTest.php000064400000001417150250215260014436 0ustar00assertSame(0x0, ErrorCorrectionLevel::M()->getBits()); $this->assertSame(0x1, ErrorCorrectionLevel::L()->getBits()); $this->assertSame(0x2, ErrorCorrectionLevel::H()->getBits()); $this->assertSame(0x3, ErrorCorrectionLevel::Q()->getBits()); } public function testInvalidErrorCorrectionLevelThrowsException() : void { $this->expectException(OutOfBoundsException::class); ErrorCorrectionLevel::forBits(4); } } test/Common/ModeTest.php000064400000001026150250215260011205 0ustar00assertSame(0x0, Mode::TERMINATOR()->getBits()); $this->assertSame(0x1, Mode::NUMERIC()->getBits()); $this->assertSame(0x2, Mode::ALPHANUMERIC()->getBits()); $this->assertSame(0x4, Mode::BYTE()->getBits()); $this->assertSame(0x8, Mode::KANJI()->getBits()); } } test/Common/FormatInformationTest.php000064400000005651150250215260013767 0ustar00assertSame(0, FormatInformation::numBitsDiffering(1, 1)); $this->assertSame(1, FormatInformation::numBitsDiffering(0, 2)); $this->assertSame(2, FormatInformation::numBitsDiffering(1, 2)); $this->assertEquals(32, FormatInformation::numBitsDiffering(-1, 0)); } public function testDecode() : void { $expected = FormatInformation::decodeFormatInformation( self::MASKED_TEST_FORMAT_INFO, self::MASKED_TEST_FORMAT_INFO ); $this->assertNotNull($expected); $this->assertSame(7, $expected->getDataMask()); $this->assertSame(ErrorCorrectionLevel::Q(), $expected->getErrorCorrectionLevel()); $this->assertEquals( $expected, FormatInformation::decodeFormatInformation( self::UNMAKSED_TEST_FORMAT_INFO, self::MASKED_TEST_FORMAT_INFO ) ); } public function testDecodeWithBitDifference() : void { $expected = FormatInformation::decodeFormatInformation( self::MASKED_TEST_FORMAT_INFO, self::MASKED_TEST_FORMAT_INFO ); $this->assertEquals( $expected, FormatInformation::decodeFormatInformation( self::MASKED_TEST_FORMAT_INFO ^ 0x1, self::MASKED_TEST_FORMAT_INFO ^ 0x1 ) ); $this->assertEquals( $expected, FormatInformation::decodeFormatInformation( self::MASKED_TEST_FORMAT_INFO ^ 0x3, self::MASKED_TEST_FORMAT_INFO ^ 0x3 ) ); $this->assertEquals( $expected, FormatInformation::decodeFormatInformation( self::MASKED_TEST_FORMAT_INFO ^ 0x7, self::MASKED_TEST_FORMAT_INFO ^ 0x7 ) ); $this->assertNull( FormatInformation::decodeFormatInformation( self::MASKED_TEST_FORMAT_INFO ^ 0xf, self::MASKED_TEST_FORMAT_INFO ^ 0xf ) ); } public function testDecodeWithMisRead() : void { $expected = FormatInformation::decodeFormatInformation( self::MASKED_TEST_FORMAT_INFO, self::MASKED_TEST_FORMAT_INFO ); $this->assertEquals( $expected, FormatInformation::decodeFormatInformation( self::MASKED_TEST_FORMAT_INFO ^ 0x3, self::MASKED_TEST_FORMAT_INFO ^ 0xf ) ); } } test/Common/BitUtilsTest.php000064400000001420150250215260012056 0ustar00assertSame(1, BitUtils::unsignedRightShift(1, 0)); $this->assertSame(1, BitUtils::unsignedRightShift(10, 3)); $this->assertSame(536870910, BitUtils::unsignedRightShift(-10, 3)); } public function testNumberOfTrailingZeros() : void { $this->assertSame(32, BitUtils::numberOfTrailingZeros(0)); $this->assertSame(1, BitUtils::numberOfTrailingZeros(10)); $this->assertSame(0, BitUtils::numberOfTrailingZeros(15)); $this->assertSame(2, BitUtils::numberOfTrailingZeros(20)); } } test/Common/BitArrayTest.php000064400000013645150250215260012050 0ustar00assertFalse($array->get($i)); $array->set($i); $this->assertTrue($array->get($i)); } } public function testGetNextSet1() : void { $array = new BitArray(32); for ($i = 0; $i < $array->getSize(); ++$i) { if ($this->getPhpUnitMajorVersion() === 7) { $this->assertEquals($i, 32, '', $array->getNextSet($i)); } else { $this->assertEqualsWithDelta($i, 32, $array->getNextSet($i)); } } $array = new BitArray(33); for ($i = 0; $i < $array->getSize(); ++$i) { if ($this->getPhpUnitMajorVersion() === 7) { $this->assertEquals($i, 33, '', $array->getNextSet($i)); } else { $this->assertEqualsWithDelta($i, 33, $array->getNextSet($i)); } } } public function testGetNextSet2() : void { $array = new BitArray(33); for ($i = 0; $i < $array->getSize(); ++$i) { if ($this->getPhpUnitMajorVersion() === 7) { $this->assertEquals($i, $i <= 31 ? 31 : 33, '', $array->getNextSet($i)); } else { $this->assertEqualsWithDelta($i, $i <= 31 ? 31 : 33, $array->getNextSet($i)); } } $array = new BitArray(33); for ($i = 0; $i < $array->getSize(); ++$i) { if ($this->getPhpUnitMajorVersion() === 7) { $this->assertEquals($i, 32, '', $array->getNextSet($i)); } else { $this->assertEqualsWithDelta($i, 32, $array->getNextSet($i)); } } } public function testGetNextSet3() : void { $array = new BitArray(63); $array->set(31); $array->set(32); for ($i = 0; $i < $array->getSize(); ++$i) { if ($i <= 31) { $expected = 31; } elseif ($i <= 32) { $expected = 32; } else { $expected = 63; } if ($this->getPhpUnitMajorVersion() === 7) { $this->assertEquals($i, $expected, '', $array->getNextSet($i)); } else { $this->assertEqualsWithDelta($i, $expected, $array->getNextSet($i)); } } } public function testGetNextSet4() : void { $array = new BitArray(63); $array->set(33); $array->set(40); for ($i = 0; $i < $array->getSize(); ++$i) { if ($i <= 33) { $expected = 33; } elseif ($i <= 40) { $expected = 40; } else { $expected = 63; } if ($this->getPhpUnitMajorVersion() === 7) { $this->assertEquals($i, $expected, '', $array->getNextSet($i)); } else { $this->assertEqualsWithDelta($i, $expected, $array->getNextSet($i)); } } } public function testGetNextSet5() : void { mt_srand(0xdeadbeef, MT_RAND_PHP); for ($i = 0; $i < 10; ++$i) { $array = new BitArray(mt_rand(1, 100)); $numSet = mt_rand(0, 19); for ($j = 0; $j < $numSet; ++$j) { $array->set(mt_rand(0, $array->getSize() - 1)); } $numQueries = mt_rand(0, 19); for ($j = 0; $j < $numQueries; ++$j) { $query = mt_rand(0, $array->getSize() - 1); $expected = $query; while ($expected < $array->getSize() && ! $array->get($expected)) { ++$expected; } $actual = $array->getNextSet($query); if ($actual !== $expected) { $array->getNextSet($query); } $this->assertEquals($expected, $actual); } } } public function testSetBulk() : void { $array = new BitArray(64); $array->setBulk(32, 0xFFFF0000); for ($i = 0; $i < 48; ++$i) { $this->assertFalse($array->get($i)); } for ($i = 48; $i < 64; ++$i) { $this->assertTrue($array->get($i)); } } public function testClear() : void { $array = new BitArray(32); for ($i = 0; $i < 32; ++$i) { $array->set($i); } $array->clear(); for ($i = 0; $i < 32; ++$i) { $this->assertFalse($array->get($i)); } } public function testGetArray() : void { $array = new BitArray(64); $array->set(0); $array->set(63); $ints = $array->getBitArray(); $this->assertSame(1, $ints[0]); $this->assertSame(0x80000000, $ints[1]); } public function testIsRange() : void { $array = new BitArray(64); $this->assertTrue($array->isRange(0, 64, false)); $this->assertFalse($array->isRange(0, 64, true)); $array->set(32); $this->assertTrue($array->isRange(32, 33, true)); $array->set(31); $this->assertTrue($array->isRange(31, 33, true)); $array->set(34); $this->assertFalse($array->isRange(31, 35, true)); for ($i = 0; $i < 31; ++$i) { $array->set($i); } $this->assertTrue($array->isRange(0, 33, true)); for ($i = 33; $i < 64; ++$i) { $array->set($i); } $this->assertTrue($array->isRange(0, 64, true)); $this->assertFalse($array->isRange(0, 64, false)); } } test/Common/BitMatrixTest.php000064400000006240150250215260012227 0ustar00assertEquals(33, $matrix->getHeight()); for ($y = 0; $y < 33; ++$y) { for ($x = 0; $x < 33; ++$x) { if ($y * $x % 3 === 0) { $matrix->set($x, $y); } } } for ($y = 0; $y < 33; $y++) { for ($x = 0; $x < 33; ++$x) { $this->assertSame(0 === $x * $y % 3, $matrix->get($x, $y)); } } } public function testSetRegion() : void { $matrix = new BitMatrix(5); $matrix->setRegion(1, 1, 3, 3); for ($y = 0; $y < 5; ++$y) { for ($x = 0; $x < 5; ++$x) { $this->assertSame($y >= 1 && $y <= 3 && $x >= 1 && $x <= 3, $matrix->get($x, $y)); } } } public function testRectangularMatrix() : void { $matrix = new BitMatrix(75, 20); $this->assertSame(75, $matrix->getWidth()); $this->assertSame(20, $matrix->getHeight()); $matrix->set(10, 0); $matrix->set(11, 1); $matrix->set(50, 2); $matrix->set(51, 3); $matrix->flip(74, 4); $matrix->flip(0, 5); $this->assertTrue($matrix->get(10, 0)); $this->assertTrue($matrix->get(11, 1)); $this->assertTrue($matrix->get(50, 2)); $this->assertTrue($matrix->get(51, 3)); $this->assertTrue($matrix->get(74, 4)); $this->assertTrue($matrix->get(0, 5)); $matrix->flip(50, 2); $matrix->flip(51, 3); $this->assertFalse($matrix->get(50, 2)); $this->assertFalse($matrix->get(51, 3)); } public function testRectangularSetRegion() : void { $matrix = new BitMatrix(320, 240); $this->assertSame(320, $matrix->getWidth()); $this->assertSame(240, $matrix->getHeight()); $matrix->setRegion(105, 22, 80, 12); for ($y = 0; $y < 240; ++$y) { for ($x = 0; $x < 320; ++$x) { $this->assertEquals($y >= 22 && $y < 34 && $x >= 105 && $x < 185, $matrix->get($x, $y)); } } } public function testGetRow() : void { $matrix = new BitMatrix(102, 5); for ($x = 0; $x < 102; ++$x) { if (0 === ($x & 3)) { $matrix->set($x, 2); } } $array1 = $matrix->getRow(2, null); $this->assertSame(102, $array1->getSize()); $array2 = new BitArray(60); $array2 = $matrix->getRow(2, $array2); $this->assertSame(102, $array2->getSize()); $array3 = new BitArray(200); $array3 = $matrix->getRow(2, $array3); $this->assertSame(200, $array3->getSize()); for ($x = 0; $x < 102; ++$x) { $on = (0 === ($x & 3)); $this->assertSame($on, $array1->get($x)); $this->assertSame($on, $array2->get($x)); $this->assertSame($on, $array3->get($x)); } } } test/Common/VersionTest.php000064400000004415150250215260011753 0ustar00assertNotNull($version); $this->assertEquals($versionNumber, $version->getVersionNumber()); $this->assertNotNull($version->getAlignmentPatternCenters()); if ($versionNumber > 1) { $this->assertTrue(count($version->getAlignmentPatternCenters()) > 0); } $this->assertEquals($dimension, $version->getDimensionForVersion()); $this->assertNotNull($version->getEcBlocksForLevel(ErrorCorrectionLevel::H())); $this->assertNotNull($version->getEcBlocksForLevel(ErrorCorrectionLevel::L())); $this->assertNotNull($version->getEcBlocksForLevel(ErrorCorrectionLevel::M())); $this->assertNotNull($version->getEcBlocksForLevel(ErrorCorrectionLevel::Q())); $this->assertNotNull($version->buildFunctionPattern()); } /** * @dataProvider versions */ public function testGetProvisionalVersionForDimension(int $versionNumber, int $dimension) : void { $this->assertSame( $versionNumber, Version::getProvisionalVersionForDimension($dimension)->getVersionNumber() ); } /** * @dataProvider decodeInformation */ public function testDecodeVersionInformation(int $expectedVersion, int $mask) : void { $version = Version::decodeVersionInformation($mask); $this->assertNotNull($version); $this->assertSame($expectedVersion, $version->getVersionNumber()); } } test/Common/ReedSolomonCodecTest.php000064400000006051150250215260013510 0ustar00encode($block, $parity); // Copy parity into test blocks for ($i = 0; $i < $numRoots; ++$i) { $block[$i + $dataSize] = $parity[$i]; $tBlock[$i + $dataSize] = $parity[$i]; } // Seed with errors for ($i = 0; $i < $errors; ++$i) { $errorValue = mt_rand(1, $blockSize); do { $errorLocation = mt_rand(0, $blockSize); } while (0 !== $errorLocations[$errorLocation]); $errorLocations[$errorLocation] = 1; if (mt_rand(0, 1)) { $erasures[] = $errorLocation; } $tBlock[$errorLocation] ^= $errorValue; } $erasures = SplFixedArray::fromArray($erasures, false); // Decode the errored block $foundErrors = $codec->decode($tBlock, $erasures); if ($errors > 0 && null === $foundErrors) { $this->assertSame($block, $tBlock, 'Decoder failed to correct errors'); } $this->assertSame($errors, $foundErrors, 'Found errors do not equal expected errors'); for ($i = 0; $i < $foundErrors; ++$i) { if (0 === $errorLocations[$erasures[$i]]) { $this->fail(sprintf('Decoder indicates error in location %d without error', $erasures[$i])); } } $this->assertEquals($block, $tBlock, 'Decoder did not correct errors'); } } } test/Integration/__snapshots__/files/ImagickRenderingTest__testGenericQrCode__1.png000064400000006047150250215260024653 0ustar00PNG  IHDRDSgAMA a cHRMz&u0`:pQ<bKGD1 IDATx{lVri,FP}2TPE즢 ܄udEE:k-8"60(e:tцB3}ONߝ>ϟ|;ӷ'99@@ @@@ @@@ @@ @@ @@ ȉA:$'{JOV-z86W][5]K.fғ緍l Y3+ߗMɧ%=م|ٺ%<9'4 \O,p@@ @@ 2(;2첬ɾe;lPt ?ݏga/[$8Ww_-n5/)L~ ܟX@@ ̠|恑ojw+ĸmBVU+ 8]ovЬ,\/?2$d|@@ rϳXzRH?p1_h|)^;5yHqcIϴ5n{= @@ Ū'ڢ΂@^UuV_2YH7fs@@@ v>}g۫ow跁Te/?@ @RzԤ%'3n >i}@-,ϧmG-/=0u[{Im'ֶ?Y-8C@@ 8 ]1{9'm ;!a-9چ%\ܧ/tTO+~K nN.;A@ @ S*֪OϫĐW{KO{ck6.jNlxD$I >Ъp5*y<05-q]䍭q\ݼɼz}SF,,w_vWgMU`r}s9s {=J܃@@  @ng_lBo7$YQCGߔl^4:z[ }P ?~t/L ژw{=Gܞ?pZhɯ4I&mwQ++c=Fv Xכּ@@ @@ \b~DTiq+pt? dյų_ӿW6Ϲ}YI`~:-?{Zzr?vt7#m i  I< VW7  @TQaogZ* JnRx6=j i3zw67,e;t?Zӭ{v@47t4OHꦨU{ޞ|%ܤ@@ @ 4&?w/ <:+iwO nնiW}?YzrȈ=Ihrjux}kf3/^$Nm<'v y 6uwFmqqł+|Qc0݃t@@ 3Jպ;FkKO>s~Uϋӳw X*Xx͉{ɡɱ_9lw>'OdcnlM'3#쯩WM8O[hfgּ_=9=@@ @ b?{zO C<=ޕgSO $m4^Edf[{hV7/fC?M?MU-9249w۬Zۡ>H_6 -|&np1֍{oEψ{@@  d+߻ *QkܺwiHOIO>oqzJ @@ @Q;6`[$/Y}Ȉ9MHH9͚K2JKp5~Z}oY}\|@C^:Z+pȜ9Cymr睷)_W5Y~cmۖ5k^bʕA @@ @Q̱w۶\Z7beM?kr-[feWKMOi;'XET yMNNК@Wxm֜_U哪****V3fbNpǗHa|c:dӦrǍW(g1zj+IJ~!h?EXK6o~k @@ @ע Łoʁo]ip~,?5!ɚ1@ @D/w7\iAc[o )C^{_ǡ&ۍ<--ˇǛ1A|{/~qMkϥ#PW-UU]׫ˏ 'N2~ssCTx(.%ILDĝ˱;| @  @@3~ˤW'W~n5Nk ۥ^31~ܹ`\,fIpڿL}}D=_ӻNaaԎ)-}& } xE,-./h^csb 8s{ VZ۵1Lv=RZxRdbl=+W0&{C d1n{3ciJ**Ź$'g}!Yc6##IRS볥#l=uNFvVMqws4`WV_xSW*  ?D19TUCk92^5/\֞y ?E.ټm-** x/K/ =Tӗ|Qk+V$˄ .90ybȬY匏hx),\8ާPE_/kkpd f+=NV:S=V83߁ @ [ ?4sKM(+6_[[{D@įK[[UvvvIolj] B~yt/31ő/?]{_Wk5//_ni j !;ªH>iqUWNLw*)]\]}<}זwu=P^S W:/jdtR#>_jt|'T5dɒmJ8G~|ϟ||]c4EFڽ#~-\8Mӕ=}J; @ @X܉uMnq\9U>6!!@Ν?#?^Q/X?Wjm??o@ ֈϛF>mذYkvxV< ΐ0*(5ĺZ>(r]RP0o^Wc0ZT1>ޒDziݺ'ݺ'LXZHq͜9Q֭X}c k2^֔ٳeݺt[Zmw@ @ @ (Ɲa6}iwXGÜ r3Fy?O\g@Y#>]7o.}xnkŴ'(חkǓoǰ~~ݵDӓG.ټm=9pO$jͩ߫ݯ5k>a=,G\'|ٹ5>H,[,[ǎ}ʴG?zZrsoc8ZndGvo޳1'͐U(oڵp"E @ @ZK{o^IƐ RVF?,vI54|)?٫S;/v{uuJKOntϜi^zXAmtL<]=t ٶ2ɓ"/HcZ{BjiB[*+2:t Qٳc:<''Ov*QvK,'@#^|sʒ%?T>~Z* qZ_AOOx'v[rGh0?ݲiSx p{=ڷm j篟}1["m+oî]?:>-IuSX#j$-ٸ1Cfp+-[VL0ё RBl??[ĉcO(Z𰥥q}; @ @;Oԁ%yy̆ 6ߗoa6Z)qX@y ^~kڰau'e8 1^_5R;RɓMӧeFNM?׎4}jUTS+/mm}b׵L=.ҥOְ ]Uڿt9窪ByMEnq>@R K[l44|)۶UjoI풘?H?WUhN-,]~] kқCy >.]z|-Z"jH57{i8;95pYwIffұ}ve⻔mot%@0;Fڵm{@Dy2%;;';/NT3Ms˴i @ @ \5hl/ٶ-==},L T2.H|1 &M៻'&&IQZ''OXݥ~x|L0c޷=+:5rۭ^r̙6)-J)*:ڂR_al{/(X*.㍍x>fo'78X))dժ 1CHzzRDaժ9 /eSS @  @@2~'mۣBH"Go8\.+l,-{)"W*G ˲$++SFtˇi{G'MGGuݒ4֯7m,ٺ.NXTV qѩIw#%x1f̘"۶s\m{XO<=HOY"̺u>]x#;wF@ @ \R&WDT(ܳH1}פx$DY1k,v\v"&O|T.ǫu :'$)oK cl$''C*+U>…nټmcq,LR4-77A*+3q'H:\> VO4Q,Y@ @b_ OErV22DVywH3~܏ ybq#/p"}d5~Zs0{Kx N0(R_>E__逍8z-[̙ӍK6m*RF[|,'"|]j]Oݰ,+-IIPv ~[Z:?2QL8Ɗ(\k @ @b܉1rAz]կVnӲ\56#6:^RSs'.tv 1GG) SkCo攔${A;Rڿ ӧ[=+w @ (J,?hÏcdzryٻu RRXdwSnR[JJ*mGK7p᏷4ڿ n=ofl~.v ӧ{mx_23ӢdN'Fx#9n| @  @@Nt r+?Ef 6>2Lg~|獵yN[OHYR5ھdt~_c?!delIX޹wB7 a״I?hCT^ڟʧ5ޢjmٹ\mx4)Ny>sgxWH8~&' ed\+99QrrcH*;w @ @D1މ>p(GYL^ﷵnү|l|%ٲav8[QNk8Qh^sWm[vF URr(VHwEG%blӹMR_!:fKjjci߾.RCqq,_{@0,+Ot k62bɝwNa"" ߁ @ @p-LLɖG0ө4}T).}G}v R\Lft> j}S?ɯ ۓ&%:ꜙ=;YNMC\BII'])Sl۶tOX@ @  @ @   k@ZIENDB`test/Integration/ImagickRenderingTest.php000064400000004031150250215260014555 0ustar00writeFile('Hello World!', $tempName); $this->assertMatchesFileSnapshot($tempName); unlink($tempName); } public function testIssue79() : void { $eye = SquareEye::instance(); $squareModule = SquareModule::instance(); $eyeFill = new EyeFill(new Rgb(100, 100, 55), new Rgb(100, 100, 255)); $gradient = new Gradient(new Rgb(100, 100, 55), new Rgb(100, 100, 255), GradientType::HORIZONTAL()); $renderer = new ImageRenderer( new RendererStyle( 400, 2, $squareModule, $eye, Fill::withForegroundGradient(new Rgb(255, 255, 255), $gradient, $eyeFill, $eyeFill, $eyeFill) ), new ImagickImageBackEnd() ); $writer = new Writer($renderer); $tempName = tempnam(sys_get_temp_dir(), 'test') . '.png'; $writer->writeFile('https://apiroad.net/very-long-url', $tempName); $this->assertMatchesFileSnapshot($tempName); unlink($tempName); } } test/Encoder/EncoderTest.php000064400000045541150250215260012041 0ustar00getMethods(ReflectionMethod::IS_STATIC) as $method) { $method->setAccessible(true); $this->methods[$method->getName()] = $method; } } public function testGetAlphanumericCode() : void { // The first ten code points are numbers. for ($i = 0; $i < 10; ++$i) { $this->assertSame($i, $this->methods['getAlphanumericCode']->invoke(null, ord('0') + $i)); } // The next 26 code points are capital alphabet letters. for ($i = 10; $i < 36; ++$i) { // The first ten code points are numbers $this->assertSame($i, $this->methods['getAlphanumericCode']->invoke(null, ord('A') + $i - 10)); } // Others are symbol letters. $this->assertSame(36, $this->methods['getAlphanumericCode']->invoke(null, ord(' '))); $this->assertSame(37, $this->methods['getAlphanumericCode']->invoke(null, ord('$'))); $this->assertSame(38, $this->methods['getAlphanumericCode']->invoke(null, ord('%'))); $this->assertSame(39, $this->methods['getAlphanumericCode']->invoke(null, ord('*'))); $this->assertSame(40, $this->methods['getAlphanumericCode']->invoke(null, ord('+'))); $this->assertSame(41, $this->methods['getAlphanumericCode']->invoke(null, ord('-'))); $this->assertSame(42, $this->methods['getAlphanumericCode']->invoke(null, ord('.'))); $this->assertSame(43, $this->methods['getAlphanumericCode']->invoke(null, ord('/'))); $this->assertSame(44, $this->methods['getAlphanumericCode']->invoke(null, ord(':'))); // Should return -1 for other letters. $this->assertSame(-1, $this->methods['getAlphanumericCode']->invoke(null, ord('a'))); $this->assertSame(-1, $this->methods['getAlphanumericCode']->invoke(null, ord('#'))); $this->assertSame(-1, $this->methods['getAlphanumericCode']->invoke(null, ord("\0"))); } public function testChooseMode() : void { // Numeric mode $this->assertSame(Mode::NUMERIC(), $this->methods['chooseMode']->invoke(null, '0')); $this->assertSame(Mode::NUMERIC(), $this->methods['chooseMode']->invoke(null, '0123456789')); // Alphanumeric mode $this->assertSame(Mode::ALPHANUMERIC(), $this->methods['chooseMode']->invoke(null, 'A')); $this->assertSame( Mode::ALPHANUMERIC(), $this->methods['chooseMode']->invoke(null, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:') ); // 8-bit byte mode $this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, 'a')); $this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, '#')); $this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, '')); // AIUE in Hiragana in SHIFT-JIS $this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, "\x8\xa\x8\xa\x8\xa\x8\xa6")); // Nihon in Kanji in SHIFT-JIS $this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, "\x9\xf\x9\x7b")); // Sou-Utso-Byou in Kanji in SHIFT-JIS $this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, "\xe\x4\x9\x5\x9\x61")); } public function testEncode() : void { $qrCode = Encoder::encode('ABCDEF', ErrorCorrectionLevel::H()); $expected = "<<\n" . " mode: ALPHANUMERIC\n" . " ecLevel: H\n" . " version: 1\n" . " maskPattern: 0\n" . " matrix:\n" . " 1 1 1 1 1 1 1 0 1 1 1 1 0 0 1 1 1 1 1 1 1\n" . " 1 0 0 0 0 0 1 0 0 1 1 1 0 0 1 0 0 0 0 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 0 1 1 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 1 1 1 0 1 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 1 1 0 0 1 0 1 1 1 0 1\n" . " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 1\n" . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" . " 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0\n" . " 0 0 1 0 1 1 1 0 1 1 0 0 1 1 0 0 0 1 0 0 1\n" . " 1 0 1 1 1 0 0 1 0 0 0 1 0 1 0 0 0 0 0 0 0\n" . " 0 0 1 1 0 0 1 0 1 0 0 0 1 0 1 0 1 0 1 1 0\n" . " 1 1 0 1 0 1 0 1 1 1 0 1 0 1 0 0 0 0 0 1 0\n" . " 0 0 1 1 0 1 1 1 1 0 0 0 1 0 1 0 1 1 1 1 0\n" . " 0 0 0 0 0 0 0 0 1 0 0 1 1 1 0 1 0 1 0 0 0\n" . " 1 1 1 1 1 1 1 0 0 0 1 0 1 0 1 1 0 0 0 0 1\n" . " 1 0 0 0 0 0 1 0 1 1 1 1 0 1 0 1 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 1 0 1 1 0 1 0 1 0 0 0 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 1 0 1 1 1 1 0 1 0 1 0\n" . " 1 0 1 1 1 0 1 0 1 0 0 0 1 0 1 0 1 1 1 0 1\n" . " 1 0 0 0 0 0 1 0 0 1 1 0 1 1 0 1 0 0 0 1 1\n" . " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 0 1 0 1\n" . ">>\n"; $this->assertSame($expected, (string) $qrCode); } public function testSimpleUtf8Eci() : void { $qrCode = Encoder::encode('hello', ErrorCorrectionLevel::H(), 'utf-8'); $expected = "<<\n" . " mode: BYTE\n" . " ecLevel: H\n" . " version: 1\n" . " maskPattern: 3\n" . " matrix:\n" . " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1\n" . " 1 0 0 0 0 0 1 0 0 0 1 0 1 0 1 0 0 0 0 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 0 1 0 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 1 0 1 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 0 1\n" . " 1 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 1\n" . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" . " 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0\n" . " 0 0 1 1 0 0 1 1 1 1 0 0 0 1 1 0 1 0 0 0 0\n" . " 0 0 1 1 1 0 0 0 0 0 1 1 0 0 0 1 0 1 1 1 0\n" . " 0 1 0 1 0 1 1 1 0 1 0 1 0 0 0 0 0 1 1 1 1\n" . " 1 1 0 0 1 0 0 1 1 0 0 1 1 1 1 0 1 0 1 1 0\n" . " 0 0 0 0 1 0 1 1 1 1 0 0 0 0 0 1 0 0 1 0 0\n" . " 0 0 0 0 0 0 0 0 1 1 1 1 0 0 1 1 1 0 0 0 1\n" . " 1 1 1 1 1 1 1 0 1 1 1 0 1 0 1 1 0 0 1 0 0\n" . " 1 0 0 0 0 0 1 0 0 0 1 0 0 1 1 1 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 0 0 0 0 1 1 0 0 0 0 0\n" . " 1 0 1 1 1 0 1 0 1 1 1 0 1 0 0 0 1 1 0 0 0\n" . " 1 0 1 1 1 0 1 0 1 1 0 0 0 1 0 0 1 0 0 0 0\n" . " 1 0 0 0 0 0 1 0 0 0 0 1 1 0 1 0 1 0 1 1 0\n" . " 1 1 1 1 1 1 1 0 0 1 0 1 1 1 0 1 1 0 0 0 0\n" . ">>\n"; $this->assertSame($expected, (string) $qrCode); } public function testAppendModeInfo() : void { $bits = new BitArray(); $this->methods['appendModeInfo']->invoke(null, Mode::NUMERIC(), $bits); $this->assertSame(' ...X', (string) $bits); } public function testAppendLengthInfo() : void { // 1 letter (1/1), 10 bits. $bits = new BitArray(); $this->methods['appendLengthInfo']->invoke( null, 1, Version::getVersionForNumber(1), Mode::NUMERIC(), $bits ); $this->assertSame(' ........ .X', (string) $bits); // 2 letters (2/1), 11 bits. $bits = new BitArray(); $this->methods['appendLengthInfo']->invoke( null, 2, Version::getVersionForNumber(10), Mode::ALPHANUMERIC(), $bits ); $this->assertSame(' ........ .X.', (string) $bits); // 255 letters (255/1), 16 bits. $bits = new BitArray(); $this->methods['appendLengthInfo']->invoke( null, 255, Version::getVersionForNumber(27), Mode::BYTE(), $bits ); $this->assertSame(' ........ XXXXXXXX', (string) $bits); // 512 letters (1024/2), 12 bits. $bits = new BitArray(); $this->methods['appendLengthInfo']->invoke( null, 512, Version::getVersionForNumber(40), Mode::KANJI(), $bits ); $this->assertSame(' ..X..... ....', (string) $bits); } public function testAppendBytes() : void { // Should use appendNumericBytes. // 1 = 01 = 0001 in 4 bits. $bits = new BitArray(); $this->methods['appendBytes']->invoke( null, '1', Mode::NUMERIC(), $bits, Encoder::DEFAULT_BYTE_MODE_ECODING ); $this->assertSame(' ...X', (string) $bits); // Should use appendAlphaNumericBytes. // A = 10 = 0xa = 001010 in 6 bits. $bits = new BitArray(); $this->methods['appendBytes']->invoke( null, 'A', Mode::ALPHANUMERIC(), $bits, Encoder::DEFAULT_BYTE_MODE_ECODING ); $this->assertSame(' ..X.X.', (string) $bits); // Should use append8BitBytes. // 0x61, 0x62, 0x63 $bits = new BitArray(); $this->methods['appendBytes']->invoke( null, 'abc', Mode::BYTE(), $bits, Encoder::DEFAULT_BYTE_MODE_ECODING ); $this->assertSame(' .XX....X .XX...X. .XX...XX', (string) $bits); // Should use appendKanjiBytes. // 0x93, 0x5f $bits = new BitArray(); $this->methods['appendBytes']->invoke( null, "\x93\x5f", Mode::KANJI(), $bits, Encoder::DEFAULT_BYTE_MODE_ECODING ); $this->assertSame(' .XX.XX.. XXXXX', (string) $bits); // Lower letters such as 'a' cannot be encoded in alphanumeric mode. $this->expectException(WriterException::class); $this->methods['appendBytes']->invoke( null, 'a', Mode::ALPHANUMERIC(), $bits, Encoder::DEFAULT_BYTE_MODE_ECODING ); } public function testTerminateBits() : void { $bits = new BitArray(); $this->methods['terminateBits']->invoke(null, 0, $bits); $this->assertSame('', (string) $bits); $bits = new BitArray(); $this->methods['terminateBits']->invoke(null, 1, $bits); $this->assertSame(' ........', (string) $bits); $bits = new BitArray(); $bits->appendBits(0, 3); $this->methods['terminateBits']->invoke(null, 1, $bits); $this->assertSame(' ........', (string) $bits); $bits = new BitArray(); $bits->appendBits(0, 5); $this->methods['terminateBits']->invoke(null, 1, $bits); $this->assertSame(' ........', (string) $bits); $bits = new BitArray(); $bits->appendBits(0, 8); $this->methods['terminateBits']->invoke(null, 1, $bits); $this->assertSame(' ........', (string) $bits); $bits = new BitArray(); $this->methods['terminateBits']->invoke(null, 2, $bits); $this->assertSame(' ........ XXX.XX..', (string) $bits); $bits = new BitArray(); $bits->appendBits(0, 1); $this->methods['terminateBits']->invoke(null, 3, $bits); $this->assertSame(' ........ XXX.XX.. ...X...X', (string) $bits); } public function testGetNumDataBytesAndNumEcBytesForBlockId() : void { // Version 1-H. list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId'] ->invoke(null, 26, 9, 1, 0); $this->assertSame(9, $numDataBytes); $this->assertSame(17, $numEcBytes); // Version 3-H. 2 blocks. list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId'] ->invoke(null, 70, 26, 2, 0); $this->assertSame(13, $numDataBytes); $this->assertSame(22, $numEcBytes); list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId'] ->invoke(null, 70, 26, 2, 1); $this->assertSame(13, $numDataBytes); $this->assertSame(22, $numEcBytes); // Version 7-H. (4 + 1) blocks. list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId'] ->invoke(null, 196, 66, 5, 0); $this->assertSame(13, $numDataBytes); $this->assertSame(26, $numEcBytes); list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId'] ->invoke(null, 196, 66, 5, 4); $this->assertSame(14, $numDataBytes); $this->assertSame(26, $numEcBytes); // Version 40-H. (20 + 61) blocks. list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId'] ->invoke(null, 3706, 1276, 81, 0); $this->assertSame(15, $numDataBytes); $this->assertSame(30, $numEcBytes); list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId'] ->invoke(null, 3706, 1276, 81, 20); $this->assertSame(16, $numDataBytes); $this->assertSame(30, $numEcBytes); list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId'] ->invoke(null, 3706, 1276, 81, 80); $this->assertSame(16, $numDataBytes); $this->assertSame(30, $numEcBytes); } public function testInterleaveWithEcBytes() : void { $dataBytes = SplFixedArray::fromArray([32, 65, 205, 69, 41, 220, 46, 128, 236], false); $in = new BitArray(); foreach ($dataBytes as $dataByte) { $in->appendBits($dataByte, 8); } $outBits = $this->methods['interleaveWithEcBytes']->invoke(null, $in, 26, 9, 1); $expected = SplFixedArray::fromArray([ // Data bytes. 32, 65, 205, 69, 41, 220, 46, 128, 236, // Error correction bytes. 42, 159, 74, 221, 244, 169, 239, 150, 138, 70, 237, 85, 224, 96, 74, 219, 61, ], false); $out = $outBits->toBytes(0, count($expected)); $this->assertEquals($expected, $out); } public function testAppendNumericBytes() : void { // 1 = 01 = 0001 in 4 bits. $bits = new BitArray(); $this->methods['appendNumericBytes']->invoke(null, '1', $bits); $this->assertSame(' ...X', (string) $bits); // 12 = 0xc = 0001100 in 7 bits. $bits = new BitArray(); $this->methods['appendNumericBytes']->invoke(null, '12', $bits); $this->assertSame(' ...XX..', (string) $bits); // 123 = 0x7b = 0001111011 in 10 bits. $bits = new BitArray(); $this->methods['appendNumericBytes']->invoke(null, '123', $bits); $this->assertSame(' ...XXXX. XX', (string) $bits); // 1234 = "123" + "4" = 0001111011 + 0100 in 14 bits. $bits = new BitArray(); $this->methods['appendNumericBytes']->invoke(null, '1234', $bits); $this->assertSame(' ...XXXX. XX.X..', (string) $bits); // Empty $bits = new BitArray(); $this->methods['appendNumericBytes']->invoke(null, '', $bits); $this->assertSame('', (string) $bits); } public function testAppendAlphanumericBytes() : void { $bits = new BitArray(); $this->methods['appendAlphanumericBytes']->invoke(null, 'A', $bits); $this->assertSame(' ..X.X.', (string) $bits); $bits = new BitArray(); $this->methods['appendAlphanumericBytes']->invoke(null, 'AB', $bits); $this->assertSame(' ..XXX..X X.X', (string) $bits); $bits = new BitArray(); $this->methods['appendAlphanumericBytes']->invoke(null, 'ABC', $bits); $this->assertSame(' ..XXX..X X.X..XX. .', (string) $bits); // Empty $bits = new BitArray(); $this->methods['appendAlphanumericBytes']->invoke(null, '', $bits); $this->assertSame('', (string) $bits); // Invalid data $this->expectException(WriterException::class); $bits = new BitArray(); $this->methods['appendAlphanumericBytes']->invoke(null, 'abc', $bits); } public function testAppend8BitBytes() : void { // 0x61, 0x62, 0x63 $bits = new BitArray(); $this->methods['append8BitBytes']->invoke(null, 'abc', $bits, Encoder::DEFAULT_BYTE_MODE_ECODING); $this->assertSame(' .XX....X .XX...X. .XX...XX', (string) $bits); // Empty $bits = new BitArray(); $this->methods['append8BitBytes']->invoke(null, '', $bits, Encoder::DEFAULT_BYTE_MODE_ECODING); $this->assertSame('', (string) $bits); } public function testAppendKanjiBytes() : void { // Numbers are from page 21 of JISX0510:2004 $bits = new BitArray(); $this->methods['appendKanjiBytes']->invoke(null, "\x93\x5f", $bits); $this->assertSame(' .XX.XX.. XXXXX', (string) $bits); $this->methods['appendKanjiBytes']->invoke(null, "\xe4\xaa", $bits); $this->assertSame(' .XX.XX.. XXXXXXX. X.X.X.X. X.', (string) $bits); } public function testGenerateEcBytes() : void { // Numbers are from http://www.swetake.com/qr/qr3.html and // http://www.swetake.com/qr/qr9.html $dataBytes = SplFixedArray::fromArray([32, 65, 205, 69, 41, 220, 46, 128, 236], false); $ecBytes = $this->methods['generateEcBytes']->invoke(null, $dataBytes, 17); $expected = SplFixedArray::fromArray( [42, 159, 74, 221, 244, 169, 239, 150, 138, 70, 237, 85, 224, 96, 74, 219, 61], false ); $this->assertEquals($expected, $ecBytes); $dataBytes = SplFixedArray::fromArray( [67, 70, 22, 38, 54, 70, 86, 102, 118, 134, 150, 166, 182, 198, 214], false ); $ecBytes = $this->methods['generateEcBytes']->invoke(null, $dataBytes, 18); $expected = SplFixedArray::fromArray( [175, 80, 155, 64, 178, 45, 214, 233, 65, 209, 12, 155, 117, 31, 140, 214, 27, 187], false ); $this->assertEquals($expected, $ecBytes); // High-order zero coefficient case. $dataBytes = SplFixedArray::fromArray([32, 49, 205, 69, 42, 20, 0, 236, 17], false); $ecBytes = $this->methods['generateEcBytes']->invoke(null, $dataBytes, 17); $expected = SplFixedArray::fromArray( [0, 3, 130, 179, 194, 0, 55, 211, 110, 79, 98, 72, 170, 96, 211, 137, 213], false ); $this->assertEquals($expected, $ecBytes); } } test/Encoder/MatrixUtilTest.php000064400000036101150250215260012554 0ustar00getMethods(ReflectionMethod::IS_STATIC) as $method) { $method->setAccessible(true); $this->methods[$method->getName()] = $method; } } public function testToString() : void { $matrix = new ByteMatrix(3, 3); $matrix->set(0, 0, 0); $matrix->set(1, 0, 1); $matrix->set(2, 0, 0); $matrix->set(0, 1, 1); $matrix->set(1, 1, 0); $matrix->set(2, 1, 1); $matrix->set(0, 2, -1); $matrix->set(1, 2, -1); $matrix->set(2, 2, -1); $expected = " 0 1 0\n 1 0 1\n \n"; $this->assertSame($expected, (string) $matrix); } public function testClearMatrix() : void { $matrix = new ByteMatrix(2, 2); MatrixUtil::clearMatrix($matrix); $this->assertSame(-1, $matrix->get(0, 0)); $this->assertSame(-1, $matrix->get(1, 0)); $this->assertSame(-1, $matrix->get(0, 1)); $this->assertSame(-1, $matrix->get(1, 1)); } public function testEmbedBasicPatterns1() : void { $matrix = new ByteMatrix(21, 21); MatrixUtil::clearMatrix($matrix); $this->methods['embedBasicPatterns']->invoke( null, Version::getVersionForNumber(1), $matrix ); $expected = " 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1\n" . " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" . " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n" . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" . " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 1 \n" . " 0 \n" . " 1 \n" . " 0 \n" . " 1 \n" . " 0 0 0 0 0 0 0 0 1 \n" . " 1 1 1 1 1 1 1 0 \n" . " 1 0 0 0 0 0 1 0 \n" . " 1 0 1 1 1 0 1 0 \n" . " 1 0 1 1 1 0 1 0 \n" . " 1 0 1 1 1 0 1 0 \n" . " 1 0 0 0 0 0 1 0 \n" . " 1 1 1 1 1 1 1 0 \n"; $this->assertSame($expected, (string) $matrix); } public function testEmbedBasicPatterns2() : void { $matrix = new ByteMatrix(25, 25); MatrixUtil::clearMatrix($matrix); $this->methods['embedBasicPatterns']->invoke( null, Version::getVersionForNumber(2), $matrix ); $expected = " 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1\n" . " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n" . " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n" . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" . " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 1 \n" . " 0 \n" . " 1 \n" . " 0 \n" . " 1 \n" . " 0 \n" . " 1 \n" . " 0 \n" . " 1 1 1 1 1 1 \n" . " 0 0 0 0 0 0 0 0 1 1 0 0 0 1 \n" . " 1 1 1 1 1 1 1 0 1 0 1 0 1 \n" . " 1 0 0 0 0 0 1 0 1 0 0 0 1 \n" . " 1 0 1 1 1 0 1 0 1 1 1 1 1 \n" . " 1 0 1 1 1 0 1 0 \n" . " 1 0 1 1 1 0 1 0 \n" . " 1 0 0 0 0 0 1 0 \n" . " 1 1 1 1 1 1 1 0 \n"; $this->assertSame($expected, (string) $matrix); } public function testEmbedTypeInfo() : void { $matrix = new ByteMatrix(21, 21); MatrixUtil::clearMatrix($matrix); $this->methods['embedTypeInfo']->invoke( null, ErrorCorrectionLevel::M(), 5, $matrix ); $expected = " 0 \n" . " 1 \n" . " 1 \n" . " 1 \n" . " 0 \n" . " 0 \n" . " \n" . " 1 \n" . " 1 0 0 0 0 0 0 1 1 1 0 0 1 1 1 0\n" . " \n" . " \n" . " \n" . " \n" . " \n" . " 0 \n" . " 0 \n" . " 0 \n" . " 0 \n" . " 0 \n" . " 0 \n" . " 1 \n"; $this->assertSame($expected, (string) $matrix); } public function testEmbedVersionInfo() : void { $matrix = new ByteMatrix(21, 21); MatrixUtil::clearMatrix($matrix); $this->methods['maybeEmbedVersionInfo']->invoke( null, Version::getVersionForNumber(7), $matrix ); $expected = " 0 0 1 \n" . " 0 1 0 \n" . " 0 1 0 \n" . " 0 1 1 \n" . " 1 1 1 \n" . " 0 0 0 \n" . " \n" . " \n" . " \n" . " \n" . " 0 0 0 0 1 0 \n" . " 0 1 1 1 1 0 \n" . " 1 0 0 1 1 0 \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n"; $this->assertSame($expected, (string) $matrix); } public function testEmbedDataBits() : void { $matrix = new ByteMatrix(21, 21); MatrixUtil::clearMatrix($matrix); $this->methods['embedBasicPatterns']->invoke( null, Version::getVersionForNumber(1), $matrix ); $bits = new BitArray(); $this->methods['embedDataBits']->invoke( null, $bits, -1, $matrix ); $expected = " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1\n" . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1\n" . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 1 1 1 0 1\n" . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1\n" . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" . " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" . " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"; $this->assertSame($expected, (string) $matrix); } public function testBuildMatrix() : void { $bytes = [ 32, 65, 205, 69, 41, 220, 46, 128, 236, 42, 159, 74, 221, 244, 169, 239, 150, 138, 70, 237, 85, 224, 96, 74, 219 , 61 ]; $bits = new BitArray(); foreach ($bytes as $byte) { $bits->appendBits($byte, 8); } $matrix = new ByteMatrix(21, 21); MatrixUtil::buildMatrix( $bits, ErrorCorrectionLevel::H(), Version::getVersionForNumber(1), 3, $matrix ); $expected = " 1 1 1 1 1 1 1 0 0 1 1 0 0 0 1 1 1 1 1 1 1\n" . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1\n" . " 1 0 1 1 1 0 1 0 0 0 0 1 0 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 0 1 1 0 0 0 1 0 1 1 1 0 1\n" . " 1 0 1 1 1 0 1 0 1 1 0 0 1 0 1 0 1 1 1 0 1\n" . " 1 0 0 0 0 0 1 0 0 0 1 1 1 0 1 0 0 0 0 0 1\n" . " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n" . " 0 0 0 0 0 0 0 0 1 1 0 1 1 0 0 0 0 0 0 0 0\n" . " 0 0 1 1 0 0 1 1 1 0 0 1 1 1 1 0 1 0 0 0 0\n" . " 1 0 1 0 1 0 0 0 0 0 1 1 1 0 0 1 0 1 1 1 0\n" . " 1 1 1 1 0 1 1 0 1 0 1 1 1 0 0 1 1 1 0 1 0\n" . " 1 0 1 0 1 1 0 1 1 1 0 0 1 1 1 0 0 1 0 1 0\n" . " 0 0 1 0 0 1 1 1 0 0 0 0 0 0 1 0 1 1 1 1 1\n" . " 0 0 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 1 0 1 1\n" . " 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 0 1 0 1 1 0\n" . " 1 0 0 0 0 0 1 0 0 0 0 1 0 1 1 1 0 0 0 0 0\n" . " 1 0 1 1 1 0 1 0 0 1 0 0 1 1 0 0 1 0 0 1 1\n" . " 1 0 1 1 1 0 1 0 1 1 0 1 0 0 0 0 0 1 1 1 0\n" . " 1 0 1 1 1 0 1 0 1 1 1 1 0 0 0 0 1 1 1 0 0\n" . " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 1 0 0\n" . " 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 0 1 0 0 1 0\n"; $this->assertSame($expected, (string) $matrix); } public function testFindMsbSet() : void { $this->assertSame(0, $this->methods['findMsbSet']->invoke(null, 0)); $this->assertSame(1, $this->methods['findMsbSet']->invoke(null, 1)); $this->assertSame(8, $this->methods['findMsbSet']->invoke(null, 0x80)); $this->assertSame(32, $this->methods['findMsbSet']->invoke(null, 0x80000000)); } public function testCalculateBchCode() : void { // Encoding of type information. // From Appendix C in JISX0510:2004 (p 65) $this->assertSame(0xdc, $this->methods['calculateBchCode']->invoke(null, 5, 0x537)); // From http://www.swetake.com/qr/qr6.html $this->assertSame(0x1c2, $this->methods['calculateBchCode']->invoke(null, 0x13, 0x537)); // From http://www.swetake.com/qr/qr11.html $this->assertSame(0x214, $this->methods['calculateBchCode']->invoke(null, 0x1b, 0x537)); // Encoding of version information. // From Appendix D in JISX0510:2004 (p 68) $this->assertSame(0xc94, $this->methods['calculateBchCode']->invoke(null, 7, 0x1f25)); $this->assertSame(0x5bc, $this->methods['calculateBchCode']->invoke(null, 8, 0x1f25)); $this->assertSame(0xa99, $this->methods['calculateBchCode']->invoke(null, 9, 0x1f25)); $this->assertSame(0x4d3, $this->methods['calculateBchCode']->invoke(null, 10, 0x1f25)); $this->assertSame(0x9a6, $this->methods['calculateBchCode']->invoke(null, 20, 0x1f25)); $this->assertSame(0xd75, $this->methods['calculateBchCode']->invoke(null, 30, 0x1f25)); $this->assertSame(0xc69, $this->methods['calculateBchCode']->invoke(null, 40, 0x1f25)); } public function testMakeVersionInfoBits() : void { // From Appendix D in JISX0510:2004 (p 68) $bits = new BitArray(); $this->methods['makeVersionInfoBits']->invoke(null, Version::getVersionForNumber(7), $bits); $this->assertSame(' ...XXXXX ..X..X.X ..', (string) $bits); } public function testMakeTypeInfoBits() : void { // From Appendix D in JISX0510:2004 (p 68) $bits = new BitArray(); $this->methods['makeTypeInfoBits']->invoke(null, ErrorCorrectionLevel::M(), 5, $bits); $this->assertSame(' X......X X..XXX.', (string) $bits); } } test/Encoder/MaskUtilTest.php000064400000017106150250215260012207 0ustar00assertSame( 1 === $expected[$y][$x], MaskUtil::getDataMaskBit($maskPattern, $x, $y) ); } } } public function testApplyMaskPenaltyRule1() : void { $matrix = new ByteMatrix(4, 1); $matrix->set(0, 0, 0); $matrix->set(1, 0, 0); $matrix->set(2, 0, 0); $matrix->set(3, 0, 0); $this->assertSame(0, MaskUtil::applyMaskPenaltyRule1($matrix)); // Horizontal $matrix = new ByteMatrix(6, 1); $matrix->set(0, 0, 0); $matrix->set(1, 0, 0); $matrix->set(2, 0, 0); $matrix->set(3, 0, 0); $matrix->set(4, 0, 0); $matrix->set(5, 0, 1); $this->assertSame(3, MaskUtil::applyMaskPenaltyRule1($matrix)); $matrix->set(5, 0, 0); $this->assertSame(4, MaskUtil::applyMaskPenaltyRule1($matrix)); // Vertical $matrix = new ByteMatrix(1, 6); $matrix->set(0, 0, 0); $matrix->set(0, 1, 0); $matrix->set(0, 2, 0); $matrix->set(0, 3, 0); $matrix->set(0, 4, 0); $matrix->set(0, 5, 1); $this->assertSame(3, MaskUtil::applyMaskPenaltyRule1($matrix)); $matrix->set(0, 5, 0); $this->assertSame(4, MaskUtil::applyMaskPenaltyRule1($matrix)); } public function testApplyMaskPenaltyRule2() : void { $matrix = new ByteMatrix(1, 1); $matrix->set(0, 0, 0); $this->assertSame(0, MaskUtil::applyMaskPenaltyRule2($matrix)); $matrix = new ByteMatrix(2, 2); $matrix->set(0, 0, 0); $matrix->set(1, 0, 0); $matrix->set(0, 1, 0); $matrix->set(1, 1, 1); $this->assertSame(0, MaskUtil::applyMaskPenaltyRule2($matrix)); $matrix = new ByteMatrix(2, 2); $matrix->set(0, 0, 0); $matrix->set(1, 0, 0); $matrix->set(0, 1, 0); $matrix->set(1, 1, 0); $this->assertSame(3, MaskUtil::applyMaskPenaltyRule2($matrix)); $matrix = new ByteMatrix(3, 3); $matrix->set(0, 0, 0); $matrix->set(1, 0, 0); $matrix->set(2, 0, 0); $matrix->set(0, 1, 0); $matrix->set(1, 1, 0); $matrix->set(2, 1, 0); $matrix->set(0, 2, 0); $matrix->set(1, 2, 0); $matrix->set(2, 2, 0); $this->assertSame(3 * 4, MaskUtil::applyMaskPenaltyRule2($matrix)); } public function testApplyMaskPenalty3() : void { // Horizontal 00001011101 $matrix = new ByteMatrix(11, 1); $matrix->set(0, 0, 0); $matrix->set(1, 0, 0); $matrix->set(2, 0, 0); $matrix->set(3, 0, 0); $matrix->set(4, 0, 1); $matrix->set(5, 0, 0); $matrix->set(6, 0, 1); $matrix->set(7, 0, 1); $matrix->set(8, 0, 1); $matrix->set(9, 0, 0); $matrix->set(10, 0, 1); $this->assertSame(40, MaskUtil::applyMaskPenaltyRule3($matrix)); // Horizontal 10111010000 $matrix = new ByteMatrix(11, 1); $matrix->set(0, 0, 1); $matrix->set(1, 0, 0); $matrix->set(2, 0, 1); $matrix->set(3, 0, 1); $matrix->set(4, 0, 1); $matrix->set(5, 0, 0); $matrix->set(6, 0, 1); $matrix->set(7, 0, 0); $matrix->set(8, 0, 0); $matrix->set(9, 0, 0); $matrix->set(10, 0, 0); $this->assertSame(40, MaskUtil::applyMaskPenaltyRule3($matrix)); // Vertical 00001011101 $matrix = new ByteMatrix(1, 11); $matrix->set(0, 0, 0); $matrix->set(0, 1, 0); $matrix->set(0, 2, 0); $matrix->set(0, 3, 0); $matrix->set(0, 4, 1); $matrix->set(0, 5, 0); $matrix->set(0, 6, 1); $matrix->set(0, 7, 1); $matrix->set(0, 8, 1); $matrix->set(0, 9, 0); $matrix->set(0, 10, 1); $this->assertSame(40, MaskUtil::applyMaskPenaltyRule3($matrix)); // Vertical 10111010000 $matrix = new ByteMatrix(1, 11); $matrix->set(0, 0, 1); $matrix->set(0, 1, 0); $matrix->set(0, 2, 1); $matrix->set(0, 3, 1); $matrix->set(0, 4, 1); $matrix->set(0, 5, 0); $matrix->set(0, 6, 1); $matrix->set(0, 7, 0); $matrix->set(0, 8, 0); $matrix->set(0, 9, 0); $matrix->set(0, 10, 0); $this->assertSame(40, MaskUtil::applyMaskPenaltyRule3($matrix)); } public function testApplyMaskPenaltyRule4() : void { // Dark cell ratio = 0% $matrix = new ByteMatrix(1, 1); $matrix->set(0, 0, 0); $this->assertSame(100, MaskUtil::applyMaskPenaltyRule4($matrix)); // Dark cell ratio = 5% $matrix = new ByteMatrix(2, 1); $matrix->set(0, 0, 0); $matrix->set(0, 0, 1); $this->assertSame(0, MaskUtil::applyMaskPenaltyRule4($matrix)); // Dark cell ratio = 66.67% $matrix = new ByteMatrix(6, 1); $matrix->set(0, 0, 0); $matrix->set(1, 0, 1); $matrix->set(2, 0, 1); $matrix->set(3, 0, 1); $matrix->set(4, 0, 1); $matrix->set(5, 0, 0); $this->assertSame(30, MaskUtil::applyMaskPenaltyRule4($matrix)); } }