BitMatrix.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <?php
  2. declare(strict_types = 1);
  3. namespace BaconQrCode\Common;
  4. use BaconQrCode\Exception\InvalidArgumentException;
  5. use SplFixedArray;
  6. /**
  7. * Bit matrix.
  8. *
  9. * Represents a 2D matrix of bits. In function arguments below, and throughout
  10. * the common module, x is the column position, and y is the row position. The
  11. * ordering is always x, y. The origin is at the top-left.
  12. */
  13. class BitMatrix
  14. {
  15. /**
  16. * Width of the bit matrix.
  17. *
  18. * @var int
  19. */
  20. private $width;
  21. /**
  22. * Height of the bit matrix.
  23. *
  24. * @var int
  25. */
  26. private $height;
  27. /**
  28. * Size in bits of each individual row.
  29. *
  30. * @var int
  31. */
  32. private $rowSize;
  33. /**
  34. * Bits representation.
  35. *
  36. * @var SplFixedArray<int>
  37. */
  38. private $bits;
  39. /**
  40. * @throws InvalidArgumentException if a dimension is smaller than zero
  41. */
  42. public function __construct(int $width, int $height = null)
  43. {
  44. if (null === $height) {
  45. $height = $width;
  46. }
  47. if ($width < 1 || $height < 1) {
  48. throw new InvalidArgumentException('Both dimensions must be greater than zero');
  49. }
  50. $this->width = $width;
  51. $this->height = $height;
  52. $this->rowSize = ($width + 31) >> 5;
  53. $this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
  54. }
  55. /**
  56. * Gets the requested bit, where true means black.
  57. */
  58. public function get(int $x, int $y) : bool
  59. {
  60. $offset = $y * $this->rowSize + ($x >> 5);
  61. return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
  62. }
  63. /**
  64. * Sets the given bit to true.
  65. */
  66. public function set(int $x, int $y) : void
  67. {
  68. $offset = $y * $this->rowSize + ($x >> 5);
  69. $this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
  70. }
  71. /**
  72. * Flips the given bit.
  73. */
  74. public function flip(int $x, int $y) : void
  75. {
  76. $offset = $y * $this->rowSize + ($x >> 5);
  77. $this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
  78. }
  79. /**
  80. * Clears all bits (set to false).
  81. */
  82. public function clear() : void
  83. {
  84. $max = count($this->bits);
  85. for ($i = 0; $i < $max; ++$i) {
  86. $this->bits[$i] = 0;
  87. }
  88. }
  89. /**
  90. * Sets a square region of the bit matrix to true.
  91. *
  92. * @throws InvalidArgumentException if left or top are negative
  93. * @throws InvalidArgumentException if width or height are smaller than 1
  94. * @throws InvalidArgumentException if region does not fit into the matix
  95. */
  96. public function setRegion(int $left, int $top, int $width, int $height) : void
  97. {
  98. if ($top < 0 || $left < 0) {
  99. throw new InvalidArgumentException('Left and top must be non-negative');
  100. }
  101. if ($height < 1 || $width < 1) {
  102. throw new InvalidArgumentException('Width and height must be at least 1');
  103. }
  104. $right = $left + $width;
  105. $bottom = $top + $height;
  106. if ($bottom > $this->height || $right > $this->width) {
  107. throw new InvalidArgumentException('The region must fit inside the matrix');
  108. }
  109. for ($y = $top; $y < $bottom; ++$y) {
  110. $offset = $y * $this->rowSize;
  111. for ($x = $left; $x < $right; ++$x) {
  112. $index = $offset + ($x >> 5);
  113. $this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
  114. }
  115. }
  116. }
  117. /**
  118. * A fast method to retrieve one row of data from the matrix as a BitArray.
  119. */
  120. public function getRow(int $y, BitArray $row = null) : BitArray
  121. {
  122. if (null === $row || $row->getSize() < $this->width) {
  123. $row = new BitArray($this->width);
  124. }
  125. $offset = $y * $this->rowSize;
  126. for ($x = 0; $x < $this->rowSize; ++$x) {
  127. $row->setBulk($x << 5, $this->bits[$offset + $x]);
  128. }
  129. return $row;
  130. }
  131. /**
  132. * Sets a row of data from a BitArray.
  133. */
  134. public function setRow(int $y, BitArray $row) : void
  135. {
  136. $bits = $row->getBitArray();
  137. for ($i = 0; $i < $this->rowSize; ++$i) {
  138. $this->bits[$y * $this->rowSize + $i] = $bits[$i];
  139. }
  140. }
  141. /**
  142. * This is useful in detecting the enclosing rectangle of a 'pure' barcode.
  143. *
  144. * @return int[]|null
  145. */
  146. public function getEnclosingRectangle() : ?array
  147. {
  148. $left = $this->width;
  149. $top = $this->height;
  150. $right = -1;
  151. $bottom = -1;
  152. for ($y = 0; $y < $this->height; ++$y) {
  153. for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
  154. $bits = $this->bits[$y * $this->rowSize + $x32];
  155. if (0 !== $bits) {
  156. if ($y < $top) {
  157. $top = $y;
  158. }
  159. if ($y > $bottom) {
  160. $bottom = $y;
  161. }
  162. if ($x32 * 32 < $left) {
  163. $bit = 0;
  164. while (($bits << (31 - $bit)) === 0) {
  165. $bit++;
  166. }
  167. if (($x32 * 32 + $bit) < $left) {
  168. $left = $x32 * 32 + $bit;
  169. }
  170. }
  171. }
  172. if ($x32 * 32 + 31 > $right) {
  173. $bit = 31;
  174. while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
  175. --$bit;
  176. }
  177. if (($x32 * 32 + $bit) > $right) {
  178. $right = $x32 * 32 + $bit;
  179. }
  180. }
  181. }
  182. }
  183. $width = $right - $left;
  184. $height = $bottom - $top;
  185. if ($width < 0 || $height < 0) {
  186. return null;
  187. }
  188. return [$left, $top, $width, $height];
  189. }
  190. /**
  191. * Gets the most top left set bit.
  192. *
  193. * This is useful in detecting a corner of a 'pure' barcode.
  194. *
  195. * @return int[]|null
  196. */
  197. public function getTopLeftOnBit() : ?array
  198. {
  199. $bitsOffset = 0;
  200. while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
  201. ++$bitsOffset;
  202. }
  203. if (count($this->bits) === $bitsOffset) {
  204. return null;
  205. }
  206. $x = intdiv($bitsOffset, $this->rowSize);
  207. $y = ($bitsOffset % $this->rowSize) << 5;
  208. $bits = $this->bits[$bitsOffset];
  209. $bit = 0;
  210. while (0 === ($bits << (31 - $bit))) {
  211. ++$bit;
  212. }
  213. $x += $bit;
  214. return [$x, $y];
  215. }
  216. /**
  217. * Gets the most bottom right set bit.
  218. *
  219. * This is useful in detecting a corner of a 'pure' barcode.
  220. *
  221. * @return int[]|null
  222. */
  223. public function getBottomRightOnBit() : ?array
  224. {
  225. $bitsOffset = count($this->bits) - 1;
  226. while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
  227. --$bitsOffset;
  228. }
  229. if ($bitsOffset < 0) {
  230. return null;
  231. }
  232. $x = intdiv($bitsOffset, $this->rowSize);
  233. $y = ($bitsOffset % $this->rowSize) << 5;
  234. $bits = $this->bits[$bitsOffset];
  235. $bit = 0;
  236. while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
  237. --$bit;
  238. }
  239. $x += $bit;
  240. return [$x, $y];
  241. }
  242. /**
  243. * Gets the width of the matrix,
  244. */
  245. public function getWidth() : int
  246. {
  247. return $this->width;
  248. }
  249. /**
  250. * Gets the height of the matrix.
  251. */
  252. public function getHeight() : int
  253. {
  254. return $this->height;
  255. }
  256. }