QRCode.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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. namespace chillerlan\QRCode;
  11. use chillerlan\QRCode\Common\{ECICharset, MaskPattern, MaskPatternTester, Mode};
  12. use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number, QRData, QRCodeDataException, QRDataModeInterface, QRMatrix};
  13. use chillerlan\QRCode\Decoder\{Decoder, DecoderResult, GDLuminanceSource, IMagickLuminanceSource};
  14. use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString};
  15. use chillerlan\Settings\SettingsContainerInterface;
  16. use function class_exists, class_implements, in_array, mb_convert_encoding, mb_detect_encoding;
  17. /**
  18. * Turns a text string into a Model 2 QR Code
  19. *
  20. * @see https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
  21. * @see http://www.qrcode.com/en/codes/model12.html
  22. * @see https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf
  23. * @see https://en.wikipedia.org/wiki/QR_code
  24. * @see http://www.thonky.com/qr-code-tutorial/
  25. */
  26. class QRCode{
  27. /** @var int */
  28. public const VERSION_AUTO = -1;
  29. /** @var int */
  30. public const MASK_PATTERN_AUTO = -1;
  31. /** @var string */
  32. public const OUTPUT_MARKUP_HTML = 'html';
  33. /** @var string */
  34. public const OUTPUT_MARKUP_SVG = 'svg';
  35. /** @var string */
  36. public const OUTPUT_IMAGE_PNG = 'png';
  37. /** @var string */
  38. public const OUTPUT_IMAGE_JPG = 'jpg';
  39. /** @var string */
  40. public const OUTPUT_IMAGE_GIF = 'gif';
  41. /** @var string */
  42. public const OUTPUT_STRING_JSON = 'json';
  43. /** @var string */
  44. public const OUTPUT_STRING_TEXT = 'text';
  45. /** @var string */
  46. public const OUTPUT_IMAGICK = 'imagick';
  47. /** @var string */
  48. public const OUTPUT_FPDF = 'fpdf';
  49. /** @var string */
  50. public const OUTPUT_CUSTOM = 'custom';
  51. /**
  52. * Map of built-in output modules => capabilities
  53. *
  54. * @var string[][]
  55. */
  56. public const OUTPUT_MODES = [
  57. QRMarkup::class => [
  58. self::OUTPUT_MARKUP_SVG,
  59. self::OUTPUT_MARKUP_HTML,
  60. ],
  61. QRImage::class => [
  62. self::OUTPUT_IMAGE_PNG,
  63. self::OUTPUT_IMAGE_GIF,
  64. self::OUTPUT_IMAGE_JPG,
  65. ],
  66. QRString::class => [
  67. self::OUTPUT_STRING_JSON,
  68. self::OUTPUT_STRING_TEXT,
  69. ],
  70. QRImagick::class => [
  71. self::OUTPUT_IMAGICK,
  72. ],
  73. QRFpdf::class => [
  74. self::OUTPUT_FPDF,
  75. ],
  76. ];
  77. /**
  78. * The settings container
  79. *
  80. * @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface
  81. */
  82. protected SettingsContainerInterface $options;
  83. /**
  84. * The selected data interface (Number, AlphaNum, Kanji, Byte)
  85. */
  86. protected QRData $dataInterface;
  87. /**
  88. * A collection of one or more data segments of [classname, data] to write
  89. *
  90. * @see \chillerlan\QRCode\Data\QRDataModeInterface
  91. *
  92. * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
  93. */
  94. protected array $dataSegments = [];
  95. /**
  96. * The FQCN of the luminance sporce class to use in the reader (GD or Imagick)
  97. *
  98. * @see \chillerlan\QRCode\Decoder\LuminanceSourceInterface
  99. */
  100. private string $luminanceSourceClass;
  101. /**
  102. * QRCode constructor.
  103. *
  104. * Sets the options instance
  105. */
  106. public function __construct(SettingsContainerInterface $options = null){
  107. $this->options = $options ?? new QROptions;
  108. $this->luminanceSourceClass = $this->options->useImagickIfAvailable
  109. ? IMagickLuminanceSource::class
  110. : GDLuminanceSource::class;
  111. }
  112. /**
  113. * Renders a QR Code for the given $data and QROptions
  114. *
  115. * @return mixed
  116. */
  117. public function render(string $data = null, string $file = null){
  118. if($data !== null){
  119. /** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */
  120. foreach(Mode::DATA_INTERFACES as $dataInterface){
  121. if($dataInterface::validateString($data)){
  122. $this->addSegment(new $dataInterface($data));
  123. break;
  124. }
  125. }
  126. }
  127. return $this->initOutputInterface()->dump($file);
  128. }
  129. /**
  130. * Returns a QRMatrix object for the given $data and current QROptions
  131. *
  132. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  133. */
  134. public function getMatrix():QRMatrix{
  135. if(empty($this->dataSegments)){
  136. throw new QRCodeDataException('QRCode::getMatrix() No data given.');
  137. }
  138. $this->dataInterface = new QRData($this->options, $this->dataSegments);
  139. $maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
  140. ? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern()
  141. : new MaskPattern($this->options->maskPattern);
  142. $matrix = $this->dataInterface->writeMatrix($maskPattern);
  143. if($this->options->addQuietzone){
  144. $matrix->setQuietZone($this->options->quietzoneSize);
  145. }
  146. return $matrix;
  147. }
  148. /**
  149. * returns a fresh (built-in) QROutputInterface
  150. *
  151. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  152. */
  153. protected function initOutputInterface():QROutputInterface{
  154. if($this->options->outputType === $this::OUTPUT_CUSTOM){
  155. return $this->initCustomOutputInterface();
  156. }
  157. foreach($this::OUTPUT_MODES as $outputInterface => $modes){
  158. if(in_array($this->options->outputType, $modes)){
  159. return new $outputInterface($this->options, $this->getMatrix());
  160. }
  161. }
  162. throw new QRCodeOutputException('invalid output type');
  163. }
  164. /**
  165. * initializes a custom output module after checking the existence of the class and if it implemnts the required interface
  166. *
  167. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  168. */
  169. protected function initCustomOutputInterface():QROutputInterface{
  170. if(!class_exists($this->options->outputInterface)){
  171. throw new QRCodeOutputException('invalid custom output module');
  172. }
  173. if(!in_array(QROutputInterface::class, class_implements($this->options->outputInterface))){
  174. throw new QRCodeOutputException('custom output module does not implement QROutputInterface');
  175. }
  176. /** @phan-suppress-next-line PhanTypeExpectedObjectOrClassName */
  177. return new $this->options->outputInterface($this->options, $this->getMatrix());
  178. }
  179. /**
  180. * checks if a string qualifies as numeric (convenience method)
  181. */
  182. public function isNumber(string $string):bool{
  183. return Number::validateString($string);
  184. }
  185. /**
  186. * checks if a string qualifies as alphanumeric (convenience method)
  187. */
  188. public function isAlphaNum(string $string):bool{
  189. return AlphaNum::validateString($string);
  190. }
  191. /**
  192. * checks if a string qualifies as Kanji (convenience method)
  193. */
  194. public function isKanji(string $string):bool{
  195. return Kanji::validateString($string);
  196. }
  197. /**
  198. * a dummy (convenience method)
  199. */
  200. public function isByte(string $string):bool{
  201. return Byte::validateString($string);
  202. }
  203. /**
  204. * ISO/IEC 18004:2000 8.3.6 - Mixing modes
  205. * ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length
  206. */
  207. protected function addSegment(QRDataModeInterface $segment):void{
  208. $this->dataSegments[] = $segment;
  209. }
  210. /**
  211. * ISO/IEC 18004:2000 8.3.2 - Numeric Mode
  212. */
  213. public function addNumberSegment(string $data):self{
  214. $this->addSegment(new Number($data));
  215. return $this;
  216. }
  217. /**
  218. * ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode
  219. */
  220. public function addAlphaNumSegment(string $data):self{
  221. $this->addSegment(new AlphaNum($data));
  222. return $this;
  223. }
  224. /**
  225. * ISO/IEC 18004:2000 8.3.5 - Kanji Mode
  226. */
  227. public function addKanjiSegment(string $data):self{
  228. $this->addSegment(new Kanji($data));
  229. return $this;
  230. }
  231. /**
  232. * ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode
  233. */
  234. public function addByteSegment(string $data):self{
  235. $this->addSegment(new Byte($data));
  236. return $this;
  237. }
  238. /**
  239. * ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode
  240. */
  241. public function addEciDesignator(int $encoding):self{
  242. $this->addSegment(new ECI($encoding));
  243. return $this;
  244. }
  245. /**
  246. * i hate this somehow but i'll leave it for now
  247. *
  248. * @throws \chillerlan\QRCode\QRCodeException
  249. */
  250. public function addEciSegment(int $encoding, string $data):self{
  251. // validate the encoding id
  252. $eciCharset = new ECICharset($encoding);
  253. // get charset name
  254. $eciCharsetName = $eciCharset->getName();
  255. // convert the string to the given charset
  256. if($eciCharsetName !== null){
  257. $data = mb_convert_encoding($data, $eciCharsetName, mb_detect_encoding($data));
  258. // add ECI designator
  259. $this->addSegment(new ECI($eciCharset->getID()));
  260. $this->addSegment(new Byte($data));
  261. return $this;
  262. }
  263. throw new QRCodeException('unable to add ECI segment');
  264. }
  265. /**
  266. * Clears the data segments array
  267. */
  268. public function clearSegments():self{
  269. $this->dataSegments = [];
  270. return $this;
  271. }
  272. /**
  273. * Reads a QR Code from a given file
  274. */
  275. public function readFromFile(string $path):DecoderResult{
  276. /** @noinspection PhpUndefinedMethodInspection */
  277. return (new Decoder)->decode($this->luminanceSourceClass::fromFile($path));
  278. }
  279. /**
  280. * Reads a QR Code from the given data blob
  281. */
  282. public function readFromBlob(string $blob):DecoderResult{
  283. /** @noinspection PhpUndefinedMethodInspection */
  284. return (new Decoder)->decode($this->luminanceSourceClass::fromBlob($blob));
  285. }
  286. }