ECI.php 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. <?php
  2. /**
  3. * Class ECI
  4. *
  5. * @created 20.11.2020
  6. * @author smiley <smiley@chillerlan.net>
  7. * @copyright 2020 smiley
  8. * @license MIT
  9. */
  10. namespace chillerlan\QRCode\Data;
  11. use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode};
  12. use function mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, sprintf;
  13. /**
  14. * Adds an ECI Designator
  15. *
  16. * ISO/IEC 18004:2000 8.4.1.1
  17. *
  18. * Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment()
  19. */
  20. final class ECI extends QRDataModeAbstract{
  21. public const DATAMODE = Mode::ECI;
  22. /**
  23. * The current ECI encoding id
  24. */
  25. private int $encoding;
  26. /**
  27. * @inheritDoc
  28. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  29. * @noinspection PhpMissingParentConstructorInspection
  30. */
  31. public function __construct(int $encoding){
  32. if($encoding < 0 || $encoding > 999999){
  33. throw new QRCodeDataException(sprintf('invalid encoding id: "%s"', $encoding));
  34. }
  35. $this->encoding = $encoding;
  36. }
  37. public function getLengthInBits():int{
  38. if($this->encoding < 128){
  39. return 8;
  40. }
  41. if($this->encoding < 16384){
  42. return 16;
  43. }
  44. return 24;
  45. }
  46. /**
  47. * Writes an ECI designator to the bitbuffer
  48. *
  49. * @inheritDoc
  50. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  51. */
  52. public function write(BitBuffer $bitBuffer, int $versionNumber):static{
  53. $bitBuffer->put(self::DATAMODE, 4);
  54. if($this->encoding < 128){
  55. $bitBuffer->put($this->encoding, 8);
  56. }
  57. elseif($this->encoding < 16384){
  58. $bitBuffer->put(($this->encoding | 0x8000), 16);
  59. }
  60. elseif($this->encoding < 1000000){
  61. $bitBuffer->put(($this->encoding | 0xC00000), 24);
  62. }
  63. else{
  64. throw new QRCodeDataException('invalid ECI ID');
  65. }
  66. return $this;
  67. }
  68. /**
  69. * Reads and parses the value of an ECI designator
  70. *
  71. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  72. */
  73. public static function parseValue(BitBuffer $bitBuffer):ECICharset{
  74. $firstByte = $bitBuffer->read(8);
  75. // just one byte
  76. if(($firstByte & 0b10000000) === 0){
  77. $id = ($firstByte & 0b01111111);
  78. }
  79. // two bytes
  80. elseif(($firstByte & 0b11000000) === 0b10000000){
  81. $id = ((($firstByte & 0b00111111) << 8) | $bitBuffer->read(8));
  82. }
  83. // three bytes
  84. elseif(($firstByte & 0b11100000) === 0b11000000){
  85. $id = ((($firstByte & 0b00011111) << 16) | $bitBuffer->read(16));
  86. }
  87. else{
  88. throw new QRCodeDataException(sprintf('error decoding ECI value first byte: %08b', $firstByte));// @codeCoverageIgnore
  89. }
  90. return new ECICharset($id);
  91. }
  92. /**
  93. * @codeCoverageIgnore Unused, but required as per interface
  94. */
  95. public static function validateString(string $string):bool{
  96. return true;
  97. }
  98. /**
  99. * Reads and decodes the ECI designator including the following byte sequence
  100. *
  101. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  102. */
  103. public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
  104. $eciCharset = self::parseValue($bitBuffer);
  105. $nextMode = $bitBuffer->read(4);
  106. if($nextMode !== Mode::BYTE){
  107. throw new QRCodeDataException(sprintf('ECI designator followed by invalid mode: "%04b"', $nextMode));
  108. }
  109. $data = Byte::decodeSegment($bitBuffer, $versionNumber);
  110. $encoding = $eciCharset->getName();
  111. if($encoding === null){
  112. // The spec isn't clear on this mode; see
  113. // section 6.4.5: t does not say which encoding to assuming
  114. // upon decoding. I have seen ISO-8859-1 used as well as
  115. // Shift_JIS -- without anything like an ECI designator to
  116. // give a hint.
  117. $encoding = mb_detect_encoding($data, ['ISO-8859-1', 'Windows-1252', 'SJIS', 'UTF-8'], true);
  118. if($encoding === false){
  119. throw new QRCodeDataException('could not determine encoding in ECI mode'); // @codeCoverageIgnore
  120. }
  121. }
  122. return mb_convert_encoding($data, mb_internal_encoding(), $encoding);
  123. }
  124. }