QRCode.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. <?php
  2. /**
  3. * Class QRCode
  4. *
  5. * @created 26.11.2015
  6. * @author Smiley <smiley@chillerlan.net>
  7. * @copyright 2015 Smiley
  8. * @license MIT
  9. */
  10. declare(strict_types=1);
  11. namespace chillerlan\QRCode;
  12. use chillerlan\QRCode\Common\{
  13. ECICharset, GDLuminanceSource, IMagickLuminanceSource, LuminanceSourceInterface, MaskPattern, Mode
  14. };
  15. use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number, QRData, QRDataModeInterface, QRMatrix};
  16. use chillerlan\QRCode\Decoder\{Decoder, DecoderResult};
  17. use chillerlan\QRCode\Output\{QRCodeOutputException, QROutputInterface};
  18. use chillerlan\Settings\SettingsContainerInterface;
  19. use function class_exists, class_implements, in_array, is_iterable, mb_convert_encoding, mb_internal_encoding;
  20. /**
  21. * Turns a text string into a Model 2 QR Code
  22. *
  23. * @see https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
  24. * @see https://www.qrcode.com/en/codes/model12.html
  25. * @see https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf
  26. * @see https://en.wikipedia.org/wiki/QR_code
  27. * @see https://www.thonky.com/qr-code-tutorial/
  28. */
  29. class QRCode{
  30. /**
  31. * The settings container
  32. */
  33. protected SettingsContainerInterface|QROptions $options;
  34. /**
  35. * A collection of one or more data segments of QRDataModeInterface instances to write
  36. *
  37. * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
  38. */
  39. protected array $dataSegments = [];
  40. /**
  41. * The luminance source for the reader
  42. */
  43. protected string $luminanceSourceFQN = GDLuminanceSource::class;
  44. /**
  45. * QRCode constructor.
  46. *
  47. * @phpstan-param array<string, mixed> $options
  48. */
  49. public function __construct(SettingsContainerInterface|QROptions|iterable $options = new QROptions){
  50. $this->setOptions($options);
  51. }
  52. /**
  53. * Sets an options instance
  54. *
  55. * @phpstan-param array<string, mixed> $options
  56. */
  57. public function setOptions(SettingsContainerInterface|QROptions|iterable $options):static{
  58. if(is_iterable($options)){
  59. $options = new QROptions($options);
  60. }
  61. $this->options = $options;
  62. if($this->options->readerUseImagickIfAvailable){
  63. $this->luminanceSourceFQN = IMagickLuminanceSource::class;
  64. }
  65. return $this;
  66. }
  67. /**
  68. * Renders a QR Code for the given $data and QROptions, saves $file optionally
  69. */
  70. public function render(string|null $data = null, string|null $file = null):mixed{
  71. if($data !== null){
  72. /** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */
  73. foreach(Mode::INTERFACES as $dataInterface){
  74. if($dataInterface::validateString($data)){
  75. $this->addSegment(new $dataInterface($data));
  76. break;
  77. }
  78. }
  79. }
  80. return $this->renderMatrix($this->getQRMatrix(), $file);
  81. }
  82. /**
  83. * Renders a QR Code for the given QRMatrix and QROptions, saves $file optionally
  84. */
  85. public function renderMatrix(QRMatrix $matrix, string|null $file = null):mixed{
  86. return $this->initOutputInterface($matrix)->dump($file ?? $this->options->cachefile);
  87. }
  88. /**
  89. * Returns a QRMatrix object for the given $data and current QROptions
  90. */
  91. public function getQRMatrix():QRMatrix{
  92. $matrix = (new QRData($this->options, $this->dataSegments))->writeMatrix();
  93. $maskPattern = $this->options->maskPattern === MaskPattern::AUTO
  94. ? MaskPattern::getBestPattern($matrix)
  95. : new MaskPattern($this->options->maskPattern);
  96. $matrix->setFormatInfo($maskPattern)->mask($maskPattern);
  97. return $this->addMatrixModifications($matrix);
  98. }
  99. /**
  100. * add matrix modifications after mask pattern evaluation and before handing over to output
  101. */
  102. protected function addMatrixModifications(QRMatrix $matrix):QRMatrix{
  103. if($this->options->addLogoSpace){
  104. // check whether one of the dimensions was omitted
  105. $logoSpaceWidth = ($this->options->logoSpaceWidth ?? $this->options->logoSpaceHeight ?? 0);
  106. $logoSpaceHeight = ($this->options->logoSpaceHeight ?? $logoSpaceWidth);
  107. $matrix->setLogoSpace(
  108. $logoSpaceWidth,
  109. $logoSpaceHeight,
  110. $this->options->logoSpaceStartX,
  111. $this->options->logoSpaceStartY,
  112. );
  113. }
  114. if($this->options->addQuietzone){
  115. $matrix->setQuietZone($this->options->quietzoneSize);
  116. }
  117. return $matrix;
  118. }
  119. /**
  120. * initializes a fresh built-in or custom QROutputInterface
  121. *
  122. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  123. */
  124. protected function initOutputInterface(QRMatrix $matrix):QROutputInterface{
  125. $outputInterface = $this->options->outputInterface;
  126. if(!class_exists($outputInterface)){
  127. throw new QRCodeOutputException('invalid output class');
  128. }
  129. if(!in_array(QROutputInterface::class, class_implements($outputInterface), true)){
  130. throw new QRCodeOutputException('output class does not implement QROutputInterface');
  131. }
  132. /** @var \chillerlan\QRCode\Output\QROutputInterface $instance */
  133. $instance = new $outputInterface($this->options, $matrix);
  134. return $instance;
  135. }
  136. /**
  137. * Adds a data segment
  138. *
  139. * ISO/IEC 18004:2000 8.3.6 - Mixing modes
  140. * ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length
  141. */
  142. public function addSegment(QRDataModeInterface $segment):static{
  143. $this->dataSegments[] = $segment;
  144. return $this;
  145. }
  146. /**
  147. * Clears the data segments array
  148. *
  149. * @codeCoverageIgnore
  150. */
  151. public function clearSegments():static{
  152. $this->dataSegments = [];
  153. return $this;
  154. }
  155. /**
  156. * Adds a numeric data segment
  157. *
  158. * ISO/IEC 18004:2000 8.3.2 - Numeric Mode
  159. */
  160. public function addNumericSegment(string $data):static{
  161. return $this->addSegment(new Number($data));
  162. }
  163. /**
  164. * Adds an alphanumeric data segment
  165. *
  166. * ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode
  167. */
  168. public function addAlphaNumSegment(string $data):static{
  169. return $this->addSegment(new AlphaNum($data));
  170. }
  171. /**
  172. * Adds a Kanji data segment (Japanese 13-bit double-byte characters, Shift-JIS)
  173. *
  174. * ISO/IEC 18004:2000 8.3.5 - Kanji Mode
  175. */
  176. public function addKanjiSegment(string $data):static{
  177. return $this->addSegment(new Kanji($data));
  178. }
  179. /**
  180. * Adds a Hanzi data segment (simplified Chinese 13-bit double-byte characters, GB2312/GB18030)
  181. *
  182. * GBT18284-2000 Hanzi Mode
  183. */
  184. public function addHanziSegment(string $data):static{
  185. return $this->addSegment(new Hanzi($data));
  186. }
  187. /**
  188. * Adds an 8-bit byte data segment
  189. *
  190. * ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode
  191. */
  192. public function addByteSegment(string $data):static{
  193. return $this->addSegment(new Byte($data));
  194. }
  195. /**
  196. * Adds a standalone ECI designator
  197. *
  198. * The ECI designator must be followed by a Byte segment that contains the string encoded according to the given ECI charset
  199. *
  200. * ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode
  201. */
  202. public function addEciDesignator(int $encoding):static{
  203. return $this->addSegment(new ECI($encoding));
  204. }
  205. /**
  206. * Adds an ECI data segment (including designator)
  207. *
  208. * The given string will be encoded from mb_internal_encoding() to the given ECI character set
  209. *
  210. * I hate this somehow, but I'll leave it for now
  211. *
  212. * @throws \chillerlan\QRCode\QRCodeException
  213. */
  214. public function addEciSegment(int $encoding, string $data):static{
  215. // validate the encoding id
  216. $eciCharset = new ECICharset($encoding);
  217. // get charset name
  218. $eciCharsetName = $eciCharset->getName();
  219. // convert the string to the given charset
  220. if($eciCharsetName !== null){
  221. $data = mb_convert_encoding($data, $eciCharsetName, mb_internal_encoding());
  222. if($data === false){
  223. throw new QRCodeException('mb_convert_encoding() error'); // @codeCoverageIgnore
  224. }
  225. return $this
  226. ->addEciDesignator($eciCharset->getID())
  227. ->addByteSegment($data)
  228. ;
  229. }
  230. throw new QRCodeException('unable to add ECI segment');
  231. }
  232. /**
  233. * Reads a QR Code from a given file
  234. */
  235. public function readFromFile(string $path):DecoderResult{
  236. return $this->readFromSource($this->luminanceSourceFQN::fromFile($path, $this->options));
  237. }
  238. /**
  239. * Reads a QR Code from the given data blob
  240. */
  241. public function readFromBlob(string $blob):DecoderResult{
  242. return $this->readFromSource($this->luminanceSourceFQN::fromBlob($blob, $this->options));
  243. }
  244. /**
  245. * Reads a QR Code from the given luminance source
  246. */
  247. public function readFromSource(LuminanceSourceInterface $source):DecoderResult{
  248. return (new Decoder($this->options))->decode($source);
  249. }
  250. }