* @copyright 2015 Smiley * @license MIT */ namespace chillerlan\QRCode; use chillerlan\QRCode\Data\{ AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRCodeDataException, QRDataInterface, QRMatrix }; use chillerlan\QRCode\Output\{ QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString }; use chillerlan\Settings\SettingsContainerInterface; use function call_user_func_array, class_exists, in_array, ord, strlen, strtolower, str_split; /** * Turns a text string into a Model 2 QR Code * * @see https://github.com/kazuhikoarase/qrcode-generator/tree/master/php * @see http://www.qrcode.com/en/codes/model12.html * @see https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf * @see https://en.wikipedia.org/wiki/QR_code * @see http://www.thonky.com/qr-code-tutorial/ */ class QRCode{ /** @var int */ public const VERSION_AUTO = -1; /** @var int */ public const MASK_PATTERN_AUTO = -1; // ISO/IEC 18004:2000 Table 2 /** @var int */ public const DATA_NUMBER = 0b0001; /** @var int */ public const DATA_ALPHANUM = 0b0010; /** @var int */ public const DATA_BYTE = 0b0100; /** @var int */ public const DATA_KANJI = 0b1000; /** * References to the keys of the following tables: * * @see \chillerlan\QRCode\Data\QRDataInterface::MAX_LENGTH * * @var int[] */ public const DATA_MODES = [ self::DATA_NUMBER => 0, self::DATA_ALPHANUM => 1, self::DATA_BYTE => 2, self::DATA_KANJI => 3, ]; // ISO/IEC 18004:2000 Tables 12, 25 /** @var int */ public const ECC_L = 0b01; // 7%. /** @var int */ public const ECC_M = 0b00; // 15%. /** @var int */ public const ECC_Q = 0b11; // 25%. /** @var int */ public const ECC_H = 0b10; // 30%. /** * References to the keys of the following tables: * * @see \chillerlan\QRCode\Data\QRDataInterface::MAX_BITS * @see \chillerlan\QRCode\Data\QRDataInterface::RSBLOCKS * @see \chillerlan\QRCode\Data\QRMatrix::formatPattern * * @var int[] */ public const ECC_MODES = [ self::ECC_L => 0, self::ECC_M => 1, self::ECC_Q => 2, self::ECC_H => 3, ]; /** @var string */ public const OUTPUT_MARKUP_HTML = 'html'; /** @var string */ public const OUTPUT_MARKUP_SVG = 'svg'; /** @var string */ public const OUTPUT_IMAGE_PNG = 'png'; /** @var string */ public const OUTPUT_IMAGE_JPG = 'jpg'; /** @var string */ public const OUTPUT_IMAGE_GIF = 'gif'; /** @var string */ public const OUTPUT_STRING_JSON = 'json'; /** @var string */ public const OUTPUT_STRING_TEXT = 'text'; /** @var string */ public const OUTPUT_IMAGICK = 'imagick'; /** @var string */ public const OUTPUT_FPDF = 'fpdf'; /** @var string */ public const OUTPUT_CUSTOM = 'custom'; /** * Map of built-in output modules => capabilities * * @var string[][] */ public const OUTPUT_MODES = [ QRMarkup::class => [ self::OUTPUT_MARKUP_SVG, self::OUTPUT_MARKUP_HTML, ], QRImage::class => [ self::OUTPUT_IMAGE_PNG, self::OUTPUT_IMAGE_GIF, self::OUTPUT_IMAGE_JPG, ], QRString::class => [ self::OUTPUT_STRING_JSON, self::OUTPUT_STRING_TEXT, ], QRImagick::class => [ self::OUTPUT_IMAGICK, ], QRFpdf::class => [ self::OUTPUT_FPDF ] ]; /** * Map of data mode => interface * * @var string[] */ protected const DATA_INTERFACES = [ 'number' => Number::class, 'alphanum' => AlphaNum::class, 'kanji' => Kanji::class, 'byte' => Byte::class, ]; /** * The settings container * * @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface */ protected SettingsContainerInterface $options; /** * The selected data interface (Number, AlphaNum, Kanji, Byte) */ protected QRDataInterface $dataInterface; /** * QRCode constructor. * * Sets the options instance, determines the current mb-encoding and sets it to UTF-8 */ public function __construct(SettingsContainerInterface $options = null){ $this->options = $options ?? new QROptions; } /** * Renders a QR Code for the given $data and QROptions * * @return mixed */ public function render(string $data, string $file = null){ return $this->initOutputInterface($data)->dump($file); } /** * Returns a QRMatrix object for the given $data and current QROptions * * @throws \chillerlan\QRCode\Data\QRCodeDataException */ public function getMatrix(string $data):QRMatrix{ if(empty($data)){ throw new QRCodeDataException('QRCode::getMatrix() No data given.'); } $this->dataInterface = $this->initDataInterface($data); $maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO ? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern() : $this->options->maskPattern; $matrix = $this->dataInterface->initMatrix($maskPattern); if((bool)$this->options->addQuietzone){ $matrix->setQuietZone($this->options->quietzoneSize); } return $matrix; } /** * returns a fresh QRDataInterface for the given $data * * @throws \chillerlan\QRCode\Data\QRCodeDataException */ public function initDataInterface(string $data):QRDataInterface{ // allow forcing the data mode // see https://github.com/chillerlan/php-qrcode/issues/39 $interface = $this::DATA_INTERFACES[strtolower($this->options->dataModeOverride)] ?? null; if($interface !== null){ return new $interface($this->options, $data); } foreach($this::DATA_INTERFACES as $mode => $dataInterface){ if(call_user_func_array([$this, 'is'.$mode], [$data])){ return new $dataInterface($this->options, $data); } } throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore } /** * returns a fresh (built-in) QROutputInterface * * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ protected function initOutputInterface(string $data):QROutputInterface{ if($this->options->outputType === $this::OUTPUT_CUSTOM && class_exists($this->options->outputInterface)){ /** @phan-suppress-next-line PhanTypeExpectedObjectOrClassName */ return new $this->options->outputInterface($this->options, $this->getMatrix($data)); } foreach($this::OUTPUT_MODES as $outputInterface => $modes){ if(in_array($this->options->outputType, $modes, true) && class_exists($outputInterface)){ return new $outputInterface($this->options, $this->getMatrix($data)); } } throw new QRCodeOutputException('invalid output type'); } /** * checks if a string qualifies as numeric */ public function isNumber(string $string):bool{ return $this->checkString($string, QRDataInterface::CHAR_MAP_NUMBER); } /** * checks if a string qualifies as alphanumeric */ public function isAlphaNum(string $string):bool{ return $this->checkString($string, QRDataInterface::CHAR_MAP_ALPHANUM); } /** * checks is a given $string matches the characters of a given $charmap, returns false on the first invalid occurence. */ protected function checkString(string $string, array $charmap):bool{ foreach(str_split($string) as $chr){ if(!isset($charmap[$chr])){ return false; } } return true; } /** * checks if a string qualifies as Kanji */ public function isKanji(string $string):bool{ $i = 0; $len = strlen($string); while($i + 1 < $len){ $c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1])); if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){ return false; } $i += 2; } return $i >= $len; } /** * a dummy */ public function isByte(string $data):bool{ return $data !== ''; } }