Browse Source

Merge branch 'v5'

# Conflicts:
#	.github/workflows/tests.yml
#	.travis.yml
#	README.md
#	composer.json
#	examples/imageWithLogo.php
#	examples/svg.php
#	src/QRCode.php
codemasher 4 năm trước cách đây
mục cha
commit
c3d299153a
100 tập tin đã thay đổi với 7839 bổ sung1749 xóa
  1. 12 0
      .editorconfig
  2. 6 4
      .github/workflows/docs.yml
  3. 13 1
      .gitignore
  4. 888 0
      .idea/codeStyles/Project.xml
  5. 5 0
      .idea/codeStyles/codeStyleConfig.xml
  6. 28 0
      .idea/inspectionProfiles/Project_Default.xml
  7. 11 0
      .phan/stubs/misc.php
  8. 13 0
      NOTICE
  9. 12 9
      README.md
  10. 10 2
      composer.json
  11. 0 0
      docs/example_image.png
  12. 0 0
      docs/example_svg.png
  13. 1 3
      examples/MyCustomOutput.php
  14. 1 3
      examples/QRImageWithLogo.php
  15. 2 4
      examples/QRImageWithText.php
  16. 7 5
      examples/custom_output.php
  17. 18 16
      examples/fpdf.php
  18. 23 35
      examples/html.php
  19. 22 22
      examples/image.php
  20. 7 5
      examples/imageWithLogo.php
  21. 4 2
      examples/imageWithText.php
  22. 19 18
      examples/imagick.php
  23. 27 26
      examples/svg.php
  24. 20 19
      examples/text.php
  25. 0 1
      public/qrcode.php
  26. 160 0
      src/Common/BitBuffer.php
  27. 109 0
      src/Common/ECICharset.php
  28. 209 0
      src/Common/EccLevel.php
  29. 82 0
      src/Common/FormatInformation.php
  30. 151 0
      src/Common/GF256.php
  31. 276 0
      src/Common/GenericGFPoly.php
  32. 86 0
      src/Common/MaskPattern.php
  33. 17 18
      src/Common/MaskPatternTester.php
  34. 112 0
      src/Common/Mode.php
  35. 192 0
      src/Common/ReedSolomonDecoder.php
  36. 115 0
      src/Common/ReedSolomonEncoder.php
  37. 334 0
      src/Common/Version.php
  38. 98 14
      src/Data/AlphaNum.php
  39. 47 9
      src/Data/Byte.php
  40. 92 0
      src/Data/ECI.php
  41. 85 17
      src/Data/Kanji.php
  42. 123 16
      src/Data/Number.php
  43. 0 2
      src/Data/QRCodeDataException.php
  44. 213 0
      src/Data/QRData.php
  45. 0 311
      src/Data/QRDataAbstract.php
  46. 0 200
      src/Data/QRDataInterface.php
  47. 48 0
      src/Data/QRDataModeAbstract.php
  48. 48 0
      src/Data/QRDataModeInterface.php
  49. 88 245
      src/Data/QRMatrix.php
  50. 361 0
      src/Decoder/Binarizer.php
  51. 203 0
      src/Decoder/BitMatrix.php
  52. 362 0
      src/Decoder/BitMatrixParser.php
  53. 337 0
      src/Decoder/Decoder.php
  54. 86 0
      src/Decoder/DecoderResult.php
  55. 66 0
      src/Decoder/GDLuminanceSource.php
  56. 53 0
      src/Decoder/IMagickLuminanceSource.php
  57. 107 0
      src/Decoder/LuminanceSource.php
  58. 34 0
      src/Detector/AlignmentPattern.php
  59. 284 0
      src/Detector/AlignmentPatternFinder.php
  60. 357 0
      src/Detector/Detector.php
  61. 80 0
      src/Detector/FinderPattern.php
  62. 772 0
      src/Detector/FinderPatternFinder.php
  63. 171 0
      src/Detector/GridSampler.php
  64. 153 0
      src/Detector/PerspectiveTransform.php
  65. 61 0
      src/Detector/ResultPoint.php
  66. 0 89
      src/Helpers/BitBuffer.php
  67. 0 178
      src/Helpers/Polynomial.php
  68. 0 2
      src/Output/QRCodeOutputException.php
  69. 3 6
      src/Output/QRFpdf.php
  70. 3 7
      src/Output/QRImage.php
  71. 0 2
      src/Output/QRImagick.php
  72. 1 3
      src/Output/QRMarkup.php
  73. 10 4
      src/Output/QROutputAbstract.php
  74. 20 22
      src/Output/QROutputInterface.php
  75. 0 2
      src/Output/QRString.php
  76. 127 133
      src/QRCode.php
  77. 0 2
      src/QRCodeException.php
  78. 87 0
      src/QRCodeReader.php
  79. 0 3
      src/QROptions.php
  80. 4 13
      src/QROptionsTrait.php
  81. 6 9
      tests/Common/BitBufferTest.php
  82. 7 8
      tests/Common/MaskPatternTesterTest.php
  83. 5 14
      tests/Data/AlphaNumTest.php
  84. 1 13
      tests/Data/ByteTest.php
  85. 31 25
      tests/Data/DatainterfaceTestAbstract.php
  86. 8 15
      tests/Data/KanjiTest.php
  87. 5 14
      tests/Data/NumberTest.php
  88. 60 57
      tests/Data/QRMatrixTest.php
  89. 0 42
      tests/Helpers/PolynomialTest.php
  90. 0 2
      tests/Output/QRFpdfTest.php
  91. 2 4
      tests/Output/QRImageTest.php
  92. 0 2
      tests/Output/QRImagickTest.php
  93. 3 5
      tests/Output/QRMarkupTest.php
  94. 4 4
      tests/Output/QROutputTestAbstract.php
  95. 6 6
      tests/Output/QRStringTest.php
  96. 0 0
      tests/Output/samples/json
  97. 0 0
      tests/Output/samples/svg
  98. 123 0
      tests/QRCodeReaderTest.php
  99. 2 54
      tests/QRCodeTest.php
  100. 0 2
      tests/QROptionsTest.php

+ 12 - 0
.editorconfig

@@ -0,0 +1,12 @@
+root = true
+
+[*]
+indent_style = tab
+charset = utf-8
+end_of_line = lf
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.yml]
+indent_style = space
+indent_size = 2

+ 6 - 4
.github/workflows/docs.yml

@@ -22,7 +22,9 @@ jobs:
         uses: ./.github/actions/build-docs
 
       - name: "Publish Docs to gh-pages"
-        uses: maxheld83/ghpages@v0.3.0
-        env:
-          BUILD_DIR: docs/
-          GH_PAT: ${{ secrets.GH_PAT }}
+        uses: JamesIves/github-pages-deploy-action@4.1
+        with:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          BRANCH: gh-pages
+          FOLDER: dist
+          CLEAN: true

+ 13 - 1
.gitignore

@@ -1,5 +1,17 @@
-.build/*
+# IDE - IntelliJ
 .idea/*
+# Keep the code styles.
+!.idea/codeStyles
+.idea/codeStyles/*
+!.idea/codeStyles/Project.xml
+!.idea/codeStyles/codeStyleConfig.xml
+# Keep the inspection levels
+!.idea/inspectionProfiles
+.idea/inspectionProfiles/*
+!.idea/inspectionProfiles/Project_Default.xml
+
+# project stuff
+.build/*
 docs/*
 vendor/*
 composer.lock

+ 888 - 0
.idea/codeStyles/Project.xml

@@ -0,0 +1,888 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <option name="AUTODETECT_INDENTS" value="false" />
+    <option name="OTHER_INDENT_OPTIONS">
+      <value>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+      </value>
+    </option>
+    <option name="RIGHT_MARGIN" value="130" />
+    <CssCodeStyleSettings>
+      <option name="HEX_COLOR_LOWER_CASE" value="true" />
+      <option name="HEX_COLOR_SHORT_FORMAT" value="true" />
+      <option name="KEEP_SINGLE_LINE_BLOCKS" value="true" />
+      <option name="SPACE_BEFORE_OPENING_BRACE" value="false" />
+    </CssCodeStyleSettings>
+    <DB2CodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </DB2CodeStyleSettings>
+    <DerbyCodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </DerbyCodeStyleSettings>
+    <H2CodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </H2CodeStyleSettings>
+    <H2CodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </H2CodeStyleSettings>
+    <HSQLCodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </HSQLCodeStyleSettings>
+    <HTMLCodeStyleSettings>
+      <option name="HTML_ATTRIBUTE_WRAP" value="0" />
+      <option name="HTML_TEXT_WRAP" value="0" />
+      <option name="HTML_ENFORCE_QUOTES" value="true" />
+    </HTMLCodeStyleSettings>
+    <JSCodeStyleSettings version="0">
+      <option name="FORCE_SEMICOLON_STYLE" value="true" />
+      <option name="ALIGN_OBJECT_PROPERTIES" value="2" />
+      <option name="ALIGN_VAR_STATEMENTS" value="1" />
+      <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
+      <option name="SPACE_BEFORE_CLASS_LBRACE" value="false" />
+      <option name="USE_DOUBLE_QUOTES" value="false" />
+      <option name="FORCE_QUOTE_STYlE" value="true" />
+      <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
+      <option name="IMPORTS_WRAP" value="1" />
+      <option name="SPACE_BEFORE_ASYNC_ARROW_LPAREN" value="false" />
+    </JSCodeStyleSettings>
+    <MSSQLCodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </MSSQLCodeStyleSettings>
+    <MySQLCodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </MySQLCodeStyleSettings>
+    <OracleCodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </OracleCodeStyleSettings>
+    <PHPCodeStyleSettings>
+      <option name="ALIGN_KEY_VALUE_PAIRS" value="true" />
+      <option name="ALIGN_PHPDOC_PARAM_NAMES" value="true" />
+      <option name="ALIGN_PHPDOC_COMMENTS" value="true" />
+      <option name="ALIGN_ASSIGNMENTS" value="true" />
+      <option name="CONCAT_SPACES" value="false" />
+      <option name="COMMA_AFTER_LAST_ARRAY_ELEMENT" value="true" />
+      <option name="PHPDOC_BLANK_LINE_BEFORE_TAGS" value="true" />
+      <option name="PHPDOC_BLANK_LINES_AROUND_PARAMETERS" value="true" />
+      <option name="PHPDOC_WRAP_LONG_LINES" value="true" />
+      <option name="LOWER_CASE_BOOLEAN_CONST" value="true" />
+      <option name="LOWER_CASE_NULL_CONST" value="true" />
+      <option name="ELSE_IF_STYLE" value="COMBINE" />
+      <option name="FIELDS_DEFAULT_VISIBILITY" value="protected" />
+      <option name="BLANK_LINES_BEFORE_RETURN_STATEMENT" value="1" />
+      <option name="KEEP_RPAREN_AND_LBRACE_ON_ONE_LINE" value="true" />
+      <option name="ALIGN_CLASS_CONSTANTS" value="true" />
+      <option name="KEEP_BLANK_LINES_AFTER_LBRACE" value="1" />
+      <option name="SPACE_BEFORE_CLOSURE_LEFT_PARENTHESIS" value="false" />
+      <option name="FORCE_SHORT_DECLARATION_ARRAY_STYLE" value="true" />
+      <option name="SPACE_AROUND_ASSIGNMENT_IN_DECLARE" value="true" />
+      <option name="SPACE_AFTER_COLON_IN_RETURN_TYPE" value="false" />
+      <option name="PHPDOC_USE_FQCN" value="true" />
+      <option name="MULTILINE_CHAINED_CALLS_SEMICOLON_ON_NEW_LINE" value="true" />
+      <option name="PREFER_TEMPLATE_INDENTS" value="true" />
+    </PHPCodeStyleSettings>
+    <PostgresCodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </PostgresCodeStyleSettings>
+    <SQLiteCodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </SQLiteCodeStyleSettings>
+    <SqlCodeStyleSettings version="5">
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="SELECT_ALIGN_AS" value="false" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_INDENT_THEN_ELSE" value="true" />
+      <option name="IMP_IF_THEN_INDENT_END" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+      <option name="ALIGN_AS_IN_SELECT_STATEMENT" value="false" />
+      <option name="NEW_LINE_BEFORE_THEN" value="false" />
+      <option name="INDENT_SELECT_INTO_CLAUSE" value="true" />
+    </SqlCodeStyleSettings>
+    <SybaseCodeStyleSettings version="5">
+      <option name="USE_GENERAL_STYLE" value="false" />
+      <option name="TYPE_CASE" value="3" />
+      <option name="CUSTOM_TYPE_CASE" value="3" />
+      <option name="ALIAS_CASE" value="4" />
+      <option name="BUILT_IN_CASE" value="0" />
+      <option name="QUERY_TRUE_INDENT" value="false" />
+      <option name="QUERY_ALIGN_ELEMENTS" value="false" />
+      <option name="QUERY_ALIGN_LINE_COMMENTS" value="false" />
+      <option name="INSERT_EL_COMMA" value="2" />
+      <option name="SET_EL_WRAP" value="0" />
+      <option name="SET_EL_COMMA" value="0" />
+      <option name="WITH_EL_WRAP" value="0" />
+      <option name="WITH_EL_COMMA" value="0" />
+      <option name="SELECT_EL_WRAP" value="3" />
+      <option name="SELECT_EL_COMMA" value="2" />
+      <option name="FROM_EL_WRAP" value="2" />
+      <option name="FROM_EL_COMMA" value="2" />
+      <option name="FROM_PLACE_ON" value="10" />
+      <option name="WHERE_EL_WRAP" value="3" />
+      <option name="WHERE_EL_BOUND" value="2" />
+      <option name="ORDER_EL_COMMA" value="2" />
+      <option name="TABLE_OPENING" value="1" />
+      <option name="TABLE_CONTENT" value="2" />
+      <option name="TABLE_CLOSING" value="3" />
+      <option name="TABLE_ALTER_INSTRUCTION_ALIGN" value="false" />
+      <option name="POST_OPT_WRAP_1" value="true" />
+      <option name="POST_OPT_ALIGN" value="false" />
+      <option name="ROUTINE_ARG_COMMA" value="2" />
+      <option name="ROUTINE_ARG_ALIGN_TYPES" value="true" />
+      <option name="IMP_DECLARE_EL_WRAP" value="1" />
+      <option name="IMP_IF_THEN_WRAP_THEN" value="true" />
+      <option name="CORTEGE_SPACE_BEFORE_L_PAREN" value="false" />
+      <option name="EXPR_CASE_WHEN_WRAP" value="false" />
+      <option name="EXPR_CASE_THEN_WRAP" value="true" />
+      <option name="PRIMARY_KEY_NAME_TEMPLATE" value="{table}_{columns}_pk" />
+    </SybaseCodeStyleSettings>
+    <XML>
+      <option name="XML_ATTRIBUTE_WRAP" value="0" />
+      <option name="XML_TEXT_WRAP" value="0" />
+      <option name="XML_KEEP_WHITE_SPACES_INSIDE_CDATA" value="true" />
+    </XML>
+    <codeStyleSettings language="DB2">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="Derby">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="H2">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="HSQLDB">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="HTML">
+      <option name="SOFT_MARGINS" value="130" />
+      <indentOptions>
+        <option name="CONTINUATION_INDENT_SIZE" value="4" />
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="JSON">
+      <indentOptions>
+        <option name="INDENT_SIZE" value="4" />
+        <option name="CONTINUATION_INDENT_SIZE" value="4" />
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="JavaScript">
+      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
+      <option name="ELSE_ON_NEW_LINE" value="true" />
+      <option name="WHILE_ON_NEW_LINE" value="true" />
+      <option name="CATCH_ON_NEW_LINE" value="true" />
+      <option name="FINALLY_ON_NEW_LINE" value="true" />
+      <option name="ALIGN_MULTILINE_CHAINED_METHODS" value="true" />
+      <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+      <option name="ALIGN_MULTILINE_BINARY_OPERATION" value="true" />
+      <option name="ALIGN_MULTILINE_TERNARY_OPERATION" value="true" />
+      <option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
+      <option name="SPACE_BEFORE_IF_PARENTHESES" value="false" />
+      <option name="SPACE_BEFORE_WHILE_PARENTHESES" value="false" />
+      <option name="SPACE_BEFORE_FOR_PARENTHESES" value="false" />
+      <option name="SPACE_BEFORE_CATCH_PARENTHESES" value="false" />
+      <option name="SPACE_BEFORE_SWITCH_PARENTHESES" value="false" />
+      <option name="SPACE_BEFORE_METHOD_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_IF_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_ELSE_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_WHILE_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_FOR_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_DO_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_SWITCH_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_TRY_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_CATCH_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_FINALLY_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_ELSE_KEYWORD" value="false" />
+      <option name="SPACE_BEFORE_WHILE_KEYWORD" value="false" />
+      <option name="SPACE_BEFORE_CATCH_KEYWORD" value="false" />
+      <option name="SPACE_BEFORE_FINALLY_KEYWORD" value="false" />
+      <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+      <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+      <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+      <option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
+      <option name="ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE" value="true" />
+      <option name="PLACE_ASSIGNMENT_SIGN_ON_NEXT_LINE" value="true" />
+      <option name="IF_BRACE_FORCE" value="3" />
+      <option name="DOWHILE_BRACE_FORCE" value="3" />
+      <option name="WHILE_BRACE_FORCE" value="3" />
+      <option name="FOR_BRACE_FORCE" value="3" />
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="Markdown">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="MySQL">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="Oracle">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="PHP">
+      <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
+      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
+      <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
+      <option name="BLANK_LINES_AFTER_PACKAGE" value="1" />
+      <option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
+      <option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
+      <option name="BLANK_LINES_BEFORE_CLASS_END" value="1" />
+      <option name="CLASS_BRACE_STYLE" value="1" />
+      <option name="METHOD_BRACE_STYLE" value="1" />
+      <option name="ELSE_ON_NEW_LINE" value="true" />
+      <option name="WHILE_ON_NEW_LINE" value="true" />
+      <option name="CATCH_ON_NEW_LINE" value="true" />
+      <option name="FINALLY_ON_NEW_LINE" value="true" />
+      <option name="SPECIAL_ELSE_IF_TREATMENT" value="true" />
+      <option name="ALIGN_MULTILINE_CHAINED_METHODS" value="true" />
+      <option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="true" />
+      <option name="ALIGN_MULTILINE_BINARY_OPERATION" value="true" />
+      <option name="ALIGN_MULTILINE_TERNARY_OPERATION" value="true" />
+      <option name="ALIGN_MULTILINE_EXTENDS_LIST" value="true" />
+      <option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
+      <option name="ALIGN_GROUP_FIELD_DECLARATIONS" value="true" />
+      <option name="SPACE_BEFORE_IF_PARENTHESES" value="false" />
+      <option name="SPACE_BEFORE_WHILE_PARENTHESES" value="false" />
+      <option name="SPACE_BEFORE_FOR_PARENTHESES" value="false" />
+      <option name="SPACE_BEFORE_CATCH_PARENTHESES" value="false" />
+      <option name="SPACE_BEFORE_SWITCH_PARENTHESES" value="false" />
+      <option name="SPACE_BEFORE_CLASS_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_METHOD_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_IF_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_ELSE_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_WHILE_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_FOR_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_DO_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_SWITCH_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_TRY_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_CATCH_LBRACE" value="false" />
+      <option name="SPACE_BEFORE_FINALLY_LBRACE" value="false" />
+      <option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
+      <option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
+      <option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
+      <option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
+      <option name="PARENTHESES_EXPRESSION_LPAREN_WRAP" value="true" />
+      <option name="PARENTHESES_EXPRESSION_RPAREN_WRAP" value="true" />
+      <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+      <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+      <option name="FOR_STATEMENT_RPAREN_ON_NEXT_LINE" value="true" />
+      <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+      <option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
+      <option name="ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE" value="true" />
+      <option name="PLACE_ASSIGNMENT_SIGN_ON_NEXT_LINE" value="true" />
+      <option name="IF_BRACE_FORCE" value="3" />
+      <option name="DOWHILE_BRACE_FORCE" value="3" />
+      <option name="WHILE_BRACE_FORCE" value="3" />
+      <option name="FOR_BRACE_FORCE" value="3" />
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+      </indentOptions>
+      <arrangement>
+        <rules>
+          <section>
+            <rule>
+              <match>
+                <CONST />
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <FIELD />
+                  <PUBLIC />
+                  <STATIC />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <FIELD />
+                  <PROTECTED />
+                  <STATIC />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <FIELD />
+                  <PRIVATE />
+                  <STATIC />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <FIELD />
+                  <PUBLIC />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <FIELD />
+                  <PROTECTED />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <FIELD />
+                  <PRIVATE />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <CONSTRUCTOR />
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <METHOD />
+                  <PUBLIC />
+                  <STATIC />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <METHOD />
+                  <PROTECTED />
+                  <STATIC />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <METHOD />
+                  <PRIVATE />
+                  <STATIC />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <METHOD />
+                  <PUBLIC />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <METHOD />
+                  <PROTECTED />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <METHOD />
+                  <PRIVATE />
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <TRAIT />
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <INTERFACE />
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <CLASS />
+              </match>
+            </rule>
+          </section>
+        </rules>
+      </arrangement>
+    </codeStyleSettings>
+    <codeStyleSettings language="PostgreSQL">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="SQL">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="SQLite">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="Sybase">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="TSQL">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+        <option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="XML">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+        <option name="SMART_TABS" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+  </code_scheme>
+</component>

+ 5 - 0
.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+  </state>
+</component>

+ 28 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,28 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="PhpAssignmentInConditionInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
+    <inspection_tool class="PhpComposerExtensionStubsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
+    <inspection_tool class="PhpDivisionByZeroInspection" enabled="true" level="WARNING" enabled_by_default="true" />
+    <inspection_tool class="PhpDocSignatureInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="ALLOW_MISSING_PARAMETERS" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PhpIllegalPsrClassPathInspection" enabled="true" level="WARNING" enabled_by_default="true" />
+    <inspection_tool class="PhpInternalEntityUsedInspection" enabled="true" level="INFO" enabled_by_default="true" />
+    <inspection_tool class="PhpMethodOrClassCallIsNotCaseSensitiveInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
+    <inspection_tool class="PhpMissingDocCommentInspection" enabled="true" level="INFO" enabled_by_default="true" />
+    <inspection_tool class="PhpMissingParentCallMagicInspection" enabled="false" level="WARNING" enabled_by_default="false">
+      <option name="ENABLE_FOR_SLEEP" value="false" />
+      <option name="ENABLE_FOR_WAKEUP" value="false" />
+    </inspection_tool>
+    <inspection_tool class="PhpMultipleClassesDeclarationsInOneFile" enabled="true" level="WARNING" enabled_by_default="true" />
+    <inspection_tool class="PhpStatementHasEmptyBodyInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
+    <inspection_tool class="PhpUsageOfSilenceOperatorInspection" enabled="true" level="STFU!" enabled_by_default="true" />
+    <inspection_tool class="PhpVoidFunctionResultUsedInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
+    <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
+      <option name="processCode" value="true" />
+      <option name="processLiterals" value="true" />
+      <option name="processComments" value="true" />
+    </inspection_tool>
+  </profile>
+</component>

+ 11 - 0
.phan/stubs/misc.php

@@ -0,0 +1,11 @@
+<?php
+/**
+ * Miscellaneous stubs for phan
+ *
+ * @created      25.01.2021
+ * @author       smiley <smiley@chillerlan.net>
+ * @copyright    2021 smiley
+ * @license      MIT
+ */
+
+class GdImage{}

+ 13 - 0
NOTICE

@@ -0,0 +1,13 @@
+Copyright 2007 ZXing authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

+ 12 - 9
README.md

@@ -9,12 +9,12 @@ namespaced, cleaned up, improved and other stuff.
 [![CodeCov][coverage-badge]][coverage]
 [![Scrunitizer CI][scrutinizer-badge]][scrutinizer]
 [![Packagist downloads][downloads-badge]][downloads]<br/>
-[![Continuous Integration][gh-action-badge]][gh-action] 
+[![Continuous Integration][gh-action-badge]][gh-action]
 [![phpDocs][gh-docs-badge]][gh-docs]
 
 [php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-qrcode?logo=php&color=8892BF
 [php]: https://www.php.net/supported-versions.php
-[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-qrcode.svg
+[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-qrcode.svg?logo=packagist
 [packagist]: https://packagist.org/packages/chillerlan/php-qrcode
 [license-badge]: https://img.shields.io/github/license/chillerlan/php-qrcode.svg
 [license]: https://github.com/chillerlan/php-qrcode/blob/main/LICENSE
@@ -22,7 +22,7 @@ namespaced, cleaned up, improved and other stuff.
 [coverage]: https://codecov.io/github/chillerlan/php-qrcode
 [scrutinizer-badge]: https://img.shields.io/scrutinizer/g/chillerlan/php-qrcode.svg?logo=scrutinizer
 [scrutinizer]: https://scrutinizer-ci.com/g/chillerlan/php-qrcode
-[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-qrcode.svg
+[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-qrcode.svg?logo=packagist
 [downloads]: https://packagist.org/packages/chillerlan/php-qrcode/stats
 [gh-action-badge]: https://github.com/chillerlan/php-qrcode/workflows/Continuous%20Integration/badge.svg
 [gh-action]: https://github.com/chillerlan/php-qrcode/actions?query=workflow%3A%22Continuous+Integration%22
@@ -31,13 +31,13 @@ namespaced, cleaned up, improved and other stuff.
 
 ## Documentation
 
-See [the wiki](https://github.com/chillerlan/php-qrcode/wiki) for advanced documentation. 
+See [the wiki](https://github.com/chillerlan/php-qrcode/wiki) for advanced documentation.
 An API documentation created with [phpDocumentor](https://www.phpdoc.org/) can be found at https://chillerlan.github.io/php-qrcode/ (WIP).
 
 ### Requirements
 - PHP 7.4+
   - `ext-mbstring`
-  - optional: 
+  - optional:
     - `ext-json`, `ext-gd`
     - `ext-imagick` with [ImageMagick](https://imagemagick.org) installed
     - [`setasign/fpdf`](https://github.com/setasign/fpdf) for the PDF output module
@@ -47,7 +47,7 @@ An API documentation created with [phpDocumentor](https://www.phpdoc.org/) can b
 
 via terminal: `composer require chillerlan/php-qrcode`
 
-*composer.json* 
+*composer.json*
 ```json
 {
 	"require": {
@@ -74,8 +74,8 @@ echo '<img src="'.(new QRCode)->render($data).'" alt="QR Code" />';
 ```
 
 <p align="center">
-	<img alt="QR codes are awesome!" src="https://raw.githubusercontent.com/chillerlan/php-qrcode/main/examples/example_image.png">
-	<img alt="QR codes are awesome!" src="https://raw.githubusercontent.com/chillerlan/php-qrcode/main/examples/example_svg.png">
+	<img alt="QR codes are awesome!" src="https://raw.githubusercontent.com/chillerlan/php-qrcode/main/docs/example_image.png">
+	<img alt="QR codes are awesome!" src="https://raw.githubusercontent.com/chillerlan/php-qrcode/main/docs/example_svg.png">
 </p>
 
 Wait, what was that? Please again, slower! See [Advanced usage](https://github.com/chillerlan/php-qrcode/wiki/Advanced-usage) on the wiki.
@@ -84,7 +84,7 @@ Wait, what was that? Please again, slower! See [Advanced usage](https://github.c
 - Drupal [Google Authenticator Login `ga_login`](https://www.drupal.org/project/ga_login)
 - WordPress [`wp-two-factor-auth`](https://github.com/sjinks/wp-two-factor-auth)
 - WordPress [Simple 2FA `simple-2fa`](https://wordpress.org/plugins/simple-2fa/)
-- WoltLab Suite [two-step-verification](http://pluginstore.woltlab.com/file/3007-two-step-verification/)  
+- WoltLab Suite [two-step-verification](http://pluginstore.woltlab.com/file/3007-two-step-verification/)
 - [Cachet](https://github.com/CachetHQ/Cachet)
 - [Appwrite](https://github.com/appwrite/appwrite)
 - [twill](https://github.com/area17/twill)
@@ -100,6 +100,9 @@ Hi, please check out my other projects that are way cooler than qrcodes!
 ### Disclaimer!
 I don't take responsibility for molten CPUs, misled applications, failed log-ins etc.. Use at your own risk!
 
+#### License notice
+Parts of this code are [ported to php](https://github.com/khanamiryan/php-qrcode-detector-decoder) from the [ZXing project](https://github.com/zxing/zxing) and licensed under the Apache License, Version 2.0.
+
 #### Trademark Notice
 
 The word "QR Code" is registered trademark of *DENSO WAVE INCORPORATED*<br>

+ 10 - 2
composer.json

@@ -6,12 +6,20 @@
 	"minimum-stability": "stable",
 	"type": "library",
 	"keywords": [
-		"QR code", "qrcode", "qr", "qrcode-generator", "phpqrcode"
+		"QR code", "qrcode", "qr", "qrcode-generator", "phpqrcode", "qrcode-reader"
 	],
 	"authors": [
 		{
 			"name": "Kazuhiko Arase",
-			"homepage": "https://github.com/kazuhikoarase"
+			"homepage": "https://github.com/kazuhikoarase/qrcode-generator"
+		},
+		{
+			"name":"ZXing Authors",
+			"homepage": "https://github.com/zxing/zxing"
+		},
+		{
+			"name": "Ashot Khanamiryan",
+			"homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder"
 		},
 		{
 			"name": "Smiley",

+ 0 - 0
examples/example_image.png → docs/example_image.png


+ 0 - 0
examples/example_svg.png → docs/example_svg.png


+ 1 - 3
examples/MyCustomOutput.php

@@ -2,9 +2,7 @@
 /**
  * Class MyCustomOutput
  *
- * @filesource   MyCustomOutput.php
  * @created      24.12.2017
- * @package      chillerlan\QRCodeExamples
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -20,7 +18,7 @@ class MyCustomOutput extends QROutputAbstract{
 		// TODO: Implement setModuleValues() method.
 	}
 
-	public function dump(string $file = null){
+	public function dump(string $file = null):string{
 
 		$output = '';
 

+ 1 - 3
examples/QRImageWithLogo.php

@@ -2,9 +2,7 @@
 /**
  * Class QRImageWithLogo
  *
- * @filesource   QRImageWithLogo.php
  * @created      18.11.2020
- * @package      chillerlan\QRCodeExamples
  * @author       smiley <smiley@chillerlan.net>
  * @copyright    2020 smiley
  * @license      MIT
@@ -72,7 +70,7 @@ class QRImageWithLogo extends QRImage{
 		}
 
 		if($this->options->imageBase64){
-			$imageData = 'data:image/'.$this->options->outputType.';base64,'.base64_encode($imageData);
+			$imageData = $this->base64encode($imageData, 'image/'.$this->options->outputType);
 		}
 
 		return $imageData;

+ 2 - 4
examples/QRImageWithText.php

@@ -6,9 +6,7 @@
  *
  * @link         https://github.com/chillerlan/php-qrcode/issues/35
  *
- * @filesource   QRImageWithText.php
  * @created      22.06.2019
- * @package      chillerlan\QRCodeExamples
  * @author       smiley <smiley@chillerlan.net>
  * @copyright    2019 smiley
  * @license      MIT
@@ -20,7 +18,7 @@ namespace chillerlan\QRCodeExamples;
 
 use chillerlan\QRCode\Output\QRImage;
 
-use function base64_encode, imagechar, imagecolorallocate, imagecolortransparent, imagecopymerge, imagecreatetruecolor,
+use function imagechar, imagecolorallocate, imagecolortransparent, imagecopymerge, imagecreatetruecolor,
 	imagedestroy, imagefilledrectangle, imagefontwidth, in_array, round, str_split, strlen;
 
 class QRImageWithText extends QRImage{
@@ -50,7 +48,7 @@ class QRImageWithText extends QRImage{
 		}
 
 		if($this->options->imageBase64){
-			$imageData = 'data:image/'.$this->options->outputType.';base64,'.base64_encode($imageData);
+			$imageData = $this->base64encode($imageData, 'image/'.$this->options->outputType);
 		}
 
 		return $imageData;

+ 7 - 5
examples/custom_output.php

@@ -1,7 +1,5 @@
 <?php
 /**
- *
- * @filesource   custom_output.php
  * @created      24.12.2017
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
@@ -11,6 +9,7 @@
 namespace chillerlan\QRCodeExamples;
 
 use chillerlan\QRCode\{QRCode, QROptions};
+use chillerlan\QRCode\Common\EccLevel;
 
 require_once __DIR__.'/../vendor/autoload.php';
 
@@ -19,10 +18,13 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s';
 // invoke the QROutputInterface manually
 $options = new QROptions([
 	'version'      => 5,
-	'eccLevel'     => QRCode::ECC_L,
+	'eccLevel'     => EccLevel::L,
 ]);
 
-$qrOutputInterface = new MyCustomOutput($options, (new QRCode($options))->getMatrix($data));
+$qrcode = new QRCode($options);
+$qrcode->addByteSegment($data);
+
+$qrOutputInterface = new MyCustomOutput($options, $qrcode->getMatrix());
 
 var_dump($qrOutputInterface->dump());
 
@@ -30,7 +32,7 @@ var_dump($qrOutputInterface->dump());
 // or just
 $options = new QROptions([
 	'version'         => 5,
-	'eccLevel'        => QRCode::ECC_L,
+	'eccLevel'        => EccLevel::L,
 	'outputType'      => QRCode::OUTPUT_CUSTOM,
 	'outputInterface' => MyCustomOutput::class,
 ]);

+ 18 - 16
examples/fpdf.php

@@ -3,6 +3,8 @@
 namespace chillerlan\QRCodeExamples;
 
 use chillerlan\QRCode\{QRCode, QROptions};
+use chillerlan\QRCode\Data\QRMatrix;
+use chillerlan\QRCode\Common\EccLevel;
 
 require_once __DIR__ . '/../vendor/autoload.php';
 
@@ -11,34 +13,34 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s';
 $options = new QROptions([
     'version'      => 7,
     'outputType'   => QRCode::OUTPUT_FPDF,
-    'eccLevel'     => QRCode::ECC_L,
+    'eccLevel'     => EccLevel::L,
     'scale'        => 5,
     'imageBase64'  => false,
     'moduleValues' => [
         // finder
-        1536 => [0, 63, 255], // dark (true)
-        6    => [255, 255, 255], // light (false), white is the transparency color and is enabled by default
+        QRMatrix::M_FINDER | QRMatrix::IS_DARK     => [0, 63, 255], // dark (true)
+        QRMatrix::M_FINDER                         => [255, 255, 255], // light (false), white is the transparency color and is enabled by default
         // alignment
-        2560 => [255, 0, 255],
-        10   => [255, 255, 255],
+        QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK  => [255, 0, 255],
+        QRMatrix::M_ALIGNMENT                      => [255, 255, 255],
         // timing
-        3072 => [255, 0, 0],
-        12   => [255, 255, 255],
+        QRMatrix::M_TIMING | QRMatrix::IS_DARK     => [255, 0, 0],
+        QRMatrix::M_TIMING                         => [255, 255, 255],
         // format
-        3584 => [67, 191, 84],
-        14   => [255, 255, 255],
+        QRMatrix::M_FORMAT | QRMatrix::IS_DARK     => [67, 191, 84],
+        QRMatrix::M_FORMAT                         => [255, 255, 255],
         // version
-        4096 => [62, 174, 190],
-        16   => [255, 255, 255],
+        QRMatrix::M_VERSION | QRMatrix::IS_DARK    => [62, 174, 190],
+        QRMatrix::M_VERSION                        => [255, 255, 255],
         // data
-        1024 => [0, 0, 0],
-        4    => [255, 255, 255],
+        QRMatrix::M_DATA | QRMatrix::IS_DARK       => [0, 0, 0],
+        QRMatrix::M_DATA                           => [255, 255, 255],
         // darkmodule
-        512  => [0, 0, 0],
+        QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => [0, 0, 0],
         // separator
-        8    => [255, 255, 255],
+        QRMatrix::M_SEPARATOR                      => [255, 255, 255],
         // quietzone
-        18   => [255, 255, 255],
+        QRMatrix::M_QUIETZONE                      => [255, 255, 255],
     ],
 ]);
 

+ 23 - 35
examples/html.php

@@ -1,7 +1,5 @@
 <?php
 /**
- *
- * @filesource   html.php
  * @created      21.12.2017
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
@@ -11,6 +9,8 @@
 namespace chillerlan\QRCodeExamples;
 
 use chillerlan\QRCode\{QRCode, QROptions};
+use chillerlan\QRCode\Data\QRMatrix;
+use chillerlan\QRCode\Common\EccLevel;
 
 require_once '../vendor/autoload.php';
 
@@ -24,20 +24,12 @@ header('Content-Type: text/html; charset=utf-8');
 	<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
 	<title>QRCode test</title>
 	<style>
-		body{
-			margin: 5em;
-			padding: 0;
-		}
-
 		div.qrcode{
-			margin: 0;
-			padding: 0;
+            margin: 5em;
 		}
 
 		/* rows */
 		div.qrcode > div {
-			margin: 0;
-			padding: 0;
 			height: 10px;
 		}
 
@@ -47,54 +39,50 @@ header('Content-Type: text/html; charset=utf-8');
 			width: 10px;
 			height: 10px;
 		}
-
-		div.qrcode > div > span {
-			background-color: #ccc;
-		}
 	</style>
 </head>
 <body>
-	<div class="qrcode">
 <?php
 
 	$data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s';
 
 	$options = new QROptions([
-		'version' => 5,
-		'outputType' => QRCode::OUTPUT_MARKUP_HTML,
-		'eccLevel' => QRCode::ECC_L,
+		'version'      => 5,
+		'outputType'   => QRCode::OUTPUT_MARKUP_HTML,
+		'eccLevel'     => EccLevel::L,
+		'cssClass'     => 'qrcode',
 		'moduleValues' => [
 			// finder
-			1536 => '#A71111', // dark (true)
-			6    => '#FFBFBF', // light (false)
+			QRMatrix::M_FINDER | QRMatrix::IS_DARK     => '#A71111', // dark (true)
+			QRMatrix::M_FINDER                         => '#FFBFBF', // light (false)
+			QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => '#A71111', // finder dot, dark (true)
 			// alignment
-			2560 => '#A70364',
-			10   => '#FFC9C9',
+			QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK  => '#A70364',
+			QRMatrix::M_ALIGNMENT                      => '#FFC9C9',
 			// timing
-			3072 => '#98005D',
-			12   => '#FFB8E9',
+			QRMatrix::M_TIMING | QRMatrix::IS_DARK     => '#98005D',
+			QRMatrix::M_TIMING                         => '#FFB8E9',
 			// format
-			3584 => '#003804',
-			14   => '#00FB12',
+			QRMatrix::M_FORMAT | QRMatrix::IS_DARK     => '#003804',
+			QRMatrix::M_FORMAT                         => '#00FB12',
 			// version
-			4096 => '#650098',
-			16   => '#E0B8FF',
+			QRMatrix::M_VERSION | QRMatrix::IS_DARK    => '#650098',
+			QRMatrix::M_VERSION                        => '#E0B8FF',
 			// data
-			1024 => '#4A6000',
-			4    => '#ECF9BE',
+			QRMatrix::M_DATA | QRMatrix::IS_DARK       => '#4A6000',
+			QRMatrix::M_DATA                           => '#ECF9BE',
 			// darkmodule
-			512  => '#080063',
+			QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => '#080063',
 			// separator
-			8    => '#AFBFBF',
+			QRMatrix::M_SEPARATOR                      => '#AFBFBF',
 			// quietzone
-			18   => '#FFFFFF',
+			QRMatrix::M_QUIETZONE                      => '#DDDDDD',
 		],
 	]);
 
 	echo (new QRCode($options))->render($data);
 
 ?>
-	</div>
 </body>
 </html>
 

+ 22 - 22
examples/image.php

@@ -1,7 +1,5 @@
 <?php
 /**
- *
- * @filesource   image.php
  * @created      24.12.2017
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
@@ -11,45 +9,47 @@
 namespace chillerlan\QRCodeExamples;
 
 use chillerlan\QRCode\{QRCode, QROptions};
+use chillerlan\QRCode\Data\QRMatrix;
+use chillerlan\QRCode\Common\EccLevel;
 
 require_once __DIR__.'/../vendor/autoload.php';
 
 $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s';
 
 $options = new QROptions([
-	'version'      => 10,
+	'version'      => 5,
 	'outputType'   => QRCode::OUTPUT_IMAGE_PNG,
-	'eccLevel'     => QRCode::ECC_H,
+	'eccLevel'     => EccLevel::L,
 	'scale'        => 5,
 	'imageBase64'  => false,
 	'moduleValues' => [
 		// finder
-		1536 => [0, 63, 255], // dark (true)
-		6    => [255, 255, 255], // light (false), white is the transparency color and is enabled by default
-		5632 => [241, 28, 163], // finder dot, dark (true)
+		QRMatrix::M_FINDER | QRMatrix::IS_DARK     => [0, 63, 255], // dark (true)
+		QRMatrix::M_FINDER                         => [255, 255, 255], // light (false), white is the transparency color and is enabled by default
+		QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => [241, 28, 163], // finder dot, dark (true)
 		// alignment
-		2560 => [255, 0, 255],
-		10   => [255, 255, 255],
+		QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK  => [255, 0, 255],
+		QRMatrix::M_ALIGNMENT                      => [255, 255, 255],
 		// timing
-		3072 => [255, 0, 0],
-		12   => [255, 255, 255],
+		QRMatrix::M_TIMING | QRMatrix::IS_DARK     => [255, 0, 0],
+		QRMatrix::M_TIMING                         => [255, 255, 255],
 		// format
-		3584 => [67, 99, 84],
-		14   => [255, 255, 255],
+		QRMatrix::M_FORMAT | QRMatrix::IS_DARK     => [67, 99, 84],
+		QRMatrix::M_FORMAT                         => [255, 255, 255],
 		// version
-		4096 => [62, 174, 190],
-		16   => [255, 255, 255],
+		QRMatrix::M_VERSION | QRMatrix::IS_DARK    => [62, 174, 190],
+		QRMatrix::M_VERSION                        => [255, 255, 255],
 		// data
-		1024 => [0, 0, 0],
-		4    => [255, 255, 255],
+		QRMatrix::M_DATA | QRMatrix::IS_DARK       => [0, 0, 0],
+		QRMatrix::M_DATA                           => [255, 255, 255],
 		// darkmodule
-		512  => [0, 0, 0],
+		QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => [0, 0, 0],
 		// separator
-		8    => [255, 255, 255],
+		QRMatrix::M_SEPARATOR                      => [255, 255, 255],
 		// quietzone
-		18   => [255, 255, 255],
-		// logo (requires a call to QRMatrix::setLogoSpace())
-		20    => [255, 255, 255],
+		QRMatrix::M_QUIETZONE                      => [255, 255, 255],
+		// logo (requires a call to QRMatrix::setLogoSpace()), see QRImageWithLogo
+		QRMatrix::M_LOGO                           => [255, 255, 255],
 	],
 ]);
 

+ 7 - 5
examples/imageWithLogo.php

@@ -1,7 +1,5 @@
 <?php
 /**
- *
- * @filesource   imageWithLogo.php
  * @created      18.11.2020
  * @author       smiley <smiley@chillerlan.net>
  * @copyright    2020 smiley
@@ -11,6 +9,7 @@
 namespace chillerlan\QRCodeExamples;
 
 use chillerlan\QRCode\{QRCode, QROptions};
+use chillerlan\QRCode\Common\EccLevel;
 
 require_once __DIR__.'/../vendor/autoload.php';
 
@@ -30,16 +29,19 @@ class LogoOptions extends QROptions{
 $options = new LogoOptions;
 
 $options->version          = 7;
-$options->eccLevel         = QRCode::ECC_H;
+$options->eccLevel         = EccLevel::H;
 $options->imageBase64      = false;
 $options->logoSpaceWidth   = 13;
-$options->logoSpaceHeight  = 13;
+$options->logoSpaceWidth   = 13;
 $options->scale            = 5;
 $options->imageTransparent = false;
 
+$qrcode = new QRCode($options);
+$qrcode->addByteSegment($data);
+
 header('Content-type: image/png');
 
-$qrOutputInterface = new QRImageWithLogo($options, (new QRCode($options))->getMatrix($data));
+$qrOutputInterface = new QRImageWithLogo($options, $qrcode->getMatrix());
 
 // dump the output, with an additional logo
 echo $qrOutputInterface->dump(null, __DIR__.'/octocat.png');

+ 4 - 2
examples/imageWithText.php

@@ -3,7 +3,6 @@
  * example for additional text
  * @link https://github.com/chillerlan/php-qrcode/issues/35
  *
- * @filesource   imageWithText.php
  * @created      22.06.2019
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2019 Smiley
@@ -25,9 +24,12 @@ $options = new QROptions([
 	'imageBase64'  => false,
 ]);
 
+$qrcode = new QRCode($options);
+$qrcode->addByteSegment($data);
+
 header('Content-type: image/png');
 
-$qrOutputInterface = new QRImageWithText($options, (new QRCode($options))->getMatrix($data));
+$qrOutputInterface = new QRImageWithText($options, $qrcode->getMatrix());
 
 // dump the output, with additional text
 echo $qrOutputInterface->dump(null, 'example text');

+ 19 - 18
examples/imagick.php

@@ -1,7 +1,5 @@
 <?php
 /**
- *
- * @filesource   image.php
  * @created      24.12.2017
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
@@ -11,6 +9,8 @@
 namespace chillerlan\QRCodeExamples;
 
 use chillerlan\QRCode\{QRCode, QROptions};
+use chillerlan\QRCode\Data\QRMatrix;
+use chillerlan\QRCode\Common\EccLevel;
 
 require_once __DIR__.'/../vendor/autoload.php';
 
@@ -19,33 +19,34 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s';
 $options = new QROptions([
 	'version'      => 7,
 	'outputType'   => QRCode::OUTPUT_IMAGICK,
-	'eccLevel'     => QRCode::ECC_L,
+	'eccLevel'     => EccLevel::L,
 	'scale'        => 5,
 	'moduleValues' => [
 		// finder
-		1536 => '#A71111', // dark (true)
-		6    => '#FFBFBF', // light (false)
+		QRMatrix::M_FINDER | QRMatrix::IS_DARK     => '#A71111', // dark (true)
+		QRMatrix::M_FINDER                         => '#FFBFBF', // light (false)
+		QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => '#A71111', // finder dot, dark (true)
 		// alignment
-		2560 => '#A70364',
-		10   => '#FFC9C9',
+		QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK  => '#A70364',
+		QRMatrix::M_ALIGNMENT                      => '#FFC9C9',
 		// timing
-		3072 => '#98005D',
-		12   => '#FFB8E9',
+		QRMatrix::M_TIMING | QRMatrix::IS_DARK     => '#98005D',
+		QRMatrix::M_TIMING                         => '#FFB8E9',
 		// format
-		3584 => '#003804',
-		14   => '#00FB12',
+		QRMatrix::M_FORMAT | QRMatrix::IS_DARK     => '#003804',
+		QRMatrix::M_FORMAT                         => '#00FB12',
 		// version
-		4096 => '#650098',
-		16   => '#E0B8FF',
+		QRMatrix::M_VERSION | QRMatrix::IS_DARK    => '#650098',
+		QRMatrix::M_VERSION                        => '#E0B8FF',
 		// data
-		1024 => '#4A6000',
-		4    => '#ECF9BE',
+		QRMatrix::M_DATA | QRMatrix::IS_DARK       => '#4A6000',
+		QRMatrix::M_DATA                           => '#ECF9BE',
 		// darkmodule
-		512  => '#080063',
+		QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => '#080063',
 		// separator
-		8    => '#DDDDDD',
+		QRMatrix::M_SEPARATOR                      => '#DDDDDD',
 		// quietzone
-		18   => '#DDDDDD',
+		QRMatrix::M_QUIETZONE                      => '#DDDDDD',
 	],
 ]);
 

+ 27 - 26
examples/svg.php

@@ -1,7 +1,5 @@
 <?php
 /**
- *
- * @filesource   svg.php
  * @created      21.12.2017
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
@@ -11,6 +9,8 @@
 namespace chillerlan\QRCodeExamples;
 
 use chillerlan\QRCode\{QRCode, QROptions};
+use chillerlan\QRCode\Data\QRMatrix;
+use chillerlan\QRCode\Common\EccLevel;
 
 require_once __DIR__.'/../vendor/autoload.php';
 
@@ -18,15 +18,15 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s';
 $gzip = true;
 
 $options = new QROptions([
-	'version'      => 7,
-	'outputType'   => QRCode::OUTPUT_MARKUP_SVG,
-	'imageBase64'  => false,
-	'eccLevel'     => QRCode::ECC_L,
+	'version'        => 7,
+	'outputType'     => QRCode::OUTPUT_MARKUP_SVG,
+	'imageBase64'    => false,
+	'eccLevel'       => EccLevel::L,
 	'svgViewBoxSize' => 530,
-	'addQuietzone' => true,
-	'cssClass'     => 'my-css-class',
-	'svgOpacity'   => 1.0,
-	'svgDefs'      => '
+	'addQuietzone'   => true,
+	'cssClass'       => 'my-css-class',
+	'svgOpacity'     => 1.0,
+	'svgDefs'        => '
 		<linearGradient id="g2">
 			<stop offset="0%" stop-color="#39F" />
 			<stop offset="100%" stop-color="#F3F" />
@@ -38,29 +38,30 @@ $options = new QROptions([
 		<style>rect{shape-rendering:crispEdges}</style>',
 	'moduleValues' => [
 		// finder
-		1536 => 'url(#g1)', // dark (true)
-		6    => '#fff', // light (false)
+		QRMatrix::M_FINDER | QRMatrix::IS_DARK     => 'url(#g1)', // dark (true)
+		QRMatrix::M_FINDER                         => '#fff',     // light (false)
+		QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => 'url(#g2)', // finder dot, dark (true)
 		// alignment
-		2560 => 'url(#g1)',
-		10   => '#fff',
+		QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK  => 'url(#g1)',
+		QRMatrix::M_ALIGNMENT                      => '#fff',
 		// timing
-		3072 => 'url(#g1)',
-		12   => '#fff',
+		QRMatrix::M_TIMING | QRMatrix::IS_DARK     => 'url(#g1)',
+		QRMatrix::M_TIMING                         => '#fff',
 		// format
-		3584 => 'url(#g1)',
-		14   => '#fff',
+		QRMatrix::M_FORMAT | QRMatrix::IS_DARK     => 'url(#g1)',
+		QRMatrix::M_FORMAT                         => '#fff',
 		// version
-		4096 => 'url(#g1)',
-		16   => '#fff',
+		QRMatrix::M_VERSION | QRMatrix::IS_DARK    => 'url(#g1)',
+		QRMatrix::M_VERSION                        => '#fff',
 		// data
-		1024 => 'url(#g2)',
-		4    => '#fff',
+		QRMatrix::M_DATA | QRMatrix::IS_DARK       => 'url(#g2)',
+		QRMatrix::M_DATA                           => '#fff',
 		// darkmodule
-		512  => 'url(#g1)',
+		QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => 'url(#g1)',
 		// separator
-		8    => '#fff',
+		QRMatrix::M_SEPARATOR                      => '#fff',
 		// quietzone
-		18   => '#fff',
+		QRMatrix::M_QUIETZONE                      => '#fff',
 	],
 ]);
 
@@ -71,7 +72,7 @@ header('Content-type: image/svg+xml');
 if($gzip === true){
 	header('Vary: Accept-Encoding');
 	header('Content-Encoding: gzip');
-	$qrcode = gzencode($qrcode ,9);
+	$qrcode = gzencode($qrcode, 9);
 }
 echo $qrcode;
 

+ 20 - 19
examples/text.php

@@ -1,7 +1,5 @@
 <?php
 /**
- *
- * @filesource   text.php
  * @created      21.12.2017
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
@@ -11,6 +9,8 @@
 namespace chillerlan\QRCodeExamples;
 
 use chillerlan\QRCode\{QRCode, QROptions};
+use chillerlan\QRCode\Data\QRMatrix;
+use chillerlan\QRCode\Common\EccLevel;
 
 require_once __DIR__.'/../vendor/autoload.php';
 
@@ -19,7 +19,7 @@ $data = 'https://www.youtube.com/watch?v=DLzxrzFCyOs&t=43s';
 $options = new QROptions([
 	'version'      => 5,
 	'outputType'   => QRCode::OUTPUT_STRING_TEXT,
-	'eccLevel'     => QRCode::ECC_L,
+	'eccLevel'     => EccLevel::L,
 ]);
 
 // <pre> to view it in a browser
@@ -30,32 +30,33 @@ echo '<pre style="font-size: 75%; line-height: 1;">'.(new QRCode($options))->ren
 $options = new QROptions([
 	'version'      => 5,
 	'outputType'   => QRCode::OUTPUT_STRING_TEXT,
-	'eccLevel'     => QRCode::ECC_L,
+	'eccLevel'     => EccLevel::L,
 	'moduleValues' => [
 		// finder
-		1536 => 'A', // dark (true)
-		6    => 'a', // light (false)
+		QRMatrix::M_FINDER | QRMatrix::IS_DARK     => 'A', // dark (true)
+		QRMatrix::M_FINDER                         => 'a', // light (false)
+		QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => 'ä', // finder dot, dark (true)
 		// alignment
-		2560 => 'B',
-		10   => 'b',
+		QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK  => 'B',
+		QRMatrix::M_ALIGNMENT                      => 'b',
 		// timing
-		3072 => 'C',
-		12   => 'c',
+		QRMatrix::M_TIMING | QRMatrix::IS_DARK     => 'C',
+		QRMatrix::M_TIMING                         => 'c',
 		// format
-		3584 => 'D',
-		14   => 'd',
+		QRMatrix::M_FORMAT | QRMatrix::IS_DARK     => 'D',
+		QRMatrix::M_FORMAT                         => 'd',
 		// version
-		4096 => 'E',
-		16   => 'e',
+		QRMatrix::M_VERSION | QRMatrix::IS_DARK    => 'E',
+		QRMatrix::M_VERSION                        => 'e',
 		// data
-		1024 => 'F',
-		4    => 'f',
+		QRMatrix::M_DATA | QRMatrix::IS_DARK       => 'F',
+		QRMatrix::M_DATA                           => 'f',
 		// darkmodule
-		512  => 'G',
+		QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => 'G',
 		// separator
-		8    => 'h',
+		QRMatrix::M_SEPARATOR                      => 'h',
 		// quietzone
-		18   => 'i',
+		QRMatrix::M_QUIETZONE                      => 'i',
 	],
 ]);
 

+ 0 - 1
public/qrcode.php

@@ -1,6 +1,5 @@
 <?php
 /**
- * @filesource   qrcode.php
  * @created      18.11.2017
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley

+ 160 - 0
src/Common/BitBuffer.php

@@ -0,0 +1,160 @@
+<?php
+/**
+ * Class BitBuffer
+ *
+ * @created      25.11.2015
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2015 Smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use InvalidArgumentException;
+use function count, floor;
+
+/**
+ * Holds the raw binary data
+ */
+final class BitBuffer{
+
+	/**
+	 * The buffer content
+	 *
+	 * @var int[]
+	 */
+	private array $buffer;
+
+	/**
+	 * Length of the content (bits)
+	 */
+	private int $length;
+
+	private int $bytesRead = 0;
+	private int $bitsRead  = 0;
+
+	/**
+	 * BitBuffer constructor.
+	 */
+	public function __construct(array $bytes = null){
+		$this->buffer = $bytes ?? [];
+		$this->length = count($this->buffer);
+	}
+
+	/**
+	 * clears the buffer
+	 */
+	public function clear():BitBuffer{
+		$this->buffer = [];
+		$this->length = 0;
+
+		return $this;
+	}
+
+	/**
+	 * appends a sequence of bits
+	 */
+	public function put(int $num, int $length):BitBuffer{
+
+		for($i = 0; $i < $length; $i++){
+			$this->putBit((($num >> ($length - $i - 1)) & 1) === 1);
+		}
+
+		return $this;
+	}
+
+	/**
+	 * appends a single bit
+	 */
+	public function putBit(bool $bit):BitBuffer{
+		$bufIndex = (int)floor($this->length / 8);
+
+		if(count($this->buffer) <= $bufIndex){
+			$this->buffer[] = 0;
+		}
+
+		if($bit === true){
+			$this->buffer[$bufIndex] |= (0x80 >> ($this->length % 8));
+		}
+
+		$this->length++;
+
+		return $this;
+	}
+
+	/**
+	 * returns the current buffer length
+	 */
+	public function getLength():int{
+		return $this->length;
+	}
+
+	/**
+	 * returns the buffer content
+	 */
+	public function getBuffer():array{
+		return $this->buffer;
+	}
+
+	/**
+	 * @return int number of bits that can be read successfully
+	 */
+	public function available():int{
+		return 8 * ($this->length - $this->bytesRead) - $this->bitsRead;
+	}
+
+	/**
+	 * @author Sean Owen, ZXing
+	 *
+	 * @param int $numBits number of bits to read
+	 *
+	 * @return int representing the bits read. The bits will appear as the least-significant
+	 *         bits of the int
+	 * @throws InvalidArgumentException if numBits isn't in [1,32] or more than is available
+	 */
+	public function read(int $numBits):int{
+
+		if($numBits < 1 || $numBits > 32 || $numBits > $this->available()){
+			throw new InvalidArgumentException('invalid $numBits: '.$numBits);
+		}
+
+		$result = 0;
+
+		// First, read remainder from current byte
+		if($this->bitsRead > 0){
+			$bitsLeft       = 8 - $this->bitsRead;
+			$toRead         = $numBits < $bitsLeft ? $numBits : $bitsLeft;
+			$bitsToNotRead  = $bitsLeft - $toRead;
+			$mask           = (0xff >> (8 - $toRead)) << $bitsToNotRead;
+			$result         = ($this->buffer[$this->bytesRead] & $mask) >> $bitsToNotRead;
+			$numBits        -= $toRead;
+			$this->bitsRead += $toRead;
+
+			if($this->bitsRead == 8){
+				$this->bitsRead = 0;
+				$this->bytesRead++;
+			}
+		}
+
+		// Next read whole bytes
+		if($numBits > 0){
+
+			while($numBits >= 8){
+				$result = ($result << 8) | ($this->buffer[$this->bytesRead] & 0xff);
+				$this->bytesRead++;
+				$numBits -= 8;
+			}
+
+			// Finally read a partial byte
+			if($numBits > 0){
+				$bitsToNotRead  = 8 - $numBits;
+				$mask           = (0xff >> $bitsToNotRead) << $bitsToNotRead;
+				$result         = ($result << $numBits) | (($this->buffer[$this->bytesRead] & $mask) >> $bitsToNotRead);
+				$this->bitsRead += $numBits;
+			}
+		}
+
+		return $result;
+	}
+
+}

+ 109 - 0
src/Common/ECICharset.php

@@ -0,0 +1,109 @@
+<?php
+/**
+ * Class ECICharset
+ *
+ * @created      21.01.2021
+ * @author       smiley <smiley@chillerlan.net>
+ * @copyright    2021 smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use InvalidArgumentException;
+use function array_key_exists;
+
+/**
+ *
+ */
+final class ECICharset{
+
+	public const CP437                 = 0;  // Code page 437, DOS Latin US
+	public const ISO_IEC_8859_1_GLI    = 1;  // GLI encoding with characters 0 to 127 identical to ISO/IEC 646 and characters 128 to 255 identical to ISO 8859-1
+	public const CP437_WO_GLI          = 2;  // An equivalent code table to CP437, without the return-to-GLI 0 logic
+	public const ISO_IEC_8859_1        = 3;  // Latin-1 (Default)
+	public const ISO_IEC_8859_2        = 4;  // Latin-2
+	public const ISO_IEC_8859_3        = 5;  // Latin-3
+	public const ISO_IEC_8859_4        = 6;  // Latin-4
+	public const ISO_IEC_8859_5        = 7;  // Latin/Cyrillic
+	public const ISO_IEC_8859_6        = 8;  // Latin/Arabic
+	public const ISO_IEC_8859_7        = 9;  // Latin/Greek
+	public const ISO_IEC_8859_8        = 10; // Latin/Hebrew
+	public const ISO_IEC_8859_9        = 11; // Latin-5
+	public const ISO_IEC_8859_10       = 12; // Latin-6
+	public const ISO_IEC_8859_11       = 13; // Latin/Thai
+	// 14 reserved
+	public const ISO_IEC_8859_13       = 15; // Latin-7 (Baltic Rim)
+	public const ISO_IEC_8859_14       = 16; // Latin-8 (Celtic)
+	public const ISO_IEC_8859_15       = 17; // Latin-9
+	public const ISO_IEC_8859_16       = 18; // Latin-10
+	// 19 reserved
+	public const SHIFT_JIS             = 20; // JIS X 0208 Annex 1 + JIS X 0201
+	public const WINDOWS_1250_LATIN_2  = 21; // Superset of Latin-2, Central Europe
+	public const WINDOWS_1251_CYRILLIC = 22; // Latin/Cyrillic
+	public const WINDOWS_1252_LATIN_1  = 23; // Superset of Latin-1
+	public const WINDOWS_1256_ARABIC   = 24;
+	public const ISO_IEC_10646_UCS_2   = 25; // High order byte first (UTF-16BE)
+	public const ISO_IEC_10646_UTF_8   = 26; // UTF-8
+	public const ISO_IEC_646_1991      = 27; // International Reference Version of ISO 7-bit coded character set (US-ASCII)
+	public const BIG5                  = 28; // Big 5 (Taiwan) Chinese Character Set
+	public const GB18030               = 29; // GB (PRC) Chinese Character Set
+	public const EUC_KR                = 30; // Korean Character Set
+
+	/**
+	 * map of charset id -> name
+	 *
+	 * @see \mb_list_encodings()
+	 */
+	public const MB_ENCODINGS = [
+		self::CP437                 => null,
+		self::ISO_IEC_8859_1_GLI    => null,
+		self::CP437_WO_GLI          => null,
+		self::ISO_IEC_8859_1        => 'ISO-8859-1',
+		self::ISO_IEC_8859_2        => 'ISO-8859-2',
+		self::ISO_IEC_8859_3        => 'ISO-8859-3',
+		self::ISO_IEC_8859_4        => 'ISO-8859-4',
+		self::ISO_IEC_8859_5        => 'ISO-8859-5',
+		self::ISO_IEC_8859_6        => 'ISO-8859-6',
+		self::ISO_IEC_8859_7        => 'ISO-8859-7',
+		self::ISO_IEC_8859_8        => 'ISO-8859-8',
+		self::ISO_IEC_8859_9        => 'ISO-8859-9',
+		self::ISO_IEC_8859_10       => 'ISO-8859-10',
+		self::ISO_IEC_8859_11       => null,
+		self::ISO_IEC_8859_13       => 'ISO-8859-13',
+		self::ISO_IEC_8859_14       => 'ISO-8859-14',
+		self::ISO_IEC_8859_15       => 'ISO-8859-15',
+		self::ISO_IEC_8859_16       => 'ISO-8859-16',
+		self::SHIFT_JIS             => 'SJIS',
+		self::WINDOWS_1250_LATIN_2  => null, // @see https://www.php.net/manual/en/function.mb-convert-encoding.php#112547
+		self::WINDOWS_1251_CYRILLIC => 'Windows-1251',
+		self::WINDOWS_1252_LATIN_1  => 'Windows-1252',
+		self::WINDOWS_1256_ARABIC   => null, // @see https://stackoverflow.com/a/8592995
+		self::ISO_IEC_10646_UCS_2   => 'UTF-16BE',
+		self::ISO_IEC_10646_UTF_8   => 'UTF-8',
+		self::ISO_IEC_646_1991      => 'ASCII',
+		self::BIG5                  => 'BIG-5',
+		self::GB18030               => 'GB18030',
+		self::EUC_KR                => 'EUC-KR',
+	];
+
+	private int $charsetID;
+
+	public function __construct(int $charsetID){
+
+		if(!array_key_exists($charsetID, self::MB_ENCODINGS)){
+			throw new InvalidArgumentException('invalid charset id: '.$charsetID);
+		}
+
+		$this->charsetID = $charsetID;
+	}
+
+	public function getID():int{
+		return $this->charsetID;
+	}
+
+	public function getName():?string{
+		return self::MB_ENCODINGS[$this->charsetID];
+	}
+
+}

+ 209 - 0
src/Common/EccLevel.php

@@ -0,0 +1,209 @@
+<?php
+/**
+ * Class EccLevel
+ *
+ * @created      19.11.2020
+ * @author       smiley <smiley@chillerlan.net>
+ * @copyright    2020 smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use chillerlan\QRCode\QRCodeException;
+
+use function array_column, array_combine, array_keys;
+
+/**
+ *
+ */
+final class EccLevel{
+
+	// ISO/IEC 18004:2000 Tables 12, 25
+
+	/** @var int */
+	public const L = 0b01; // 7%.
+	/** @var int */
+	public const M = 0b00; // 15%.
+	/** @var int */
+	public const Q = 0b11; // 25%.
+	/** @var int */
+	public const H = 0b10; // 30%.
+
+	/**
+	 * References to the keys of the following tables:
+	 *
+	 * @see \chillerlan\QRCode\Common\Version::MAX_BITS
+	 * @see \chillerlan\QRCode\Common\EccLevel::RSBLOCKS
+	 * @see \chillerlan\QRCode\Common\EccLevel::formatPattern
+	 *
+	 * @var int[]
+	 */
+	public const MODES = [
+		self::L => 0,
+		self::M => 1,
+		self::Q => 2,
+		self::H => 3,
+	];
+
+	public const MODES_STRING = [
+		self::L => 'L',
+		self::M => 'M',
+		self::Q => 'Q',
+		self::H => 'H',
+	];
+
+	/**
+	 * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
+	 *
+	 * @var int [][]
+	 */
+	private const MAX_BITS = [
+	//  v  => [    L,     M,     Q,     H]  // modules
+		1  => [  152,   128,   104,    72], //  21
+		2  => [  272,   224,   176,   128], //  25
+		3  => [  440,   352,   272,   208], //  29
+		4  => [  640,   512,   384,   288], //  33
+		5  => [  864,   688,   496,   368], //  37
+		6  => [ 1088,   864,   608,   480], //  41
+		7  => [ 1248,   992,   704,   528], //  45
+		8  => [ 1552,  1232,   880,   688], //  49
+		9  => [ 1856,  1456,  1056,   800], //  53
+		10 => [ 2192,  1728,  1232,   976], //  57
+		11 => [ 2592,  2032,  1440,  1120], //  61
+		12 => [ 2960,  2320,  1648,  1264], //  65
+		13 => [ 3424,  2672,  1952,  1440], //  69 NICE!
+		14 => [ 3688,  2920,  2088,  1576], //  73
+		15 => [ 4184,  3320,  2360,  1784], //  77
+		16 => [ 4712,  3624,  2600,  2024], //  81
+		17 => [ 5176,  4056,  2936,  2264], //  85
+		18 => [ 5768,  4504,  3176,  2504], //  89
+		19 => [ 6360,  5016,  3560,  2728], //  93
+		20 => [ 6888,  5352,  3880,  3080], //  97
+		21 => [ 7456,  5712,  4096,  3248], // 101
+		22 => [ 8048,  6256,  4544,  3536], // 105
+		23 => [ 8752,  6880,  4912,  3712], // 109
+		24 => [ 9392,  7312,  5312,  4112], // 113
+		25 => [10208,  8000,  5744,  4304], // 117
+		26 => [10960,  8496,  6032,  4768], // 121
+		27 => [11744,  9024,  6464,  5024], // 125
+		28 => [12248,  9544,  6968,  5288], // 129
+		29 => [13048, 10136,  7288,  5608], // 133
+		30 => [13880, 10984,  7880,  5960], // 137
+		31 => [14744, 11640,  8264,  6344], // 141
+		32 => [15640, 12328,  8920,  6760], // 145
+		33 => [16568, 13048,  9368,  7208], // 149
+		34 => [17528, 13800,  9848,  7688], // 153
+		35 => [18448, 14496, 10288,  7888], // 157
+		36 => [19472, 15312, 10832,  8432], // 161
+		37 => [20528, 15936, 11408,  8768], // 165
+		38 => [21616, 16816, 12016,  9136], // 169
+		39 => [22496, 17728, 12656,  9776], // 173
+		40 => [23648, 18672, 13328, 10208], // 177
+	];
+
+	/**
+	 * ISO/IEC 18004:2000 Section 8.9 - Format Information
+	 *
+	 * ECC level -> mask pattern
+	 *
+	 * @var int[][]
+	 */
+	private const FORMAT_PATTERN = [
+		[ // L
+		  0b111011111000100,
+		  0b111001011110011,
+		  0b111110110101010,
+		  0b111100010011101,
+		  0b110011000101111,
+		  0b110001100011000,
+		  0b110110001000001,
+		  0b110100101110110,
+		],
+		[ // M
+		  0b101010000010010,
+		  0b101000100100101,
+		  0b101111001111100,
+		  0b101101101001011,
+		  0b100010111111001,
+		  0b100000011001110,
+		  0b100111110010111,
+		  0b100101010100000,
+		],
+		[ // Q
+		  0b011010101011111,
+		  0b011000001101000,
+		  0b011111100110001,
+		  0b011101000000110,
+		  0b010010010110100,
+		  0b010000110000011,
+		  0b010111011011010,
+		  0b010101111101101,
+		],
+		[ // H
+		  0b001011010001001,
+		  0b001001110111110,
+		  0b001110011100111,
+		  0b001100111010000,
+		  0b000011101100010,
+		  0b000001001010101,
+		  0b000110100001100,
+		  0b000100000111011,
+		],
+	];
+
+	private int $eccLevel;
+
+	/**
+	 * @param int $eccLevel containing the two bits encoding a QR Code's error correction level
+	 *
+	 * @throws \chillerlan\QRCode\QRCodeException
+	 */
+	public function __construct(int $eccLevel){
+
+		if((0b11 & $eccLevel) !== $eccLevel){
+			throw new QRCodeException('invalid ECC level');
+		}
+
+		$this->eccLevel = $eccLevel;
+	}
+
+	/**
+	 * returns the string representation of the current ECC level
+	 */
+	public function __toString():string{
+		return self::MODES_STRING[$this->eccLevel];
+	}
+
+	/**
+	 * returns the current ECC level
+	 */
+	public function getLevel():int{
+		return $this->eccLevel;
+	}
+
+	/**
+	 * returns the ordinal value of the current ECC level
+	 */
+	public function getOrdinal():int{
+		return self::MODES[$this->eccLevel];
+	}
+
+	/**
+	 * returns the format pattern for the given $eccLevel and $maskPattern
+	 */
+	public function getformatPattern(MaskPattern $maskPattern):int{
+		return self::FORMAT_PATTERN[self::MODES[$this->eccLevel]][$maskPattern->getPattern()];
+	}
+
+	/**
+	 * returns an array wit the max bit lengths for version 1-40 and the current ECC level
+	 */
+	public function getMaxBits():array{
+		return array_combine(
+			array_keys(self::MAX_BITS),
+			array_column(self::MAX_BITS, self::MODES[$this->eccLevel])
+		);
+	}
+
+}

+ 82 - 0
src/Common/FormatInformation.php

@@ -0,0 +1,82 @@
+<?php
+/**
+ * Class FormatInformation
+ *
+ * @created      24.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Common;
+
+/**
+ * <p>Encapsulates a QR Code's format information, including the data mask used and
+ * error correction level.</p>
+ *
+ * @author Sean Owen
+ * @see    \chillerlan\QRCode\Common\ErrorCorrectionLevel
+ */
+final class FormatInformation{
+
+	public const MASK_QR = 0x5412;
+
+	/**
+	 * See ISO 18004:2006, Annex C, Table C.1
+	 *
+	 * [data bits, sequence after masking]
+	 */
+	public const DECODE_LOOKUP = [
+		[0x00, 0x5412],
+		[0x01, 0x5125],
+		[0x02, 0x5E7C],
+		[0x03, 0x5B4B],
+		[0x04, 0x45F9],
+		[0x05, 0x40CE],
+		[0x06, 0x4F97],
+		[0x07, 0x4AA0],
+		[0x08, 0x77C4],
+		[0x09, 0x72F3],
+		[0x0A, 0x7DAA],
+		[0x0B, 0x789D],
+		[0x0C, 0x662F],
+		[0x0D, 0x6318],
+		[0x0E, 0x6C41],
+		[0x0F, 0x6976],
+		[0x10, 0x1689],
+		[0x11, 0x13BE],
+		[0x12, 0x1CE7],
+		[0x13, 0x19D0],
+		[0x14, 0x0762],
+		[0x15, 0x0255],
+		[0x16, 0x0D0C],
+		[0x17, 0x083B],
+		[0x18, 0x355F],
+		[0x19, 0x3068],
+		[0x1A, 0x3F31],
+		[0x1B, 0x3A06],
+		[0x1C, 0x24B4],
+		[0x1D, 0x2183],
+		[0x1E, 0x2EDA],
+		[0x1F, 0x2BED],
+	];
+
+	private int $errorCorrectionLevel;
+	private int $dataMask;
+
+	public function __construct(int $formatInfo){
+		$this->errorCorrectionLevel = ($formatInfo >> 3) & 0x03; // Bits 3,4
+		$this->dataMask             = ($formatInfo & 0x07); // Bottom 3 bits
+	}
+
+	public function getErrorCorrectionLevel():EccLevel{
+		return new EccLevel($this->errorCorrectionLevel);
+	}
+
+	public function getDataMask():MaskPattern{
+		return new MaskPattern($this->dataMask);
+	}
+
+}
+

+ 151 - 0
src/Common/GF256.php

@@ -0,0 +1,151 @@
+<?php
+/**
+ * Class GF256
+ *
+ * @created      16.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use InvalidArgumentException;
+
+use function array_fill;
+
+/**
+ * <p>This class contains utility methods for performing mathematical operations over
+ * the Galois Fields. Operations use a given primitive polynomial in calculations.</p>
+ *
+ * <p>Throughout this package, elements of the GF are represented as an {@code int}
+ * for convenience and speed (but at the cost of memory).
+ * </p>
+ *
+ * @author Sean Owen
+ * @author David Olivier
+ */
+final class GF256{
+
+	/**
+	 * irreducible polynomial whose coefficients are represented by the bits of an int,
+	 * where the least-significant bit represents the constant coefficient
+	 */
+#	private int $primitive = 0x011D;
+
+	private const logTable = [
+		  0, // the first value is never returned, index starts at 1
+		       0,   1,  25,   2,  50,  26, 198,   3, 223,  51, 238,  27, 104, 199,  75,
+		  4, 100, 224,  14,  52, 141, 239, 129,  28, 193, 105, 248, 200,   8,  76, 113,
+		  5, 138, 101,  47, 225,  36,  15,  33,  53, 147, 142, 218, 240,  18, 130,  69,
+		 29, 181, 194, 125, 106,  39, 249, 185, 201, 154,   9, 120,  77, 228, 114, 166,
+		  6, 191, 139,  98, 102, 221,  48, 253, 226, 152,  37, 179,  16, 145,  34, 136,
+		 54, 208, 148, 206, 143, 150, 219, 189, 241, 210,  19,  92, 131,  56,  70,  64,
+		 30,  66, 182, 163, 195,  72, 126, 110, 107,  58,  40,  84, 250, 133, 186,  61,
+		202,  94, 155, 159,  10,  21, 121,  43,  78, 212, 229, 172, 115, 243, 167,  87,
+		  7, 112, 192, 247, 140, 128,  99,  13, 103,  74, 222, 237,  49, 197, 254,  24,
+		227, 165, 153, 119,  38, 184, 180, 124,  17,  68, 146, 217,  35,  32, 137,  46,
+		 55,  63, 209,  91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190,  97,
+		242,  86, 211, 171,  20,  42,  93, 158, 132,  60,  57,  83,  71, 109,  65, 162,
+		 31,  45,  67, 216, 183, 123, 164, 118, 196,  23,  73, 236, 127,  12, 111, 246,
+		108, 161,  59,  82,  41, 157,  85, 170, 251,  96, 134, 177, 187, 204,  62,  90,
+		203,  89,  95, 176, 156, 169, 160,  81,  11, 245,  22, 235, 122, 117,  44, 215,
+		 79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168,  80,  88, 175,
+	];
+
+	private const expTable = [
+		  1,   2,   4,   8,  16,  32,  64, 128,  29,  58, 116, 232, 205, 135,  19,  38,
+		 76, 152,  45,  90, 180, 117, 234, 201, 143,   3,   6,  12,  24,  48,  96, 192,
+		157,  39,  78, 156,  37,  74, 148,  53, 106, 212, 181, 119, 238, 193, 159,  35,
+		 70, 140,   5,  10,  20,  40,  80, 160,  93, 186, 105, 210, 185, 111, 222, 161,
+		 95, 190,  97, 194, 153,  47,  94, 188, 101, 202, 137,  15,  30,  60, 120, 240,
+		253, 231, 211, 187, 107, 214, 177, 127, 254, 225, 223, 163,  91, 182, 113, 226,
+		217, 175,  67, 134,  17,  34,  68, 136,  13,  26,  52, 104, 208, 189, 103, 206,
+		129,  31,  62, 124, 248, 237, 199, 147,  59, 118, 236, 197, 151,  51, 102, 204,
+		133,  23,  46,  92, 184, 109, 218, 169,  79, 158,  33,  66, 132,  21,  42,  84,
+		168,  77, 154,  41,  82, 164,  85, 170,  73, 146,  57, 114, 228, 213, 183, 115,
+		230, 209, 191,  99, 198, 145,  63, 126, 252, 229, 215, 179, 123, 246, 241, 255,
+		227, 219, 171,  75, 150,  49,  98, 196, 149,  55, 110, 220, 165,  87, 174,  65,
+		130,  25,  50, 100, 200, 141,   7,  14,  28,  56, 112, 224, 221, 167,  83, 166,
+		 81, 162,  89, 178, 121, 242, 249, 239, 195, 155,  43,  86, 172,  69, 138,   9,
+		 18,  36,  72, 144,  61, 122, 244, 245, 247, 243, 251, 235, 203, 139,  11,  22,
+		 44,  88, 176, 125, 250, 233, 207, 131,  27,  54, 108, 216, 173,  71, 142,   1,
+	];
+
+	/**
+	 * Implements both addition and subtraction -- they are the same in GF(size).
+	 *
+	 * @return int sum/difference of a and b
+	 */
+	public static function addOrSubtract(int $a, int $b):int{
+		return $a ^ $b;
+	}
+
+	/**
+	 * @return GenericGFPoly the monomial representing coefficient * x^degree
+	 */
+	public static function buildMonomial(int $degree, int $coefficient):GenericGFPoly{
+
+		if($degree < 0){
+			throw new InvalidArgumentException();
+		}
+
+		$coefficients    = array_fill(0, $degree + 1, 0);
+		$coefficients[0] = $coefficient;
+
+		return new GenericGFPoly($coefficients);
+	}
+
+	/**
+	 * @return int 2 to the power of a in GF(size)
+	 */
+	public static function exp(int $a):int{
+
+		if($a < 0){
+			$a += 255;
+		}
+		elseif($a >= 256){
+			$a -= 255;
+		}
+
+		return self::expTable[$a];
+	}
+
+	/**
+	 * @return int base 2 log of a in GF(size)
+	 */
+	public static function log(int $a):int{
+
+		if($a < 1){
+			throw new InvalidArgumentException();
+		}
+
+		return self::logTable[$a];
+	}
+
+	/**
+	 * @return int multiplicative inverse of a
+	 */
+	public static function inverse(int $a):int{
+
+		if($a === 0){
+			throw new InvalidArgumentException();
+		}
+
+		return self::expTable[256 - self::logTable[$a] - 1];
+	}
+
+	/**
+	 * @return int product of a and b in GF(size)
+	 */
+	public static function multiply(int $a, int $b):int{
+
+		if($a === 0 || $b === 0){
+			return 0;
+		}
+
+		return self::expTable[(self::logTable[$a] + self::logTable[$b]) % 255];
+	}
+
+}

+ 276 - 0
src/Common/GenericGFPoly.php

@@ -0,0 +1,276 @@
+<?php
+/**
+ * Class GenericGFPoly
+ *
+ * @created      16.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use InvalidArgumentException;
+
+use function array_fill, array_slice, array_splice, count;
+
+/**
+ * <p>Represents a polynomial whose coefficients are elements of a GF.
+ * Instances of this class are immutable.</p>
+ *
+ * <p>Much credit is due to William Rucklidge since portions of this code are an indirect
+ * port of his C++ Reed-Solomon implementation.</p>
+ *
+ * @author Sean Owen
+ */
+final class GenericGFPoly{
+
+	private array $coefficients;
+
+	/**
+	 * @param array      $coefficients array coefficients as ints representing elements of GF(size), arranged
+	 *                                 from most significant (highest-power term) coefficient to least significant
+	 * @param int|null   $degree
+	 *
+	 * @throws \InvalidArgumentException if argument is null or empty, or if leading coefficient is 0 and this is not a
+	 *                                  constant polynomial (that is, it is not the monomial "0")
+	 */
+	public function __construct(array $coefficients, int $degree = null){
+		$degree ??= 0;
+
+		if(empty($coefficients)){
+			throw new InvalidArgumentException('arg $coefficients is empty');
+		}
+
+		if($degree < 0){
+			throw new InvalidArgumentException('negative degree');
+		}
+
+		$coefficientsLength = count($coefficients);
+
+		// Leading term must be non-zero for anything except the constant polynomial "0"
+		$firstNonZero = 0;
+
+		while($firstNonZero < $coefficientsLength && $coefficients[$firstNonZero] === 0){
+			$firstNonZero++;
+		}
+
+		if($firstNonZero === $coefficientsLength){
+			$this->coefficients = [0];
+		}
+		else{
+			$this->coefficients = array_fill(0, $coefficientsLength - $firstNonZero + $degree, 0);
+
+			for($i = 0; $i < $coefficientsLength - $firstNonZero; $i++){
+				$this->coefficients[$i] = $coefficients[$i + $firstNonZero];
+			}
+		}
+	}
+
+	/**
+	 * @return int $coefficient of x^degree term in this polynomial
+	 */
+	public function getCoefficient(int $degree):int{
+		return $this->coefficients[count($this->coefficients) - 1 - $degree];
+	}
+
+	/**
+	 * @return int[]
+	 */
+	public function getCoefficients():array{
+		return $this->coefficients;
+	}
+
+	/**
+	 * @return int $degree of this polynomial
+	 */
+	public function getDegree():int{
+		return count($this->coefficients) - 1;
+	}
+
+	/**
+	 * @return bool true if this polynomial is the monomial "0"
+	 */
+	public function isZero():bool{
+		return $this->coefficients[0] === 0;
+	}
+
+	/**
+	 * @return int evaluation of this polynomial at a given point
+	 */
+	public function evaluateAt(int $a):int{
+
+		if($a === 0){
+			// Just return the x^0 coefficient
+			return $this->getCoefficient(0);
+		}
+
+		$result = 0;
+
+		foreach($this->coefficients as $c){
+			// if $a === 1 just the sum of the coefficients
+			$result = GF256::addOrSubtract(($a === 1 ? $result : GF256::multiply($a, $result)), $c);
+		}
+
+		return $result;
+	}
+
+	/**
+	 * @param \chillerlan\QRCode\Common\GenericGFPoly $other
+	 *
+	 * @return \chillerlan\QRCode\Common\GenericGFPoly
+	 */
+	public function multiply(GenericGFPoly $other):GenericGFPoly{
+
+		if($this->isZero() || $other->isZero()){
+			return new self([0]);
+		}
+
+		$product = array_fill(0, count($this->coefficients) + count($other->coefficients) - 1, 0);
+
+		foreach($this->coefficients as $i => $aCoeff){
+			foreach($other->coefficients as $j => $bCoeff){
+				$product[$i + $j] ^= GF256::multiply($aCoeff, $bCoeff);
+			}
+		}
+
+		return new self($product);
+	}
+
+	/**
+	 * @param \chillerlan\QRCode\Common\GenericGFPoly $other
+	 *
+	 * @return \chillerlan\QRCode\Common\GenericGFPoly[] [quotient, remainder]
+	 */
+	public function divide(GenericGFPoly $other):array{
+
+		if($other->isZero()){
+			throw new InvalidArgumentException('Division by 0');
+		}
+
+		$quotient  = new self([0]);
+		$remainder = clone $this;
+
+		$denominatorLeadingTerm        = $other->getCoefficient($other->getDegree());
+		$inverseDenominatorLeadingTerm = GF256::inverse($denominatorLeadingTerm);
+
+		while($remainder->getDegree() >= $other->getDegree() && !$remainder->isZero()){
+			$scale     = GF256::multiply($remainder->getCoefficient($remainder->getDegree()), $inverseDenominatorLeadingTerm);
+			$diff      = $remainder->getDegree() - $other->getDegree();
+			$quotient  = $quotient->addOrSubtract(GF256::buildMonomial($diff, $scale));
+			$remainder = $remainder->addOrSubtract($other->multiplyByMonomial($diff, $scale));
+		}
+
+		return [$quotient, $remainder];
+
+	}
+
+	/**
+	 * @param int $scalar
+	 *
+	 * @return \chillerlan\QRCode\Common\GenericGFPoly
+	 */
+	public function multiplyInt(int $scalar):GenericGFPoly{
+
+		if($scalar === 0){
+			return new self([0]);
+		}
+
+		if($scalar === 1){
+			return $this;
+		}
+
+		$product = array_fill(0, count($this->coefficients), 0);
+
+		foreach($this->coefficients as $i => $c){
+			$product[$i] = GF256::multiply($c, $scalar);
+		}
+
+		return new self($product);
+	}
+
+	/**
+	 * @param int $degree
+	 * @param int $coefficient
+	 *
+	 * @return \chillerlan\QRCode\Common\GenericGFPoly
+	 */
+	public function multiplyByMonomial(int $degree, int $coefficient):GenericGFPoly{
+
+		if($degree < 0){
+			throw new InvalidArgumentException();
+		}
+
+		if($coefficient === 0){
+			return new self([0]);
+		}
+
+		$product = array_fill(0, count($this->coefficients) + $degree, 0);
+
+		foreach($this->coefficients as $i => $c){
+			$product[$i] = GF256::multiply($c, $coefficient);
+		}
+
+		return new self($product);
+	}
+
+	/**
+	 * @param \chillerlan\QRCode\Common\GenericGFPoly $other
+	 *
+	 * @return \chillerlan\QRCode\Common\GenericGFPoly
+	 */
+	public function mod(GenericGFPoly $other):GenericGFPoly{
+
+		if(count($this->coefficients) - count($other->coefficients) < 0){
+			return $this;
+		}
+
+		$ratio = GF256::log($this->coefficients[0]) - GF256::log($other->coefficients[0]);
+
+		foreach($other->coefficients as $i => $c){
+			$this->coefficients[$i] ^= GF256::exp(GF256::log($c) + $ratio);
+		}
+
+		return (new self($this->coefficients))->mod($other);
+	}
+
+	/**
+	 * @param \chillerlan\QRCode\Common\GenericGFPoly $other
+	 *
+	 * @return \chillerlan\QRCode\Common\GenericGFPoly
+	 */
+	public function addOrSubtract(GenericGFPoly $other):GenericGFPoly{
+
+		if($this->isZero()){
+			return $other;
+		}
+
+		if($other->isZero()){
+			return $this;
+		}
+
+		$smallerCoefficients = $this->coefficients;
+		$largerCoefficients  = $other->coefficients;
+
+		if(count($smallerCoefficients) > count($largerCoefficients)){
+			$temp                = $smallerCoefficients;
+			$smallerCoefficients = $largerCoefficients;
+			$largerCoefficients  = $temp;
+		}
+
+		$sumDiff    = array_fill(0, count($largerCoefficients), 0);
+		$lengthDiff = count($largerCoefficients) - count($smallerCoefficients);
+		// Copy high-order terms only found in higher-degree polynomial's coefficients
+		array_splice($sumDiff, 0, $lengthDiff, array_slice($largerCoefficients, 0, $lengthDiff));
+
+		$countLargerCoefficients = count($largerCoefficients);
+
+		for($i = $lengthDiff; $i < $countLargerCoefficients; $i++){
+			$sumDiff[$i] = GF256::addOrSubtract($smallerCoefficients[$i - $lengthDiff], $largerCoefficients[$i]);
+		}
+
+		return new self($sumDiff);
+	}
+
+}

+ 86 - 0
src/Common/MaskPattern.php

@@ -0,0 +1,86 @@
+<?php
+/**
+ * Class MaskPattern
+ *
+ * @created      19.01.2021
+ * @author       smiley <smiley@chillerlan.net>
+ * @copyright    2021 smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use chillerlan\QRCode\QRCodeException;
+use Closure;
+
+/**
+ * ISO/IEC 18004:2000 Section 8.8.1
+ */
+final class MaskPattern{
+
+	public const PATTERN_000 = 0b000;
+	public const PATTERN_001 = 0b001;
+	public const PATTERN_010 = 0b010;
+	public const PATTERN_011 = 0b011;
+	public const PATTERN_100 = 0b100;
+	public const PATTERN_101 = 0b101;
+	public const PATTERN_110 = 0b110;
+	public const PATTERN_111 = 0b111;
+
+	public const PATTERNS = [
+		self::PATTERN_000,
+		self::PATTERN_001,
+		self::PATTERN_010,
+		self::PATTERN_011,
+		self::PATTERN_100,
+		self::PATTERN_101,
+		self::PATTERN_110,
+		self::PATTERN_111,
+	];
+
+	private int $maskPattern;
+
+	/**
+	 * MaskPattern constructor.
+	 *
+	 * @throws \chillerlan\QRCode\QRCodeException
+	 */
+	public function __construct(int $maskPattern){
+
+		if((0b111 & $maskPattern) !== $maskPattern){
+			throw new QRCodeException('invalid mask pattern');
+		}
+
+		$this->maskPattern = $maskPattern;
+	}
+
+	/**
+	 * Returns the current mask pattern
+	 */
+	public function getPattern():int{
+		return $this->maskPattern;
+	}
+
+	/**
+	 * Returns a closure that applies the mask for the chosen mask pattern.
+	 *
+	 * Note that some versions of the QR code standard have had errors in the section about mask patterns.
+	 * The information below has been corrected.
+	 *
+	 * @see https://www.thonky.com/qr-code-tutorial/mask-patterns
+	 */
+	public function getMask():Closure{
+		// $x = column (width), $y = row (height)
+		return [
+			self::PATTERN_000 => fn(int $x, int $y):int => ($x + $y) % 2,
+			self::PATTERN_001 => fn(int $x, int $y):int => $y % 2,
+			self::PATTERN_010 => fn(int $x, int $y):int => $x % 3,
+			self::PATTERN_011 => fn(int $x, int $y):int => ($x + $y) % 3,
+			self::PATTERN_100 => fn(int $x, int $y):int => ((int)($y / 2) + (int)($x / 3)) % 2,
+			self::PATTERN_101 => fn(int $x, int $y):int => (($x * $y) % 2) + (($x * $y) % 3),
+			self::PATTERN_110 => fn(int $x, int $y):int => ((($x * $y) % 2) + (($x * $y) % 3)) % 2,
+			self::PATTERN_111 => fn(int $x, int $y):int => ((($x * $y) % 3) + (($x + $y) % 2)) % 2,
+		][$this->maskPattern];
+	}
+
+}

+ 17 - 18
src/Data/MaskPatternTester.php → src/Common/MaskPatternTester.php

@@ -2,9 +2,7 @@
 /**
  * Class MaskPatternTester
  *
- * @filesource   MaskPatternTester.php
  * @created      22.11.2017
- * @package      chillerlan\QRCode\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -12,12 +10,13 @@
  * @noinspection PhpUnused
  */
 
-namespace chillerlan\QRCode\Data;
+namespace chillerlan\QRCode\Common;
 
+use chillerlan\QRCode\Data\QRData;
 use function abs, array_search, call_user_func_array, min;
 
 /**
- * Receives a QRDataInterface object and runs the mask pattern tests on it.
+ * Receives a QRData object and runs the mask pattern tests on it.
  *
  * ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results
  *
@@ -28,16 +27,16 @@ final class MaskPatternTester{
 	/**
 	 * The data interface that contains the data matrix to test
 	 */
-	protected QRDataInterface $dataInterface;
+	private QRData $qrData;
 
 	/**
-	 * Receives the QRDataInterface
+	 * Receives the QRData object
 	 *
 	 * @see \chillerlan\QRCode\QROptions::$maskPattern
 	 * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern
 	 */
-	public function __construct(QRDataInterface $dataInterface){
-		$this->dataInterface = $dataInterface;
+	public function __construct(QRData $qrData){
+		$this->qrData = $qrData;
 	}
 
 	/**
@@ -45,14 +44,14 @@ final class MaskPatternTester{
 	 *
 	 * @see \chillerlan\QRCode\Data\MaskPatternTester
 	 */
-	public function getBestMaskPattern():int{
+	public function getBestMaskPattern():MaskPattern{
 		$penalties = [];
 
-		for($pattern = 0; $pattern < 8; $pattern++){
-			$penalties[$pattern] = $this->testPattern($pattern);
+		foreach(MaskPattern::PATTERNS as $pattern){
+			$penalties[$pattern] = $this->testPattern(new MaskPattern($pattern));
 		}
 
-		return array_search(min($penalties), $penalties, true);
+		return new MaskPattern(array_search(min($penalties), $penalties, true));
 	}
 
 	/**
@@ -61,8 +60,8 @@ final class MaskPatternTester{
 	 * @see \chillerlan\QRCode\QROptions::$maskPattern
 	 * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern
 	 */
-	public function testPattern(int $pattern):int{
-		$matrix  = $this->dataInterface->initMatrix($pattern, true);
+	public function testPattern(MaskPattern $pattern):int{
+		$matrix  = $this->qrData->writeMatrix($pattern, true);
 		$penalty = 0;
 
 		for($level = 1; $level <= 4; $level++){
@@ -75,7 +74,7 @@ final class MaskPatternTester{
 	/**
 	 * Checks for each group of five or more same-colored modules in a row (or column)
 	 */
-	protected function testLevel1(array $m, int $size):int{
+	private function testLevel1(array $m, int $size):int{
 		$penalty = 0;
 
 		foreach($m as $y => $row){
@@ -114,7 +113,7 @@ final class MaskPatternTester{
 	/**
 	 * Checks for each 2x2 area of same-colored modules in the matrix
 	 */
-	protected function testLevel2(array $m, int $size):int{
+	private function testLevel2(array $m, int $size):int{
 		$penalty = 0;
 
 		foreach($m as $y => $row){
@@ -145,7 +144,7 @@ final class MaskPatternTester{
 	/**
 	 * Checks if there are patterns that look similar to the finder patterns (1:1:3:1:1 ratio)
 	 */
-	protected function testLevel3(array $m, int $size):int{
+	private function testLevel3(array $m, int $size):int{
 		$penalties = 0;
 
 		foreach($m as $y => $row){
@@ -186,7 +185,7 @@ final class MaskPatternTester{
 	/**
 	 * Checks if more than half of the modules are dark or light, with a larger penalty for a larger difference
 	 */
-	protected function testLevel4(array $m, int $size):float{
+	private function testLevel4(array $m, int $size):float{
 		$count = 0;
 
 		foreach($m as $y => $row){

+ 112 - 0
src/Common/Mode.php

@@ -0,0 +1,112 @@
+<?php
+/**
+ * Class Mode
+ *
+ * @created      19.11.2020
+ * @author       smiley <smiley@chillerlan.net>
+ * @copyright    2020 smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use chillerlan\QRCode\Data\{AlphaNum, Byte, Kanji, Number};
+use chillerlan\QRCode\QRCodeException;
+
+/**
+ * ISO 18004:2006, 6.4.1, Tables 2 and 3
+ */
+final class Mode{
+
+	// ISO/IEC 18004:2000 Table 2
+
+	/** @var int */
+	public const DATA_TERMINATOR       = 0b0000;
+	/** @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;
+	/** @var int */
+	public const DATA_STRCTURED_APPEND = 0b0011;
+	/** @var int */
+	public const DATA_FNC1_FIRST       = 0b0101;
+	/** @var int */
+	public const DATA_FNC1_SECOND      = 0b1001;
+	/** @var int */
+	public const DATA_ECI              = 0b0111;
+
+	/**
+	 * mode length bits for the version breakpoints 1-9, 10-26 and 27-40
+	 *
+	 * ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator
+	 */
+	public const LENGTH_BITS = [
+		self::DATA_NUMBER   => [10, 12, 14],
+		self::DATA_ALPHANUM => [ 9, 11, 13],
+		self::DATA_BYTE     => [ 8, 16, 16],
+		self::DATA_KANJI    => [ 8, 10, 12],
+	];
+
+	/**
+	 * Map of data mode => interface (detection order)
+	 *
+	 * @var string[]
+	 */
+	public const DATA_INTERFACES = [
+		self::DATA_NUMBER   => Number::class,
+		self::DATA_ALPHANUM => AlphaNum::class,
+		self::DATA_KANJI    => Kanji::class,
+		self::DATA_BYTE     => Byte::class,
+	];
+
+	/**
+	 * References to the keys of the following table(s):
+	 *
+	 * @see \chillerlan\QRCode\Common\Version::MAX_LENGTH
+	 *
+	 * @var int[]
+	 */
+	public const DATA_MODES = [
+		self::DATA_NUMBER   => 0,
+		self::DATA_ALPHANUM => 1,
+		self::DATA_BYTE     => 2,
+		self::DATA_KANJI    => 3,
+	];
+
+	/**
+	 * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40
+	 *
+	 * @throws \chillerlan\QRCode\QRCodeException
+	 */
+	public static function getLengthBitsForVersion(int $mode, int $version):int{
+
+		if(!isset(self::LENGTH_BITS[$mode])){
+			throw new QRCodeException('invalid mode given');
+		}
+
+		$minVersion = 0;
+
+		foreach([9, 26, 40] as $key => $breakpoint){
+
+			if($version > $minVersion && $version <= $breakpoint){
+				return self::LENGTH_BITS[$mode][$key];
+			}
+
+			$minVersion = $breakpoint;
+		}
+
+		throw new QRCodeException(sprintf('invalid version number: %d', $version));
+	}
+
+	/**
+	 * returns the array of length bits for the given mode
+	 */
+	public static function getLengthBitsForMode(int $mode):array{
+		return self::LENGTH_BITS[$mode];
+	}
+
+}

+ 192 - 0
src/Common/ReedSolomonDecoder.php

@@ -0,0 +1,192 @@
+<?php
+/**
+ * Class ReedSolomonDecoder
+ *
+ * @created      24.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use RuntimeException;
+use function array_fill, count;
+
+/**
+ * <p>Implements Reed-Solomon decoding, as the name implies.</p>
+ *
+ * <p>The algorithm will not be explained here, but the following references were helpful
+ * in creating this implementation:</p>
+ *
+ * <ul>
+ * <li>Bruce Maggs.
+ * <a href="http://www.cs.cmu.edu/afs/cs.cmu.edu/project/pscico-guyb/realworld/www/rs_decode.ps">
+ * "Decoding Reed-Solomon Codes"</a> (see discussion of Forney's Formula)</li>
+ * <li>J.I. Hall. <a href="www.mth.msu.edu/~jhall/classes/codenotes/GRS.pdf">
+ * "Chapter 5. Generalized Reed-Solomon Codes"</a>
+ * (see discussion of Euclidean algorithm)</li>
+ * </ul>
+ *
+ * <p>Much credit is due to William Rucklidge since portions of this code are an indirect
+ * port of his C++ Reed-Solomon implementation.</p>
+ *
+ * @author Sean Owen
+ * @author William Rucklidge
+ * @author sanfordsquires
+ */
+final class ReedSolomonDecoder{
+
+	/**
+	 * <p>Decodes given set of received codewords, which include both data and error-correction
+	 * codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place,
+	 * in the input.</p>
+	 *
+	 * @param array $received        data and error-correction codewords
+	 * @param int   $numEccCodewords number of error-correction codewords available
+	 *
+	 * @return int[]
+	 * @throws \RuntimeException if decoding fails for any reason
+	 */
+	public function decode(array $received, int $numEccCodewords):array{
+		$poly                 = new GenericGFPoly($received);
+		$syndromeCoefficients = [];
+		$noError              = true;
+
+		for($i = 0, $j = $numEccCodewords - 1; $i < $numEccCodewords; $i++, $j--){
+			$eval                     = $poly->evaluateAt(GF256::exp($i));
+			$syndromeCoefficients[$j] = $eval;
+
+			if($eval !== 0){
+				$noError = false;
+			}
+		}
+
+		if($noError){
+			return $received;
+		}
+
+		[$sigma, $omega] = $this->runEuclideanAlgorithm(
+			GF256::buildMonomial($numEccCodewords, 1),
+			new GenericGFPoly($syndromeCoefficients),
+			$numEccCodewords
+		);
+
+		$errorLocations      = $this->findErrorLocations($sigma);
+		$errorMagnitudes     = $this->findErrorMagnitudes($omega, $errorLocations);
+		$errorLocationsCount = count($errorLocations);
+		$receivedCount       = count($received);
+
+		for($i = 0; $i < $errorLocationsCount; $i++){
+			$position = $receivedCount - 1 - GF256::log($errorLocations[$i]);
+
+			if($position < 0){
+				throw new RuntimeException('Bad error location');
+			}
+
+			$received[$position] ^= $errorMagnitudes[$i];
+		}
+
+		return $received;
+	}
+
+	/**
+	 * @return \chillerlan\QRCode\Common\GenericGFPoly[] [sigma, omega]
+	 * @throws \RuntimeException
+	 */
+	private function runEuclideanAlgorithm(GenericGFPoly $a, GenericGFPoly $b, int $R):array{
+		// Assume a's degree is >= b's
+		if($a->getDegree() < $b->getDegree()){
+			$temp = $a;
+			$a    = $b;
+			$b    = $temp;
+		}
+
+		$rLast = $a;
+		$r     = $b;
+		$tLast = new GenericGFPoly([0]);
+		$t     = new GenericGFPoly([1]);
+
+		// Run Euclidean algorithm until r's degree is less than R/2
+		while($r->getDegree() >= $R / 2){
+			$rLastLast = $rLast;
+			$tLastLast = $tLast;
+			$rLast     = $r;
+			$tLast     = $t;
+
+			// Divide rLastLast by rLast, with quotient in q and remainder in r
+			[$q, $r] = $rLastLast->divide($rLast);
+
+			$t = $q->multiply($tLast)->addOrSubtract($tLastLast);
+
+			if($r->getDegree() >= $rLast->getDegree()){
+				throw new RuntimeException('Division algorithm failed to reduce polynomial?');
+			}
+		}
+
+		$sigmaTildeAtZero = $t->getCoefficient(0);
+
+		if($sigmaTildeAtZero === 0){
+			throw new RuntimeException('sigmaTilde(0) was zero');
+		}
+
+		$inverse = GF256::inverse($sigmaTildeAtZero);
+
+		return [$t->multiplyInt($inverse), $r->multiplyInt($inverse)];
+	}
+
+	/**
+	 * @throws \RuntimeException
+	 */
+	private function findErrorLocations(GenericGFPoly $errorLocator):array{
+		// This is a direct application of Chien's search
+		$numErrors = $errorLocator->getDegree();
+
+		if($numErrors === 1){ // shortcut
+			return [$errorLocator->getCoefficient(1)];
+		}
+
+		$result = array_fill(0, $numErrors, 0);
+		$e      = 0;
+
+		for($i = 1; $i < 256 && $e < $numErrors; $i++){
+			if($errorLocator->evaluateAt($i) === 0){
+				$result[$e] = GF256::inverse($i);
+				$e++;
+			}
+		}
+
+		if($e !== $numErrors){
+			throw new RuntimeException('Error locator degree does not match number of roots');
+		}
+
+		return $result;
+	}
+
+	private function findErrorMagnitudes(GenericGFPoly $errorEvaluator, array $errorLocations):array{
+		// This is directly applying Forney's Formula
+		$s      = count($errorLocations);
+		$result = [];
+
+		for($i = 0; $i < $s; $i++){
+			$xiInverse   = GF256::inverse($errorLocations[$i]);
+			$denominator = 1;
+
+			for($j = 0; $j < $s; $j++){
+				if($i !== $j){
+#					$denominator = GF256::multiply($denominator, GF256::addOrSubtract(1, GF256::multiply($errorLocations[$j], $xiInverse)));
+					// Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug.
+					// Below is a funny-looking workaround from Steven Parkes
+					$term        = GF256::multiply($errorLocations[$j], $xiInverse);
+					$denominator = GF256::multiply($denominator, (($term & 0x1) === 0 ? $term | 1 : $term & ~1));
+				}
+			}
+
+			$result[$i] = GF256::multiply($errorEvaluator->evaluateAt($xiInverse), GF256::inverse($denominator));
+		}
+
+		return $result;
+	}
+
+}

+ 115 - 0
src/Common/ReedSolomonEncoder.php

@@ -0,0 +1,115 @@
+<?php
+/**
+ * Class ReedSolomonEncoder
+ *
+ * @created      07.01.2021
+ * @author       smiley <smiley@chillerlan.net>
+ * @copyright    2021 smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use SplFixedArray;
+
+use function array_fill, array_merge, count, max;
+
+/**
+ * ISO/IEC 18004:2000 Section 8.5 ff
+ *
+ * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
+ */
+final class ReedSolomonEncoder{
+
+	private SplFixedArray $interleavedData;
+	private int           $interleavedDataIndex;
+
+	/**
+	 * ECC interleaving
+	 *
+	 * @return \SplFixedArray<int>
+	 */
+	public function interleaveEcBytes(BitBuffer $bitBuffer, Version $version, EccLevel $eccLevel):SplFixedArray{
+		[$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $version->getRSBlocks($eccLevel);
+
+		$rsBlocks = array_fill(0, $l1, [$numEccCodewords + $b1, $b1]);
+
+		if($l2 > 0){
+			$rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$numEccCodewords + $b2, $b2]));
+		}
+
+		$bitBufferData  = $bitBuffer->getBuffer();
+		$dataBytes      = [];
+		$ecBytes        = [];
+		$maxDataBytes   = 0;
+		$maxEcBytes     = 0;
+		$dataByteOffset = 0;
+
+		foreach($rsBlocks as $key => $block){
+			[$rsBlockTotal, $dataByteCount] = $block;
+
+			$dataBytes[$key] = [];
+
+			for($i = 0; $i < $dataByteCount; $i++){
+				$dataBytes[$key][$i] = $bitBufferData[$i + $dataByteOffset] & 0xff;
+			}
+
+			$ecByteCount    = $rsBlockTotal - $dataByteCount;
+			$ecBytes[$key]  = $this->generateEcBytes($dataBytes[$key], $ecByteCount);
+			$maxDataBytes   = max($maxDataBytes, $dataByteCount);
+			$maxEcBytes     = max($maxEcBytes, $ecByteCount);
+			$dataByteOffset += $dataByteCount;
+		}
+
+		$this->interleavedData      = new SplFixedArray($version->getTotalCodewords());
+		$this->interleavedDataIndex = 0;
+		$numRsBlocks                = $l1 + $l2;
+
+		$this->interleave($dataBytes, $maxDataBytes, $numRsBlocks);
+		$this->interleave($ecBytes, $maxEcBytes, $numRsBlocks);
+
+		return $this->interleavedData;
+	}
+
+	/**
+	 *
+	 */
+	private function generateEcBytes(array $dataBytes, int $ecByteCount):array{
+		$rsPoly = new GenericGFPoly([1]);
+
+		for($i = 0; $i < $ecByteCount; $i++){
+			$rsPoly = $rsPoly->multiply(new GenericGFPoly([1, GF256::exp($i)]));
+		}
+
+		$rsPolyDegree = $rsPoly->getDegree();
+
+		$modCoefficients = (new GenericGFPoly($dataBytes, $rsPolyDegree))
+			->mod($rsPoly)
+			->getCoefficients()
+		;
+
+		$ecBytes = array_fill(0, $rsPolyDegree, 0);
+		$count   = count($modCoefficients) - $rsPolyDegree;
+
+		foreach($ecBytes as $i => &$val){
+			$modIndex = $i + $count;
+			$val      = $modIndex >= 0 ? $modCoefficients[$modIndex] : 0;
+		}
+
+		return $ecBytes;
+	}
+
+	/**
+	 *
+	 */
+	private function interleave(array $byteArray, int $maxBytes, int $numRsBlocks):void{
+		for($x = 0; $x < $maxBytes; $x++){
+			for($y = 0; $y < $numRsBlocks; $y++){
+				if($x < count($byteArray[$y])){
+					$this->interleavedData[$this->interleavedDataIndex++] = $byteArray[$y][$x];
+				}
+			}
+		}
+	}
+
+}

+ 334 - 0
src/Common/Version.php

@@ -0,0 +1,334 @@
+<?php
+/**
+ * Class Version
+ *
+ * @created      19.11.2020
+ * @author       smiley <smiley@chillerlan.net>
+ * @copyright    2020 smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use chillerlan\QRCode\QRCodeException;
+
+/**
+ *
+ */
+final class Version{
+
+	/**
+	 * ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns
+	 *
+	 * version -> pattern
+	 *
+	 * @var int[][]
+	 */
+	private const ALIGNMENT_PATTERN = [
+		1  => [],
+		2  => [6, 18],
+		3  => [6, 22],
+		4  => [6, 26],
+		5  => [6, 30],
+		6  => [6, 34],
+		7  => [6, 22, 38],
+		8  => [6, 24, 42],
+		9  => [6, 26, 46],
+		10 => [6, 28, 50],
+		11 => [6, 30, 54],
+		12 => [6, 32, 58],
+		13 => [6, 34, 62],
+		14 => [6, 26, 46, 66],
+		15 => [6, 26, 48, 70],
+		16 => [6, 26, 50, 74],
+		17 => [6, 30, 54, 78],
+		18 => [6, 30, 56, 82],
+		19 => [6, 30, 58, 86],
+		20 => [6, 34, 62, 90],
+		21 => [6, 28, 50, 72,  94],
+		22 => [6, 26, 50, 74,  98],
+		23 => [6, 30, 54, 78, 102],
+		24 => [6, 28, 54, 80, 106],
+		25 => [6, 32, 58, 84, 110],
+		26 => [6, 30, 58, 86, 114],
+		27 => [6, 34, 62, 90, 118],
+		28 => [6, 26, 50, 74,  98, 122],
+		29 => [6, 30, 54, 78, 102, 126],
+		30 => [6, 26, 52, 78, 104, 130],
+		31 => [6, 30, 56, 82, 108, 134],
+		32 => [6, 34, 60, 86, 112, 138],
+		33 => [6, 30, 58, 86, 114, 142],
+		34 => [6, 34, 62, 90, 118, 146],
+		35 => [6, 30, 54, 78, 102, 126, 150],
+		36 => [6, 24, 50, 76, 102, 128, 154],
+		37 => [6, 28, 54, 80, 106, 132, 158],
+		38 => [6, 32, 58, 84, 110, 136, 162],
+		39 => [6, 26, 54, 82, 110, 138, 166],
+		40 => [6, 30, 58, 86, 114, 142, 170],
+	];
+
+	/**
+	 * ISO/IEC 18004:2000 Annex D, Table D.1 - Version information bit stream for each version
+	 *
+	 * no version pattern for QR Codes < 7
+	 *
+	 * @var int[]
+	 */
+	private const VERSION_PATTERN = [
+		7  => 0b000111110010010100,
+		8  => 0b001000010110111100,
+		9  => 0b001001101010011001,
+		10 => 0b001010010011010011,
+		11 => 0b001011101111110110,
+		12 => 0b001100011101100010,
+		13 => 0b001101100001000111,
+		14 => 0b001110011000001101,
+		15 => 0b001111100100101000,
+		16 => 0b010000101101111000,
+		17 => 0b010001010001011101,
+		18 => 0b010010101000010111,
+		19 => 0b010011010100110010,
+		20 => 0b010100100110100110,
+		21 => 0b010101011010000011,
+		22 => 0b010110100011001001,
+		23 => 0b010111011111101100,
+		24 => 0b011000111011000100,
+		25 => 0b011001000111100001,
+		26 => 0b011010111110101011,
+		27 => 0b011011000010001110,
+		28 => 0b011100110000011010,
+		29 => 0b011101001100111111,
+		30 => 0b011110110101110101,
+		31 => 0b011111001001010000,
+		32 => 0b100000100111010101,
+		33 => 0b100001011011110000,
+		34 => 0b100010100010111010,
+		35 => 0b100011011110011111,
+		36 => 0b100100101100001011,
+		37 => 0b100101010000101110,
+		38 => 0b100110101001100100,
+		39 => 0b100111010101000001,
+		40 => 0b101000110001101001,
+	];
+
+	/**
+	 * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
+	 *
+	 * @see http://www.qrcode.com/en/about/version.html
+	 *
+	 * @var int [][][]
+	 */
+	private const MAX_LENGTH =[
+	//	v  => [NUMERIC => [L, M, Q, H ], ALPHANUM => [L, M, Q, H], BINARY => [L, M, Q, H  ], KANJI => [L, M, Q, H   ]]
+		1  => [[  41,   34,   27,   17], [  25,   20,   16,   10], [  17,   14,   11,    7], [  10,    8,    7,    4]],
+		2  => [[  77,   63,   48,   34], [  47,   38,   29,   20], [  32,   26,   20,   14], [  20,   16,   12,    8]],
+		3  => [[ 127,  101,   77,   58], [  77,   61,   47,   35], [  53,   42,   32,   24], [  32,   26,   20,   15]],
+		4  => [[ 187,  149,  111,   82], [ 114,   90,   67,   50], [  78,   62,   46,   34], [  48,   38,   28,   21]],
+		5  => [[ 255,  202,  144,  106], [ 154,  122,   87,   64], [ 106,   84,   60,   44], [  65,   52,   37,   27]],
+		6  => [[ 322,  255,  178,  139], [ 195,  154,  108,   84], [ 134,  106,   74,   58], [  82,   65,   45,   36]],
+		7  => [[ 370,  293,  207,  154], [ 224,  178,  125,   93], [ 154,  122,   86,   64], [  95,   75,   53,   39]],
+		8  => [[ 461,  365,  259,  202], [ 279,  221,  157,  122], [ 192,  152,  108,   84], [ 118,   93,   66,   52]],
+		9  => [[ 552,  432,  312,  235], [ 335,  262,  189,  143], [ 230,  180,  130,   98], [ 141,  111,   80,   60]],
+		10 => [[ 652,  513,  364,  288], [ 395,  311,  221,  174], [ 271,  213,  151,  119], [ 167,  131,   93,   74]],
+		11 => [[ 772,  604,  427,  331], [ 468,  366,  259,  200], [ 321,  251,  177,  137], [ 198,  155,  109,   85]],
+		12 => [[ 883,  691,  489,  374], [ 535,  419,  296,  227], [ 367,  287,  203,  155], [ 226,  177,  125,   96]],
+		13 => [[1022,  796,  580,  427], [ 619,  483,  352,  259], [ 425,  331,  241,  177], [ 262,  204,  149,  109]],
+		14 => [[1101,  871,  621,  468], [ 667,  528,  376,  283], [ 458,  362,  258,  194], [ 282,  223,  159,  120]],
+		15 => [[1250,  991,  703,  530], [ 758,  600,  426,  321], [ 520,  412,  292,  220], [ 320,  254,  180,  136]],
+		16 => [[1408, 1082,  775,  602], [ 854,  656,  470,  365], [ 586,  450,  322,  250], [ 361,  277,  198,  154]],
+		17 => [[1548, 1212,  876,  674], [ 938,  734,  531,  408], [ 644,  504,  364,  280], [ 397,  310,  224,  173]],
+		18 => [[1725, 1346,  948,  746], [1046,  816,  574,  452], [ 718,  560,  394,  310], [ 442,  345,  243,  191]],
+		19 => [[1903, 1500, 1063,  813], [1153,  909,  644,  493], [ 792,  624,  442,  338], [ 488,  384,  272,  208]],
+		20 => [[2061, 1600, 1159,  919], [1249,  970,  702,  557], [ 858,  666,  482,  382], [ 528,  410,  297,  235]],
+		21 => [[2232, 1708, 1224,  969], [1352, 1035,  742,  587], [ 929,  711,  509,  403], [ 572,  438,  314,  248]],
+		22 => [[2409, 1872, 1358, 1056], [1460, 1134,  823,  640], [1003,  779,  565,  439], [ 618,  480,  348,  270]],
+		23 => [[2620, 2059, 1468, 1108], [1588, 1248,  890,  672], [1091,  857,  611,  461], [ 672,  528,  376,  284]],
+		24 => [[2812, 2188, 1588, 1228], [1704, 1326,  963,  744], [1171,  911,  661,  511], [ 721,  561,  407,  315]],
+		25 => [[3057, 2395, 1718, 1286], [1853, 1451, 1041,  779], [1273,  997,  715,  535], [ 784,  614,  440,  330]],
+		26 => [[3283, 2544, 1804, 1425], [1990, 1542, 1094,  864], [1367, 1059,  751,  593], [ 842,  652,  462,  365]],
+		27 => [[3517, 2701, 1933, 1501], [2132, 1637, 1172,  910], [1465, 1125,  805,  625], [ 902,  692,  496,  385]],
+		28 => [[3669, 2857, 2085, 1581], [2223, 1732, 1263,  958], [1528, 1190,  868,  658], [ 940,  732,  534,  405]],
+		29 => [[3909, 3035, 2181, 1677], [2369, 1839, 1322, 1016], [1628, 1264,  908,  698], [1002,  778,  559,  430]],
+		30 => [[4158, 3289, 2358, 1782], [2520, 1994, 1429, 1080], [1732, 1370,  982,  742], [1066,  843,  604,  457]],
+		31 => [[4417, 3486, 2473, 1897], [2677, 2113, 1499, 1150], [1840, 1452, 1030,  790], [1132,  894,  634,  486]],
+		32 => [[4686, 3693, 2670, 2022], [2840, 2238, 1618, 1226], [1952, 1538, 1112,  842], [1201,  947,  684,  518]],
+		33 => [[4965, 3909, 2805, 2157], [3009, 2369, 1700, 1307], [2068, 1628, 1168,  898], [1273, 1002,  719,  553]],
+		34 => [[5253, 4134, 2949, 2301], [3183, 2506, 1787, 1394], [2188, 1722, 1228,  958], [1347, 1060,  756,  590]],
+		35 => [[5529, 4343, 3081, 2361], [3351, 2632, 1867, 1431], [2303, 1809, 1283,  983], [1417, 1113,  790,  605]],
+		36 => [[5836, 4588, 3244, 2524], [3537, 2780, 1966, 1530], [2431, 1911, 1351, 1051], [1496, 1176,  832,  647]],
+		37 => [[6153, 4775, 3417, 2625], [3729, 2894, 2071, 1591], [2563, 1989, 1423, 1093], [1577, 1224,  876,  673]],
+		38 => [[6479, 5039, 3599, 2735], [3927, 3054, 2181, 1658], [2699, 2099, 1499, 1139], [1661, 1292,  923,  701]],
+		39 => [[6743, 5313, 3791, 2927], [4087, 3220, 2298, 1774], [2809, 2213, 1579, 1219], [1729, 1362,  972,  750]],
+		40 => [[7089, 5596, 3993, 3057], [4296, 3391, 2420, 1852], [2953, 2331, 1663, 1273], [1817, 1435, 1024,  784]],
+	];
+
+	/**
+	 * ISO/IEC 18004:2000 Tables 13-22
+	 *
+	 * @see http://www.thonky.com/qr-code-tutorial/error-correction-table
+	 */
+	private const RSBLOCKS = [
+		1  => [[ 7, [[ 1,  19], [ 0,   0]]], [10, [[ 1, 16], [ 0,  0]]], [13, [[ 1, 13], [ 0,  0]]], [17, [[ 1,  9], [ 0,  0]]]],
+		2  => [[10, [[ 1,  34], [ 0,   0]]], [16, [[ 1, 28], [ 0,  0]]], [22, [[ 1, 22], [ 0,  0]]], [28, [[ 1, 16], [ 0,  0]]]],
+		3  => [[15, [[ 1,  55], [ 0,   0]]], [26, [[ 1, 44], [ 0,  0]]], [18, [[ 2, 17], [ 0,  0]]], [22, [[ 2, 13], [ 0,  0]]]],
+		4  => [[20, [[ 1,  80], [ 0,   0]]], [18, [[ 2, 32], [ 0,  0]]], [26, [[ 2, 24], [ 0,  0]]], [16, [[ 4,  9], [ 0,  0]]]],
+		5  => [[26, [[ 1, 108], [ 0,   0]]], [24, [[ 2, 43], [ 0,  0]]], [18, [[ 2, 15], [ 2, 16]]], [22, [[ 2, 11], [ 2, 12]]]],
+		6  => [[18, [[ 2,  68], [ 0,   0]]], [16, [[ 4, 27], [ 0,  0]]], [24, [[ 4, 19], [ 0,  0]]], [28, [[ 4, 15], [ 0,  0]]]],
+		7  => [[20, [[ 2,  78], [ 0,   0]]], [18, [[ 4, 31], [ 0,  0]]], [18, [[ 2, 14], [ 4, 15]]], [26, [[ 4, 13], [ 1, 14]]]],
+		8  => [[24, [[ 2,  97], [ 0,   0]]], [22, [[ 2, 38], [ 2, 39]]], [22, [[ 4, 18], [ 2, 19]]], [26, [[ 4, 14], [ 2, 15]]]],
+		9  => [[30, [[ 2, 116], [ 0,   0]]], [22, [[ 3, 36], [ 2, 37]]], [20, [[ 4, 16], [ 4, 17]]], [24, [[ 4, 12], [ 4, 13]]]],
+		10 => [[18, [[ 2,  68], [ 2,  69]]], [26, [[ 4, 43], [ 1, 44]]], [24, [[ 6, 19], [ 2, 20]]], [28, [[ 6, 15], [ 2, 16]]]],
+		11 => [[20, [[ 4,  81], [ 0,   0]]], [30, [[ 1, 50], [ 4, 51]]], [28, [[ 4, 22], [ 4, 23]]], [24, [[ 3, 12], [ 8, 13]]]],
+		12 => [[24, [[ 2,  92], [ 2,  93]]], [22, [[ 6, 36], [ 2, 37]]], [26, [[ 4, 20], [ 6, 21]]], [28, [[ 7, 14], [ 4, 15]]]],
+		13 => [[26, [[ 4, 107], [ 0,   0]]], [22, [[ 8, 37], [ 1, 38]]], [24, [[ 8, 20], [ 4, 21]]], [22, [[12, 11], [ 4, 12]]]],
+		14 => [[30, [[ 3, 115], [ 1, 116]]], [24, [[ 4, 40], [ 5, 41]]], [20, [[11, 16], [ 5, 17]]], [24, [[11, 12], [ 5, 13]]]],
+		15 => [[22, [[ 5,  87], [ 1,  88]]], [24, [[ 5, 41], [ 5, 42]]], [30, [[ 5, 24], [ 7, 25]]], [24, [[11, 12], [ 7, 13]]]],
+		16 => [[24, [[ 5,  98], [ 1,  99]]], [28, [[ 7, 45], [ 3, 46]]], [24, [[15, 19], [ 2, 20]]], [30, [[ 3, 15], [13, 16]]]],
+		17 => [[28, [[ 1, 107], [ 5, 108]]], [28, [[10, 46], [ 1, 47]]], [28, [[ 1, 22], [15, 23]]], [28, [[ 2, 14], [17, 15]]]],
+		18 => [[30, [[ 5, 120], [ 1, 121]]], [26, [[ 9, 43], [ 4, 44]]], [28, [[17, 22], [ 1, 23]]], [28, [[ 2, 14], [19, 15]]]],
+		19 => [[28, [[ 3, 113], [ 4, 114]]], [26, [[ 3, 44], [11, 45]]], [26, [[17, 21], [ 4, 22]]], [26, [[ 9, 13], [16, 14]]]],
+		20 => [[28, [[ 3, 107], [ 5, 108]]], [26, [[ 3, 41], [13, 42]]], [30, [[15, 24], [ 5, 25]]], [28, [[15, 15], [10, 16]]]],
+		21 => [[28, [[ 4, 116], [ 4, 117]]], [26, [[17, 42], [ 0,  0]]], [28, [[17, 22], [ 6, 23]]], [30, [[19, 16], [ 6, 17]]]],
+		22 => [[28, [[ 2, 111], [ 7, 112]]], [28, [[17, 46], [ 0,  0]]], [30, [[ 7, 24], [16, 25]]], [24, [[34, 13], [ 0,  0]]]],
+		23 => [[30, [[ 4, 121], [ 5, 122]]], [28, [[ 4, 47], [14, 48]]], [30, [[11, 24], [14, 25]]], [30, [[16, 15], [14, 16]]]],
+		24 => [[30, [[ 6, 117], [ 4, 118]]], [28, [[ 6, 45], [14, 46]]], [30, [[11, 24], [16, 25]]], [30, [[30, 16], [ 2, 17]]]],
+		25 => [[26, [[ 8, 106], [ 4, 107]]], [28, [[ 8, 47], [13, 48]]], [30, [[ 7, 24], [22, 25]]], [30, [[22, 15], [13, 16]]]],
+		26 => [[28, [[10, 114], [ 2, 115]]], [28, [[19, 46], [ 4, 47]]], [28, [[28, 22], [ 6, 23]]], [30, [[33, 16], [ 4, 17]]]],
+		27 => [[30, [[ 8, 122], [ 4, 123]]], [28, [[22, 45], [ 3, 46]]], [30, [[ 8, 23], [26, 24]]], [30, [[12, 15], [28, 16]]]],
+		28 => [[30, [[ 3, 117], [10, 118]]], [28, [[ 3, 45], [23, 46]]], [30, [[ 4, 24], [31, 25]]], [30, [[11, 15], [31, 16]]]],
+		29 => [[30, [[ 7, 116], [ 7, 117]]], [28, [[21, 45], [ 7, 46]]], [30, [[ 1, 23], [37, 24]]], [30, [[19, 15], [26, 16]]]],
+		30 => [[30, [[ 5, 115], [10, 116]]], [28, [[19, 47], [10, 48]]], [30, [[15, 24], [25, 25]]], [30, [[23, 15], [25, 16]]]],
+		31 => [[30, [[13, 115], [ 3, 116]]], [28, [[ 2, 46], [29, 47]]], [30, [[42, 24], [ 1, 25]]], [30, [[23, 15], [28, 16]]]],
+		32 => [[30, [[17, 115], [ 0,   0]]], [28, [[10, 46], [23, 47]]], [30, [[10, 24], [35, 25]]], [30, [[19, 15], [35, 16]]]],
+		33 => [[30, [[17, 115], [ 1, 116]]], [28, [[14, 46], [21, 47]]], [30, [[29, 24], [19, 25]]], [30, [[11, 15], [46, 16]]]],
+		34 => [[30, [[13, 115], [ 6, 116]]], [28, [[14, 46], [23, 47]]], [30, [[44, 24], [ 7, 25]]], [30, [[59, 16], [ 1, 17]]]],
+		35 => [[30, [[12, 121], [ 7, 122]]], [28, [[12, 47], [26, 48]]], [30, [[39, 24], [14, 25]]], [30, [[22, 15], [41, 16]]]],
+		36 => [[30, [[ 6, 121], [14, 122]]], [28, [[ 6, 47], [34, 48]]], [30, [[46, 24], [10, 25]]], [30, [[ 2, 15], [64, 16]]]],
+		37 => [[30, [[17, 122], [ 4, 123]]], [28, [[29, 46], [14, 47]]], [30, [[49, 24], [10, 25]]], [30, [[24, 15], [46, 16]]]],
+		38 => [[30, [[ 4, 122], [18, 123]]], [28, [[13, 46], [32, 47]]], [30, [[48, 24], [14, 25]]], [30, [[42, 15], [32, 16]]]],
+		39 => [[30, [[20, 117], [ 4, 118]]], [28, [[40, 47], [ 7, 48]]], [30, [[43, 24], [22, 25]]], [30, [[10, 15], [67, 16]]]],
+		40 => [[30, [[19, 118], [ 6, 119]]], [28, [[18, 47], [31, 48]]], [30, [[34, 24], [34, 25]]], [30, [[20, 15], [61, 16]]]],
+	];
+
+	private const TOTAL_CODEWORDS = [
+		1  => 26,
+		2  => 44,
+		3  => 70,
+		4  => 100,
+		5  => 134,
+		6  => 172,
+		7  => 196,
+		8  => 242,
+		9  => 292,
+		10 => 346,
+		11 => 404,
+		12 => 466,
+		13 => 532,
+		14 => 581,
+		15 => 655,
+		16 => 733,
+		17 => 815,
+		18 => 901,
+		19 => 991,
+		20 => 1085,
+		21 => 1156,
+		22 => 1258,
+		23 => 1364,
+		24 => 1474,
+		25 => 1588,
+		26 => 1706,
+		27 => 1828,
+		28 => 1921,
+		29 => 2051,
+		30 => 2185,
+		31 => 2323,
+		32 => 2465,
+		33 => 2611,
+		34 => 2761,
+		35 => 2876,
+		36 => 3034,
+		37 => 3196,
+		38 => 3362,
+		39 => 3532,
+		40 => 3706,
+	];
+
+	/**
+	 * QR Code version number
+	 */
+	private int $version;
+
+	/**
+	 * Version constructor.
+	 *
+	 * @throws \chillerlan\QRCode\QRCodeException
+	 */
+	public function __construct(int $version){
+
+		if($version < 1 || $version > 40){
+			throw new QRCodeException('invalid version number');
+		}
+
+		$this->version = $version;
+	}
+
+	/**
+	 * returns the current version number as string
+	 */
+	public function __toString():string{
+		return (string)$this->version;
+	}
+
+	/**
+	 * returns the current version number
+	 */
+	public function getVersionNumber():int{
+		return $this->version;
+	}
+
+	/**
+	 * the matrix size for the given version
+	 */
+	public function getDimension():int{
+		return $this->version * 4 + 17;
+	}
+
+	/**
+	 * the version pattern for the given version
+	 */
+	public function getVersionPattern():?int{
+		return self::VERSION_PATTERN[$this->version] ?? null;
+	}
+
+	/**
+	 * the alignment patterns for the current version
+	 *
+	 * @return int[]
+	 */
+	public function getAlignmentPattern():array{
+		return self::ALIGNMENT_PATTERN[$this->version];
+	}
+
+	/**
+	 * the maximum character count for the given $mode and $eccLevel
+	 */
+	public function getMaxLengthForMode(int $mode, EccLevel $eccLevel):?int{
+		return self::MAX_LENGTH[$this->version][Mode::DATA_MODES[$mode]][$eccLevel->getOrdinal()] ?? null;
+	}
+
+	/**
+	 * returns ECC block information for the given $version and $eccLevel
+	 */
+	public function getRSBlocks(EccLevel $eccLevel):array{
+		return self::RSBLOCKS[$this->version][$eccLevel->getOrdinal()];
+	}
+
+	/**
+	 * returns the maximum codewords for the current version
+	 */
+	public function getTotalCodewords():int{
+		return self::TOTAL_CODEWORDS[$this->version];
+	}
+
+
+}

+ 98 - 14
src/Data/AlphaNum.php

@@ -2,9 +2,7 @@
 /**
  * Class AlphaNum
  *
- * @filesource   AlphaNum.php
  * @created      25.11.2015
- * @package      chillerlan\QRCode\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT
@@ -12,9 +10,9 @@
 
 namespace chillerlan\QRCode\Data;
 
-use chillerlan\QRCode\QRCode;
+use chillerlan\QRCode\Common\{BitBuffer, Mode};
 
-use function ord, sprintf;
+use function array_flip, ceil, ord, sprintf, str_split;
 
 /**
  * Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / :
@@ -22,23 +20,64 @@ use function ord, sprintf;
  * ISO/IEC 18004:2000 Section 8.3.3
  * ISO/IEC 18004:2000 Section 8.4.3
  */
-final class AlphaNum extends QRDataAbstract{
+final class AlphaNum extends QRDataModeAbstract{
 
-	protected int $datamode = QRCode::DATA_ALPHANUM;
+	/**
+	 * ISO/IEC 18004:2000 Table 5
+	 *
+	 * @var int[]
+	 */
+	private const CHAR_MAP_ALPHANUM = [
+		'0' =>  0, '1' =>  1, '2' =>  2, '3' =>  3, '4' =>  4, '5' =>  5, '6' =>  6, '7' =>  7,
+		'8' =>  8, '9' =>  9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15,
+		'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23,
+		'O' => 24, 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29, 'U' => 30, 'V' => 31,
+		'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35, ' ' => 36, '$' => 37, '%' => 38, '*' => 39,
+		'+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44,
+	];
 
-	protected array $lengthBits = [9, 11, 13];
+	protected static int $datamode = Mode::DATA_ALPHANUM;
 
 	/**
 	 * @inheritdoc
 	 */
-	protected function write(string $data):void{
+	public function getLengthInBits():int{
+		return (int)ceil($this->getCharCount() * (11 / 2));
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public static function validateString(string $string):bool{
+
+		foreach(str_split($string) as $chr){
+			if(!isset(self::CHAR_MAP_ALPHANUM[$chr])){
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public function write(BitBuffer $bitBuffer, int $versionNumber):void{
+		$len = $this->getCharCount();
+
+		$bitBuffer
+			->put($this::$datamode, 4)
+			->put($len, Mode::getLengthBitsForVersion($this::$datamode, $versionNumber))
+		;
 
-		for($i = 0; $i + 1 < $this->strlen; $i += 2){
-			$this->bitBuffer->put($this->getCharCode($data[$i]) * 45 + $this->getCharCode($data[$i + 1]), 11);
+		// encode 2 characters in 11 bits
+		for($i = 0; $i + 1 < $len; $i += 2){
+			$bitBuffer->put($this->getCharCode($this->data[$i]) * 45 + $this->getCharCode($this->data[$i + 1]), 11);
 		}
 
-		if($i < $this->strlen){
-			$this->bitBuffer->put($this->getCharCode($data[$i]), 6);
+		// encode a remaining character in 6 bits
+		if($i < $len){
+			$bitBuffer->put($this->getCharCode($this->data[$i]), 6);
 		}
 
 	}
@@ -50,11 +89,56 @@ final class AlphaNum extends QRDataAbstract{
 	 */
 	protected function getCharCode(string $chr):int{
 
-		if(!isset($this::CHAR_MAP_ALPHANUM[$chr])){
+		if(!isset(self::CHAR_MAP_ALPHANUM[$chr])){
 			throw new QRCodeDataException(sprintf('illegal char: "%s" [%d]', $chr, ord($chr)));
 		}
 
-		return $this::CHAR_MAP_ALPHANUM[$chr];
+		return self::CHAR_MAP_ALPHANUM[$chr];
+	}
+
+	/**
+	 * @inheritdoc
+	 *
+	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 */
+	public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
+		$length  = $bitBuffer->read(Mode::getLengthBitsForVersion(self::$datamode, $versionNumber));
+		$charmap = array_flip(self::CHAR_MAP_ALPHANUM);
+
+		// @todo
+		$toAlphaNumericChar = function(int $ord) use ($charmap):string{
+
+			if(isset($charmap[$ord])){
+				return $charmap[$ord];
+			}
+
+			throw new QRCodeDataException('invalid character value: '.$ord);
+		};
+
+		$result = '';
+		// Read two characters at a time
+		while($length > 1){
+
+			if($bitBuffer->available() < 11){
+				throw new QRCodeDataException('not enough bits available');
+			}
+
+			$nextTwoCharsBits = $bitBuffer->read(11);
+			$result           .= $toAlphaNumericChar($nextTwoCharsBits / 45);
+			$result           .= $toAlphaNumericChar($nextTwoCharsBits % 45);
+			$length           -= 2;
+		}
+
+		if($length === 1){
+			// special case: one character left
+			if($bitBuffer->available() < 6){
+				throw new QRCodeDataException('not enough bits available');
+			}
+
+			$result .= $toAlphaNumericChar($bitBuffer->read(6));
+		}
+
+		return $result;
 	}
 
 }

+ 47 - 9
src/Data/Byte.php

@@ -2,9 +2,7 @@
 /**
  * Class Byte
  *
- * @filesource   Byte.php
  * @created      25.11.2015
- * @package      chillerlan\QRCode\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT
@@ -12,7 +10,7 @@
 
 namespace chillerlan\QRCode\Data;
 
-use chillerlan\QRCode\QRCode;
+use chillerlan\QRCode\Common\{BitBuffer, Mode};
 
 use function ord;
 
@@ -22,23 +20,63 @@ use function ord;
  * ISO/IEC 18004:2000 Section 8.3.4
  * ISO/IEC 18004:2000 Section 8.4.4
  */
-final class Byte extends QRDataAbstract{
+final class Byte extends QRDataModeAbstract{
 
-	protected int $datamode = QRCode::DATA_BYTE;
+	protected static int $datamode = Mode::DATA_BYTE;
 
-	protected array $lengthBits = [8, 16, 16];
+	/**
+	 * @inheritdoc
+	 */
+	public function getLengthInBits():int{
+		return $this->getCharCount() * 8;
+	}
 
 	/**
 	 * @inheritdoc
 	 */
-	protected function write(string $data):void{
+	public static function validateString(string $string):bool{
+		return !empty($string);
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public function write(BitBuffer $bitBuffer, int $versionNumber):void{
+		$len = $this->getCharCount();
+
+		$bitBuffer
+			->put($this::$datamode, 4)
+			->put($len, Mode::getLengthBitsForVersion($this::$datamode, $versionNumber))
+		;
+
 		$i = 0;
 
-		while($i < $this->strlen){
-			$this->bitBuffer->put(ord($data[$i]), 8);
+		while($i < $len){
+			$bitBuffer->put(ord($this->data[$i]), 8);
 			$i++;
 		}
 
 	}
 
+	/**
+	 * @inheritdoc
+	 *
+	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 */
+	public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
+		$length = $bitBuffer->read(Mode::getLengthBitsForVersion(self::$datamode, $versionNumber));
+
+		if($bitBuffer->available() < 8 * $length){
+			throw new QRCodeDataException('not enough bits available');
+		}
+
+		$readBytes = '';
+
+		for($i = 0; $i < $length; $i++){
+			$readBytes .= \chr($bitBuffer->read(8));
+		}
+
+		return $readBytes;
+	}
+
 }

+ 92 - 0
src/Data/ECI.php

@@ -0,0 +1,92 @@
+<?php
+/**
+ * Class ECI
+ *
+ * @created      20.11.2020
+ * @author       smiley <smiley@chillerlan.net>
+ * @copyright    2020 smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Data;
+
+use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode};
+
+/**
+ * Adds an ECI Designator
+ *
+ * Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment()
+ */
+final class ECI extends QRDataModeAbstract{
+
+	protected static int $datamode = Mode::DATA_ECI;
+
+	/**
+	 * The current ECI encoding id
+	 */
+	private int $encoding;
+
+	/**
+	 * @inheritDoc
+	 * @noinspection PhpMissingParentConstructorInspection
+	 */
+	public function __construct(int $encoding){
+		$this->encoding = $encoding;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function getLengthInBits():int{
+		return 8;
+	}
+
+		/**
+	 * @inheritDoc
+	 */
+	public function write(BitBuffer $bitBuffer, int $versionNumber):void{
+		$bitBuffer
+			->put($this::$datamode, 4)
+			->put($this->encoding, 8)
+		;
+	}
+
+	/**
+	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 */
+	public static function parseValue(BitBuffer $bitBuffer):ECICharset{
+		$firstByte = $bitBuffer->read(8);
+
+		if(($firstByte & 0x80) === 0){
+			// just one byte
+			return new ECICharset($firstByte & 0x7f);
+		}
+
+		if(($firstByte & 0xc0) === 0x80){
+			// two bytes
+			return new ECICharset((($firstByte & 0x3f) << 8) | $bitBuffer->read(8));
+		}
+
+		if(($firstByte & 0xe0) === 0xC0){
+			// three bytes
+			return new ECICharset((($firstByte & 0x1f) << 16) | $bitBuffer->read(16));
+		}
+
+		throw new QRCodeDataException('error decoding ECI value');
+	}
+
+	/**
+	 * @codeCoverageIgnore Unused, but required as per interface
+	 */
+	public static function validateString(string $string):bool{
+		return true;
+	}
+
+	/**
+	 * @codeCoverageIgnore Unused, but required as per interface
+	 */
+	public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
+		return '';
+	}
+
+}

+ 85 - 17
src/Data/Kanji.php

@@ -2,9 +2,7 @@
 /**
  * Class Kanji
  *
- * @filesource   Kanji.php
  * @created      25.11.2015
- * @package      chillerlan\QRCode\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT
@@ -12,9 +10,9 @@
 
 namespace chillerlan\QRCode\Data;
 
-use chillerlan\QRCode\QRCode;
+use chillerlan\QRCode\Common\{BitBuffer, Mode};
 
-use function mb_strlen, ord, sprintf, strlen;
+use function chr, implode, mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, mb_strlen, ord, sprintf, strlen;
 
 /**
  * Kanji mode: double-byte characters from the Shift JIS character set
@@ -22,17 +20,49 @@ use function mb_strlen, ord, sprintf, strlen;
  * ISO/IEC 18004:2000 Section 8.3.5
  * ISO/IEC 18004:2000 Section 8.4.5
  */
-final class Kanji extends QRDataAbstract{
+final class Kanji extends QRDataModeAbstract{
 
-	protected int $datamode = QRCode::DATA_KANJI;
+	protected static int $datamode = Mode::DATA_KANJI;
 
-	protected array $lengthBits = [8, 10, 12];
+	public function __construct(string $data){
+		parent::__construct($data);
+
+		/** @noinspection PhpFieldAssignmentTypeMismatchInspection */
+		$this->data = mb_convert_encoding($this->data, 'SJIS', mb_detect_encoding($this->data));
+	}
 
 	/**
 	 * @inheritdoc
 	 */
-	protected function getLength(string $data):int{
-		return mb_strlen($data, 'SJIS');
+	protected function getCharCount():int{
+		return mb_strlen($this->data, 'SJIS');
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public function getLengthInBits():int{
+		return $this->getCharCount() * 13;
+	}
+
+	/**
+	 * checks if a string qualifies as Kanji
+	 */
+	public static function validateString(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;
 	}
 
 	/**
@@ -40,24 +70,29 @@ final class Kanji extends QRDataAbstract{
 	 *
 	 * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
 	 */
-	protected function write(string $data):void{
-		$len = strlen($data);
+	public function write(BitBuffer $bitBuffer, int $versionNumber):void{
+
+		$bitBuffer
+			->put($this::$datamode, 4)
+			->put($this->getCharCount(), Mode::getLengthBitsForVersion($this::$datamode, $versionNumber))
+		;
+
+		$len = strlen($this->data);
 
 		for($i = 0; $i + 1 < $len; $i += 2){
-			$c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1]));
+			$c = ((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[$i + 1]));
 
-			if($c >= 0x8140 && $c <= 0x9FFC){
+			if($c >= 0x8140 && $c <= 0x9ffC){
 				$c -= 0x8140;
 			}
-			elseif($c >= 0xE040 && $c <= 0xEBBF){
-				$c -= 0xC140;
+			elseif($c >= 0xe040 && $c <= 0xebbf){
+				$c -= 0xc140;
 			}
 			else{
 				throw new QRCodeDataException(sprintf('illegal char at %d [%d]', $i + 1, $c));
 			}
 
-			$this->bitBuffer->put(((($c >> 8) & 0xff) * 0xC0) + ($c & 0xff), 13);
-
+			$bitBuffer->put(((($c >> 8) & 0xff) * 0xc0) + ($c & 0xff), 13);
 		}
 
 		if($i < $len){
@@ -66,4 +101,37 @@ final class Kanji extends QRDataAbstract{
 
 	}
 
+	/**
+	 * @inheritdoc
+	 *
+	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 */
+	public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
+		$length = $bitBuffer->read(Mode::getLengthBitsForVersion(self::$datamode, $versionNumber));
+
+		if($bitBuffer->available() < $length * 13){
+			throw new QRCodeDataException('not enough bits available');
+		}
+
+		$buffer = [];
+		$offset = 0;
+
+		while($length > 0){
+			// Each 13 bits encodes a 2-byte character
+			$twoBytes          = $bitBuffer->read(13);
+			$assembledTwoBytes = (($twoBytes / 0x0c0) << 8) | ($twoBytes % 0x0c0);
+
+			$assembledTwoBytes += ($assembledTwoBytes < 0x01f00)
+				? 0x08140  // In the 0x8140 to 0x9FFC range
+				: 0x0c140; // In the 0xE040 to 0xEBBF range
+
+			$buffer[$offset]     = chr(0xff & ($assembledTwoBytes >> 8));
+			$buffer[$offset + 1] = chr(0xff & $assembledTwoBytes);
+			$offset              += 2;
+			$length--;
+		}
+
+		return mb_convert_encoding(implode($buffer), mb_internal_encoding(), 'SJIS');
+	}
+
 }

+ 123 - 16
src/Data/Number.php

@@ -2,9 +2,7 @@
 /**
  * Class Number
  *
- * @filesource   Number.php
  * @created      26.11.2015
- * @package      chillerlan\QRCode\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT
@@ -12,9 +10,9 @@
 
 namespace chillerlan\QRCode\Data;
 
-use chillerlan\QRCode\QRCode;
+use chillerlan\QRCode\Common\{BitBuffer, Mode};
 
-use function ord, sprintf, str_split, substr;
+use function array_flip, ceil, ord, sprintf, str_split, substr;
 
 /**
  * Numeric mode: decimal digits 0 to 9
@@ -22,30 +20,66 @@ use function ord, sprintf, str_split, substr;
  * ISO/IEC 18004:2000 Section 8.3.2
  * ISO/IEC 18004:2000 Section 8.4.2
  */
-final class Number extends QRDataAbstract{
+final class Number extends QRDataModeAbstract{
 
-	protected int $datamode = QRCode::DATA_NUMBER;
+	/**
+	 * @var int[]
+	 */
+	private const CHAR_MAP_NUMBER = [
+		'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
+	];
+
+	protected static int $datamode = Mode::DATA_NUMBER;
+
+	/**
+	 * @inheritdoc
+	 */
+	public function getLengthInBits():int{
+		return (int)ceil($this->getCharCount() * (10 / 3));
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public static function validateString(string $string):bool{
+
+		foreach(str_split($string) as $chr){
+			if(!isset(self::CHAR_MAP_NUMBER[$chr])){
+				return false;
+			}
+		}
 
-	protected array $lengthBits = [10, 12, 14];
+		return true;
+	}
 
 	/**
 	 * @inheritdoc
 	 */
-	protected function write(string $data):void{
+	public function write(BitBuffer $bitBuffer, int $versionNumber):void{
+		$len = $this->getCharCount();
+
+		$bitBuffer
+			->put($this::$datamode, 4)
+			->put($len, Mode::getLengthBitsForVersion($this::$datamode, $versionNumber))
+		;
+
 		$i = 0;
 
-		while($i + 2 < $this->strlen){
-			$this->bitBuffer->put($this->parseInt(substr($data, $i, 3)), 10);
+		// encode numeric triplets in 10 bits
+		while($i + 2 < $len){
+			$bitBuffer->put($this->parseInt(substr($this->data, $i, 3)), 10);
 			$i += 3;
 		}
 
-		if($i < $this->strlen){
+		if($i < $len){
 
-			if($this->strlen - $i === 1){
-				$this->bitBuffer->put($this->parseInt(substr($data, $i, $i + 1)), 4);
+			// encode 2 remaining numbers in 7 bits
+			if($len - $i === 2){
+				$bitBuffer->put($this->parseInt(substr($this->data, $i, 2)), 7);
 			}
-			elseif($this->strlen - $i === 2){
-				$this->bitBuffer->put($this->parseInt(substr($data, $i, $i + 2)), 7);
+			// encode one remaining number in 4 bits
+			elseif($len - $i === 1){
+				$bitBuffer->put($this->parseInt(substr($this->data, $i, 1)), 4);
 			}
 
 		}
@@ -63,7 +97,7 @@ final class Number extends QRDataAbstract{
 		foreach(str_split($string) as $chr){
 			$c = ord($chr);
 
-			if(!isset($this::CHAR_MAP_NUMBER[$chr])){
+			if(!isset(self::CHAR_MAP_NUMBER[$chr])){
 				throw new QRCodeDataException(sprintf('illegal char: "%s" [%d]', $chr, $c));
 			}
 
@@ -74,4 +108,77 @@ final class Number extends QRDataAbstract{
 		return $num;
 	}
 
+	/**
+	 * @inheritdoc
+	 *
+	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 */
+	public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
+		$length  = $bitBuffer->read(Mode::getLengthBitsForVersion(self::$datamode, $versionNumber));
+		$charmap = array_flip(self::CHAR_MAP_NUMBER);
+
+		// @todo
+		$toNumericChar = function(int $ord) use ($charmap):string{
+
+			if(isset($charmap[$ord])){
+				return $charmap[$ord];
+			}
+
+			throw new QRCodeDataException('invalid character value: '.$ord);
+		};
+
+		$result = '';
+		// Read three digits at a time
+		while($length >= 3){
+			// Each 10 bits encodes three digits
+			if($bitBuffer->available() < 10){
+				throw new QRCodeDataException('not enough bits available');
+			}
+
+			$threeDigitsBits = $bitBuffer->read(10);
+
+			if($threeDigitsBits >= 1000){
+				throw new QRCodeDataException('error decoding numeric value');
+			}
+
+			$result .= $toNumericChar($threeDigitsBits / 100);
+			$result .= $toNumericChar(($threeDigitsBits / 10) % 10);
+			$result .= $toNumericChar($threeDigitsBits % 10);
+
+			$length -= 3;
+		}
+
+		if($length === 2){
+			// Two digits left over to read, encoded in 7 bits
+			if($bitBuffer->available() < 7){
+				throw new QRCodeDataException('not enough bits available');
+			}
+
+			$twoDigitsBits = $bitBuffer->read(7);
+
+			if($twoDigitsBits >= 100){
+				throw new QRCodeDataException('error decoding numeric value');
+			}
+
+			$result .= $toNumericChar($twoDigitsBits / 10);
+			$result .= $toNumericChar($twoDigitsBits % 10);
+		}
+		elseif($length === 1){
+			// One digit left over to read
+			if($bitBuffer->available() < 4){
+				throw new QRCodeDataException('not enough bits available');
+			}
+
+			$digitBits = $bitBuffer->read(4);
+
+			if($digitBits >= 10){
+				throw new QRCodeDataException('error decoding numeric value');
+			}
+
+			$result .= $toNumericChar($digitBits);
+		}
+
+		return $result;
+	}
+
 }

+ 0 - 2
src/Data/QRCodeDataException.php

@@ -2,9 +2,7 @@
 /**
  * Class QRCodeDataException
  *
- * @filesource   QRCodeDataException.php
  * @created      09.12.2015
- * @package      chillerlan\QRCode\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT

+ 213 - 0
src/Data/QRData.php

@@ -0,0 +1,213 @@
+<?php
+/**
+ * Class QRData
+ *
+ * @created      25.11.2015
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2015 Smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Data;
+
+use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Mode, ReedSolomonEncoder, Version};
+use chillerlan\QRCode\QRCode;
+use chillerlan\Settings\SettingsContainerInterface;
+
+use function range, sprintf;
+
+/**
+ * Processes the binary data and maps it on a matrix which is then being returned
+ */
+final class QRData{
+
+	/**
+	 * the options instance
+	 *
+	 * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
+	 */
+	private SettingsContainerInterface $options;
+
+	/**
+	 * a BitBuffer instance
+	 */
+	private BitBuffer $bitBuffer;
+
+	/**
+	 * an EccLevel instance
+	 */
+	private EccLevel $eccLevel;
+
+	/**
+	 * current QR Code version
+	 */
+	private Version $version;
+
+	/**
+	 * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
+	 */
+	private array $dataSegments = [];
+
+	/**
+	 * Max bits for the current ECC mode
+	 *
+	 * @var int[]
+	 */
+	private array $maxBitsForEcc;
+
+	/**
+	 * QRData constructor.
+	 *
+	 * @param \chillerlan\Settings\SettingsContainerInterface    $options
+	 * @param \chillerlan\QRCode\Data\QRDataModeInterface[]|null $dataSegments
+	 */
+	public function __construct(SettingsContainerInterface $options, array $dataSegments = null){
+		$this->options       = $options;
+		$this->bitBuffer     = new BitBuffer;
+		$this->eccLevel      = new EccLevel($this->options->eccLevel);
+		$this->maxBitsForEcc = $this->eccLevel->getMaxBits();
+
+		if(!empty($dataSegments)){
+			$this->setData($dataSegments);
+		}
+
+	}
+
+	/**
+	 * Sets the data string (internally called by the constructor)
+	 */
+	public function setData(array $dataSegments):QRData{
+		$this->dataSegments = $dataSegments;
+
+		$version = $this->options->version === QRCode::VERSION_AUTO
+			? $this->getMinimumVersion()
+			: $this->options->version;
+
+		$this->version = new Version($version);
+
+		$this->writeBitBuffer();
+
+		return $this;
+	}
+
+	/**
+	 * returns a fresh matrix object with the data written for the given $maskPattern
+	 */
+	public function writeMatrix(MaskPattern $maskPattern, bool $test = null):QRMatrix{
+		$data = (new ReedSolomonEncoder)->interleaveEcBytes($this->bitBuffer, $this->version, $this->eccLevel);
+
+		return (new QRMatrix($this->version, $this->eccLevel))
+			->init($maskPattern, $test)
+			->mapData($data)
+			->mask($maskPattern)
+		;
+	}
+
+	/**
+	 * estimates the total length of the several mode segments in order to guess the minimum version
+	 *
+	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 */
+	private function estimateTotalBitLength():int{
+		$length = 0;
+		$margin = 0;
+
+		foreach($this->dataSegments as $segment){
+			// data length in bits of the current segment +4 bits for each mode descriptor
+			$length += ($segment->getLengthInBits() + Mode::getLengthBitsForMode($segment->getDataMode())[0] + 4);
+
+			if(!$segment instanceof ECI){
+				// mode length bits margin to the next breakpoint
+				$margin += ($segment instanceof Byte ? 8 : 2);
+			}
+		}
+
+		foreach([9, 26, 40] as $breakpoint){
+
+			// length bits for the first breakpoint have already been added
+			if($breakpoint > 9){
+				$length += $margin;
+			}
+
+			if($length < $this->maxBitsForEcc[$breakpoint]){
+				return $length;
+			}
+		}
+
+		throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
+	}
+
+	/**
+	 * returns the minimum version number for the given string
+	 *
+	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 */
+	private function getMinimumVersion():int{
+		$total = $this->estimateTotalBitLength();
+
+		// guess the version number within the given range
+		foreach(range($this->options->versionMin, $this->options->versionMax) as $version){
+
+			if($total <= $this->maxBitsForEcc[$version]){
+				return $version;
+			}
+		}
+
+		// it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
+		throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
+	}
+
+	/**
+	 * creates a BitBuffer and writes the string data to it
+	 *
+	 * @throws \chillerlan\QRCode\QRCodeException on data overflow
+	 */
+	private function writeBitBuffer():void{
+		$version  = $this->version->getVersionNumber();
+		$MAX_BITS = $this->maxBitsForEcc[$version];
+
+		foreach($this->dataSegments as $segment){
+			$segment->write($this->bitBuffer, $version);
+		}
+
+		// overflow, likely caused due to invalid version setting
+		if($this->bitBuffer->getLength() > $MAX_BITS){
+			throw new QRCodeDataException(
+				sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
+			);
+		}
+
+		// add terminator (ISO/IEC 18004:2000 Table 2)
+		if($this->bitBuffer->getLength() + 4 <= $MAX_BITS){
+			$this->bitBuffer->put(0b0000, 4);
+		}
+
+		// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
+
+		// if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
+		// by the addition of padding bits with binary value 0
+		while($this->bitBuffer->getLength() % 8 !== 0){
+			$this->bitBuffer->putBit(false);
+		}
+
+		// The message bit stream shall then be extended to fill the data capacity of the symbol
+		// corresponding to the Version and Error Correction Level, by the addition of the Pad
+		// Codewords 11101100 and 00010001 alternately.
+		while(true){
+
+			if($this->bitBuffer->getLength() >= $MAX_BITS){
+				break;
+			}
+
+			$this->bitBuffer->put(0b11101100, 8);
+
+			if($this->bitBuffer->getLength() >= $MAX_BITS){
+				break;
+			}
+
+			$this->bitBuffer->put(0b00010001, 8);
+		}
+
+	}
+
+}

+ 0 - 311
src/Data/QRDataAbstract.php

@@ -1,311 +0,0 @@
-<?php
-/**
- * Class QRDataAbstract
- *
- * @filesource   QRDataAbstract.php
- * @created      25.11.2015
- * @package      chillerlan\QRCode\Data
- * @author       Smiley <smiley@chillerlan.net>
- * @copyright    2015 Smiley
- * @license      MIT
- */
-
-namespace chillerlan\QRCode\Data;
-
-use chillerlan\QRCode\QRCode;
-use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial};
-use chillerlan\Settings\SettingsContainerInterface;
-
-use function array_fill, array_merge, count, max, mb_convert_encoding, mb_detect_encoding, range, sprintf, strlen;
-
-/**
- * Processes the binary data and maps it on a matrix which is then being returned
- */
-abstract class QRDataAbstract implements QRDataInterface{
-
-	/**
-	 * the string byte count
-	 */
-	protected ?int $strlen = null;
-
-	/**
-	 * the current data mode: Num, Alphanum, Kanji, Byte
-	 */
-	protected int $datamode;
-
-	/**
-	 * mode length bits for the version breakpoints 1-9, 10-26 and 27-40
-	 *
-	 * ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator
-	 */
-	protected array $lengthBits = [0, 0, 0];
-
-	/**
-	 * current QR Code version
-	 */
-	protected int $version;
-
-	/**
-	 * ECC temp data
-	 */
-	protected array $ecdata;
-
-	/**
-	 * ECC temp data
-	 */
-	protected array $dcdata;
-
-	/**
-	 * the options instance
-	 *
-	 * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
-	 */
-	protected SettingsContainerInterface $options;
-
-	/**
-	 * a BitBuffer instance
-	 */
-	protected BitBuffer $bitBuffer;
-
-	/**
-	 * QRDataInterface constructor.
-	 */
-	public function __construct(SettingsContainerInterface $options, string $data = null){
-		$this->options = $options;
-
-		if($data !== null){
-			$this->setData($data);
-		}
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	public function setData(string $data):QRDataInterface{
-
-		if($this->datamode === QRCode::DATA_KANJI){
-			$data = mb_convert_encoding($data, 'SJIS', mb_detect_encoding($data));
-		}
-
-		$this->strlen  = $this->getLength($data);
-		$this->version = $this->options->version === QRCode::VERSION_AUTO
-			? $this->getMinimumVersion()
-			: $this->options->version;
-
-		$this->writeBitBuffer($data);
-
-		return $this;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{
-		return (new QRMatrix($this->version, $this->options->eccLevel))
-			->init($maskPattern, $test)
-			->mapData($this->maskECC(), $maskPattern)
-		;
-	}
-
-	/**
-	 * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40
-	 *
-	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
-	 * @codeCoverageIgnore
-	 */
-	protected function getLengthBits():int{
-
-		 foreach([9, 26, 40] as $key => $breakpoint){
-			 if($this->version <= $breakpoint){
-				 return $this->lengthBits[$key];
-			 }
-		 }
-
-		throw new QRCodeDataException(sprintf('invalid version number: %d', $this->version));
-	}
-
-	/**
-	 * returns the byte count of the $data string
-	 */
-	protected function getLength(string $data):int{
-		return strlen($data);
-	}
-
-	/**
-	 * returns the minimum version number for the given string
-	 *
-	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
-	 */
-	protected function getMinimumVersion():int{
-		$maxlength = 0;
-
-		// guess the version number within the given range
-		$dataMode = QRCode::DATA_MODES[$this->datamode];
-		$eccMode  = QRCode::ECC_MODES[$this->options->eccLevel];
-
-		foreach(range($this->options->versionMin, $this->options->versionMax) as $version){
-			$maxlength = $this::MAX_LENGTH[$version][$dataMode][$eccMode];
-
-			if($this->strlen <= $maxlength){
-				return $version;
-			}
-		}
-
-		throw new QRCodeDataException(sprintf('data exceeds %d characters', $maxlength));
-	}
-
-	/**
-	 * writes the actual data string to the BitBuffer
-	 *
-	 * @see \chillerlan\QRCode\Data\QRDataAbstract::writeBitBuffer()
-	 */
-	abstract protected function write(string $data):void;
-
-	/**
-	 * creates a BitBuffer and writes the string data to it
-	 *
-	 * @throws \chillerlan\QRCode\QRCodeException on data overflow
-	 */
-	protected function writeBitBuffer(string $data):void{
-		$this->bitBuffer = new BitBuffer;
-
-		$MAX_BITS = $this::MAX_BITS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
-
-		$this->bitBuffer
-			->put($this->datamode, 4)
-			->put($this->strlen, $this->getLengthBits())
-		;
-
-		$this->write($data);
-
-		// overflow, likely caused due to invalid version setting
-		if($this->bitBuffer->getLength() > $MAX_BITS){
-			throw new QRCodeDataException(sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS));
-		}
-
-		// add terminator (ISO/IEC 18004:2000 Table 2)
-		if($this->bitBuffer->getLength() + 4 <= $MAX_BITS){
-			$this->bitBuffer->put(0, 4);
-		}
-
-		// padding
-		while($this->bitBuffer->getLength() % 8 !== 0){
-			$this->bitBuffer->putBit(false);
-		}
-
-		// padding
-		while(true){
-
-			if($this->bitBuffer->getLength() >= $MAX_BITS){
-				break;
-			}
-
-			$this->bitBuffer->put(0xEC, 8);
-
-			if($this->bitBuffer->getLength() >= $MAX_BITS){
-				break;
-			}
-
-			$this->bitBuffer->put(0x11, 8);
-		}
-
-	}
-
-	/**
-	 * ECC masking
-	 *
-	 * ISO/IEC 18004:2000 Section 8.5 ff
-	 *
-	 * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
-	 */
-	protected function maskECC():array{
-		[$l1, $l2, $b1, $b2] = $this::RSBLOCKS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
-
-		$rsBlocks     = array_fill(0, $l1, [$b1, $b2]);
-		$rsCount      = $l1 + $l2;
-		$this->ecdata = array_fill(0, $rsCount, []);
-		$this->dcdata = $this->ecdata;
-
-		if($l2 > 0){
-			$rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1]));
-		}
-
-		$totalCodeCount = 0;
-		$maxDcCount     = 0;
-		$maxEcCount     = 0;
-		$offset         = 0;
-
-		$bitBuffer = $this->bitBuffer->getBuffer();
-
-		foreach($rsBlocks as $key => $block){
-			[$rsBlockTotal, $dcCount] = $block;
-
-			$ecCount            = $rsBlockTotal - $dcCount;
-			$maxDcCount         = max($maxDcCount, $dcCount);
-			$maxEcCount         = max($maxEcCount, $ecCount);
-			$this->dcdata[$key] = array_fill(0, $dcCount, null);
-
-			foreach($this->dcdata[$key] as $a => $_z){
-				$this->dcdata[$key][$a] = 0xff & $bitBuffer[$a + $offset];
-			}
-
-			[$num, $add] = $this->poly($key, $ecCount);
-
-			foreach($this->ecdata[$key] as $c => $_){
-				$modIndex               = $c + $add;
-				$this->ecdata[$key][$c] = $modIndex >= 0 ? $num[$modIndex] : 0;
-			}
-
-			$offset         += $dcCount;
-			$totalCodeCount += $rsBlockTotal;
-		}
-
-		$data  = array_fill(0, $totalCodeCount, null);
-		$index = 0;
-
-		$mask = function(array $arr, int $count) use (&$data, &$index, $rsCount):void{
-			for($x = 0; $x < $count; $x++){
-				for($y = 0; $y < $rsCount; $y++){
-					if($x < count($arr[$y])){
-						$data[$index] = $arr[$y][$x];
-						$index++;
-					}
-				}
-			}
-		};
-
-		$mask($this->dcdata, $maxDcCount);
-		$mask($this->ecdata, $maxEcCount);
-
-		return $data;
-	}
-
-	/**
-	 * helper method for the polynomial operations
-	 */
-	protected function poly(int $key, int $count):array{
-		$rsPoly  = new Polynomial;
-		$modPoly = new Polynomial;
-
-		for($i = 0; $i < $count; $i++){
-			$modPoly->setNum([1, $modPoly->gexp($i)]);
-			$rsPoly->multiply($modPoly->getNum());
-		}
-
-		$rsPolyCount = count($rsPoly->getNum());
-
-		$modPoly
-			->setNum($this->dcdata[$key], $rsPolyCount - 1)
-			->mod($rsPoly->getNum())
-		;
-
-		$this->ecdata[$key] = array_fill(0, $rsPolyCount - 1, null);
-		$num                = $modPoly->getNum();
-
-		return [
-			$num,
-			count($num) - count($this->ecdata[$key]),
-		];
-	}
-
-}

+ 0 - 200
src/Data/QRDataInterface.php

@@ -1,200 +0,0 @@
-<?php
-/**
- * Interface QRDataInterface
- *
- * @filesource   QRDataInterface.php
- * @created      01.12.2015
- * @package      chillerlan\QRCode\Data
- * @author       Smiley <smiley@chillerlan.net>
- * @copyright    2015 Smiley
- * @license      MIT
- */
-
-namespace chillerlan\QRCode\Data;
-
-/**
- * Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji)
- * and holds version information in several constants
- */
-interface QRDataInterface{
-
-	/**
-	 * @var int[]
-	 */
-	const CHAR_MAP_NUMBER = [
-		'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
-	];
-
-	/**
-	 * ISO/IEC 18004:2000 Table 5
-	 *
-	 * @var int[]
-	 */
-	const CHAR_MAP_ALPHANUM = [
-		'0' =>  0, '1' =>  1, '2' =>  2, '3' =>  3, '4' =>  4, '5' =>  5, '6' =>  6, '7' =>  7,
-		'8' =>  8, '9' =>  9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15,
-		'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23,
-		'O' => 24, 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29, 'U' => 30, 'V' => 31,
-		'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35, ' ' => 36, '$' => 37, '%' => 38, '*' => 39,
-		'+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44,
-	];
-
-	/**
-	 * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
-	 *
-	 * @see http://www.qrcode.com/en/about/version.html
-	 *
-	 * @var int [][][]
-	 */
-	const MAX_LENGTH =[
-	//	v  => [NUMERIC => [L, M, Q, H ], ALPHANUM => [L, M, Q, H], BINARY => [L, M, Q, H  ], KANJI => [L, M, Q, H   ]]  // modules
-		1  => [[  41,   34,   27,   17], [  25,   20,   16,   10], [  17,   14,   11,    7], [  10,    8,    7,    4]], //  21
-		2  => [[  77,   63,   48,   34], [  47,   38,   29,   20], [  32,   26,   20,   14], [  20,   16,   12,    8]], //  25
-		3  => [[ 127,  101,   77,   58], [  77,   61,   47,   35], [  53,   42,   32,   24], [  32,   26,   20,   15]], //  29
-		4  => [[ 187,  149,  111,   82], [ 114,   90,   67,   50], [  78,   62,   46,   34], [  48,   38,   28,   21]], //  33
-		5  => [[ 255,  202,  144,  106], [ 154,  122,   87,   64], [ 106,   84,   60,   44], [  65,   52,   37,   27]], //  37
-		6  => [[ 322,  255,  178,  139], [ 195,  154,  108,   84], [ 134,  106,   74,   58], [  82,   65,   45,   36]], //  41
-		7  => [[ 370,  293,  207,  154], [ 224,  178,  125,   93], [ 154,  122,   86,   64], [  95,   75,   53,   39]], //  45
-		8  => [[ 461,  365,  259,  202], [ 279,  221,  157,  122], [ 192,  152,  108,   84], [ 118,   93,   66,   52]], //  49
-		9  => [[ 552,  432,  312,  235], [ 335,  262,  189,  143], [ 230,  180,  130,   98], [ 141,  111,   80,   60]], //  53
-		10 => [[ 652,  513,  364,  288], [ 395,  311,  221,  174], [ 271,  213,  151,  119], [ 167,  131,   93,   74]], //  57
-		11 => [[ 772,  604,  427,  331], [ 468,  366,  259,  200], [ 321,  251,  177,  137], [ 198,  155,  109,   85]], //  61
-		12 => [[ 883,  691,  489,  374], [ 535,  419,  296,  227], [ 367,  287,  203,  155], [ 226,  177,  125,   96]], //  65
-		13 => [[1022,  796,  580,  427], [ 619,  483,  352,  259], [ 425,  331,  241,  177], [ 262,  204,  149,  109]], //  69 NICE!
-		14 => [[1101,  871,  621,  468], [ 667,  528,  376,  283], [ 458,  362,  258,  194], [ 282,  223,  159,  120]], //  73
-		15 => [[1250,  991,  703,  530], [ 758,  600,  426,  321], [ 520,  412,  292,  220], [ 320,  254,  180,  136]], //  77
-		16 => [[1408, 1082,  775,  602], [ 854,  656,  470,  365], [ 586,  450,  322,  250], [ 361,  277,  198,  154]], //  81
-		17 => [[1548, 1212,  876,  674], [ 938,  734,  531,  408], [ 644,  504,  364,  280], [ 397,  310,  224,  173]], //  85
-		18 => [[1725, 1346,  948,  746], [1046,  816,  574,  452], [ 718,  560,  394,  310], [ 442,  345,  243,  191]], //  89
-		19 => [[1903, 1500, 1063,  813], [1153,  909,  644,  493], [ 792,  624,  442,  338], [ 488,  384,  272,  208]], //  93
-		20 => [[2061, 1600, 1159,  919], [1249,  970,  702,  557], [ 858,  666,  482,  382], [ 528,  410,  297,  235]], //  97
-		21 => [[2232, 1708, 1224,  969], [1352, 1035,  742,  587], [ 929,  711,  509,  403], [ 572,  438,  314,  248]], // 101
-		22 => [[2409, 1872, 1358, 1056], [1460, 1134,  823,  640], [1003,  779,  565,  439], [ 618,  480,  348,  270]], // 105
-		23 => [[2620, 2059, 1468, 1108], [1588, 1248,  890,  672], [1091,  857,  611,  461], [ 672,  528,  376,  284]], // 109
-		24 => [[2812, 2188, 1588, 1228], [1704, 1326,  963,  744], [1171,  911,  661,  511], [ 721,  561,  407,  315]], // 113
-		25 => [[3057, 2395, 1718, 1286], [1853, 1451, 1041,  779], [1273,  997,  715,  535], [ 784,  614,  440,  330]], // 117
-		26 => [[3283, 2544, 1804, 1425], [1990, 1542, 1094,  864], [1367, 1059,  751,  593], [ 842,  652,  462,  365]], // 121
-		27 => [[3517, 2701, 1933, 1501], [2132, 1637, 1172,  910], [1465, 1125,  805,  625], [ 902,  692,  496,  385]], // 125
-		28 => [[3669, 2857, 2085, 1581], [2223, 1732, 1263,  958], [1528, 1190,  868,  658], [ 940,  732,  534,  405]], // 129
-		29 => [[3909, 3035, 2181, 1677], [2369, 1839, 1322, 1016], [1628, 1264,  908,  698], [1002,  778,  559,  430]], // 133
-		30 => [[4158, 3289, 2358, 1782], [2520, 1994, 1429, 1080], [1732, 1370,  982,  742], [1066,  843,  604,  457]], // 137
-		31 => [[4417, 3486, 2473, 1897], [2677, 2113, 1499, 1150], [1840, 1452, 1030,  790], [1132,  894,  634,  486]], // 141
-		32 => [[4686, 3693, 2670, 2022], [2840, 2238, 1618, 1226], [1952, 1538, 1112,  842], [1201,  947,  684,  518]], // 145
-		33 => [[4965, 3909, 2805, 2157], [3009, 2369, 1700, 1307], [2068, 1628, 1168,  898], [1273, 1002,  719,  553]], // 149
-		34 => [[5253, 4134, 2949, 2301], [3183, 2506, 1787, 1394], [2188, 1722, 1228,  958], [1347, 1060,  756,  590]], // 153
-		35 => [[5529, 4343, 3081, 2361], [3351, 2632, 1867, 1431], [2303, 1809, 1283,  983], [1417, 1113,  790,  605]], // 157
-		36 => [[5836, 4588, 3244, 2524], [3537, 2780, 1966, 1530], [2431, 1911, 1351, 1051], [1496, 1176,  832,  647]], // 161
-		37 => [[6153, 4775, 3417, 2625], [3729, 2894, 2071, 1591], [2563, 1989, 1423, 1093], [1577, 1224,  876,  673]], // 165
-		38 => [[6479, 5039, 3599, 2735], [3927, 3054, 2181, 1658], [2699, 2099, 1499, 1139], [1661, 1292,  923,  701]], // 169
-		39 => [[6743, 5313, 3791, 2927], [4087, 3220, 2298, 1774], [2809, 2213, 1579, 1219], [1729, 1362,  972,  750]], // 173
-		40 => [[7089, 5596, 3993, 3057], [4296, 3391, 2420, 1852], [2953, 2331, 1663, 1273], [1817, 1435, 1024,  784]], // 177
-	];
-
-	/**
-	 * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
-	 *
-	 * @var int [][]
-	 */
-	const MAX_BITS = [
-		// version => [L, M, Q, H ]
-		1  => [  152,   128,   104,    72],
-		2  => [  272,   224,   176,   128],
-		3  => [  440,   352,   272,   208],
-		4  => [  640,   512,   384,   288],
-		5  => [  864,   688,   496,   368],
-		6  => [ 1088,   864,   608,   480],
-		7  => [ 1248,   992,   704,   528],
-		8  => [ 1552,  1232,   880,   688],
-		9  => [ 1856,  1456,  1056,   800],
-		10 => [ 2192,  1728,  1232,   976],
-		11 => [ 2592,  2032,  1440,  1120],
-		12 => [ 2960,  2320,  1648,  1264],
-		13 => [ 3424,  2672,  1952,  1440],
-		14 => [ 3688,  2920,  2088,  1576],
-		15 => [ 4184,  3320,  2360,  1784],
-		16 => [ 4712,  3624,  2600,  2024],
-		17 => [ 5176,  4056,  2936,  2264],
-		18 => [ 5768,  4504,  3176,  2504],
-		19 => [ 6360,  5016,  3560,  2728],
-		20 => [ 6888,  5352,  3880,  3080],
-		21 => [ 7456,  5712,  4096,  3248],
-		22 => [ 8048,  6256,  4544,  3536],
-		23 => [ 8752,  6880,  4912,  3712],
-		24 => [ 9392,  7312,  5312,  4112],
-		25 => [10208,  8000,  5744,  4304],
-		26 => [10960,  8496,  6032,  4768],
-		27 => [11744,  9024,  6464,  5024],
-		28 => [12248,  9544,  6968,  5288],
-		29 => [13048, 10136,  7288,  5608],
-		30 => [13880, 10984,  7880,  5960],
-		31 => [14744, 11640,  8264,  6344],
-		32 => [15640, 12328,  8920,  6760],
-		33 => [16568, 13048,  9368,  7208],
-		34 => [17528, 13800,  9848,  7688],
-		35 => [18448, 14496, 10288,  7888],
-		36 => [19472, 15312, 10832,  8432],
-		37 => [20528, 15936, 11408,  8768],
-		38 => [21616, 16816, 12016,  9136],
-		39 => [22496, 17728, 12656,  9776],
-		40 => [23648, 18672, 13328, 10208],
-	];
-
-	/**
-	 * @see http://www.thonky.com/qr-code-tutorial/error-correction-table
-	 *
-	 * @var int [][][]
-	 */
-	const RSBLOCKS = [
-		1  => [[ 1,  0,  26,  19], [ 1,  0, 26, 16], [ 1,  0, 26, 13], [ 1,  0, 26,  9]],
-		2  => [[ 1,  0,  44,  34], [ 1,  0, 44, 28], [ 1,  0, 44, 22], [ 1,  0, 44, 16]],
-		3  => [[ 1,  0,  70,  55], [ 1,  0, 70, 44], [ 2,  0, 35, 17], [ 2,  0, 35, 13]],
-		4  => [[ 1,  0, 100,  80], [ 2,  0, 50, 32], [ 2,  0, 50, 24], [ 4,  0, 25,  9]],
-		5  => [[ 1,  0, 134, 108], [ 2,  0, 67, 43], [ 2,  2, 33, 15], [ 2,  2, 33, 11]],
-		6  => [[ 2,  0,  86,  68], [ 4,  0, 43, 27], [ 4,  0, 43, 19], [ 4,  0, 43, 15]],
-		7  => [[ 2,  0,  98,  78], [ 4,  0, 49, 31], [ 2,  4, 32, 14], [ 4,  1, 39, 13]],
-		8  => [[ 2,  0, 121,  97], [ 2,  2, 60, 38], [ 4,  2, 40, 18], [ 4,  2, 40, 14]],
-		9  => [[ 2,  0, 146, 116], [ 3,  2, 58, 36], [ 4,  4, 36, 16], [ 4,  4, 36, 12]],
-		10 => [[ 2,  2,  86,  68], [ 4,  1, 69, 43], [ 6,  2, 43, 19], [ 6,  2, 43, 15]],
-		11 => [[ 4,  0, 101,  81], [ 1,  4, 80, 50], [ 4,  4, 50, 22], [ 3,  8, 36, 12]],
-		12 => [[ 2,  2, 116,  92], [ 6,  2, 58, 36], [ 4,  6, 46, 20], [ 7,  4, 42, 14]],
-		13 => [[ 4,  0, 133, 107], [ 8,  1, 59, 37], [ 8,  4, 44, 20], [12,  4, 33, 11]],
-		14 => [[ 3,  1, 145, 115], [ 4,  5, 64, 40], [11,  5, 36, 16], [11,  5, 36, 12]],
-		15 => [[ 5,  1, 109,  87], [ 5,  5, 65, 41], [ 5,  7, 54, 24], [11,  7, 36, 12]],
-		16 => [[ 5,  1, 122,  98], [ 7,  3, 73, 45], [15,  2, 43, 19], [ 3, 13, 45, 15]],
-		17 => [[ 1,  5, 135, 107], [10,  1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]],
-		18 => [[ 5,  1, 150, 120], [ 9,  4, 69, 43], [17,  1, 50, 22], [ 2, 19, 42, 14]],
-		19 => [[ 3,  4, 141, 113], [ 3, 11, 70, 44], [17,  4, 47, 21], [ 9, 16, 39, 13]],
-		20 => [[ 3,  5, 135, 107], [ 3, 13, 67, 41], [15,  5, 54, 24], [15, 10, 43, 15]],
-		21 => [[ 4,  4, 144, 116], [17,  0, 68, 42], [17,  6, 50, 22], [19,  6, 46, 16]],
-		22 => [[ 2,  7, 139, 111], [17,  0, 74, 46], [ 7, 16, 54, 24], [34,  0, 37, 13]],
-		23 => [[ 4,  5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]],
-		24 => [[ 6,  4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30,  2, 46, 16]],
-		25 => [[ 8,  4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]],
-		26 => [[10,  2, 142, 114], [19,  4, 74, 46], [28,  6, 50, 22], [33,  4, 46, 16]],
-		27 => [[ 8,  4, 152, 122], [22,  3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]],
-		28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]],
-		29 => [[ 7,  7, 146, 116], [21,  7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]],
-		30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]],
-		31 => [[13,  3, 145, 115], [ 2, 29, 74, 46], [42,  1, 54, 24], [23, 28, 45, 15]],
-		32 => [[17,  0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]],
-		33 => [[17,  1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]],
-		34 => [[13,  6, 145, 115], [14, 23, 74, 46], [44,  7, 54, 24], [59,  1, 46, 16]],
-		35 => [[12,  7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]],
-		36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]],
-		37 => [[17,  4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]],
-		38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]],
-		39 => [[20,  4, 147, 117], [40,  7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]],
-		40 => [[19,  6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]],
-	];
-
-	/**
-	 * Sets the data string (internally called by the constructor)
-	 */
-	public function setData(string $data):QRDataInterface;
-
-	/**
-	 * returns a fresh matrix object with the data written for the given $maskPattern
-	 */
-	public function initMatrix(int $maskPattern, bool $test = null):QRMatrix;
-
-}

+ 48 - 0
src/Data/QRDataModeAbstract.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * Class QRDataModeAbstract
+ *
+ * @created      19.11.2020
+ * @author       smiley <smiley@chillerlan.net>
+ * @copyright    2020 smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Data;
+
+/**
+ */
+abstract class QRDataModeAbstract implements QRDataModeInterface{
+
+	/**
+	 * the current data mode: Num, Alphanum, Kanji, Byte
+	 */
+	protected static int $datamode;
+
+	/**
+	 * The data to write
+	 */
+	protected string $data;
+
+	/**
+	 * QRDataModeAbstract constructor.
+	 */
+	public function __construct(string $data){
+		$this->data = $data;
+	}
+
+	/**
+	 * returns the character count of the $data string
+	 */
+	protected function getCharCount():int{
+		return strlen($this->data);
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function getDataMode():int{
+		return $this::$datamode;
+	}
+
+}

+ 48 - 0
src/Data/QRDataModeInterface.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * Interface QRDataModeInterface
+ *
+ * @created      01.12.2015
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2015 Smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Data;
+
+use chillerlan\QRCode\Common\BitBuffer;
+
+/**
+ * Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji)
+ * and holds version information in several constants
+ */
+interface QRDataModeInterface{
+
+	/**
+	 * returns the current data mode constant
+	 */
+	public function getDataMode():int;
+
+	/**
+	 * retruns the length in bits of the data string
+	 */
+	public function getLengthInBits():int;
+
+	/**
+	 * checks if the given string qualifies for the encoder module
+	 */
+	public static function validateString(string $string):bool;
+
+	/**
+	 * writes the actual data string to the BitBuffer, uses the given version to determine the length bits
+	 *
+	 * @see \chillerlan\QRCode\Data\QRData::writeBitBuffer()
+	 */
+	public function write(BitBuffer $bitBuffer, int $versionNumber):void;
+
+	/**
+	 * reads a segment from the BitBuffer and decodes in the current data mode
+	 */
+	public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string;
+
+}

+ 88 - 245
src/Data/QRMatrix.php

@@ -2,9 +2,7 @@
 /**
  * Class QRMatrix
  *
- * @filesource   QRMatrix.php
  * @created      15.11.2017
- * @package      chillerlan\QRCode\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -12,10 +10,10 @@
 
 namespace chillerlan\QRCode\Data;
 
-use chillerlan\QRCode\QRCode;
-use Closure;
+use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version};
 
-use function array_fill, array_key_exists, array_push, array_unshift, count, floor, in_array, max, min, range;
+use SplFixedArray;
+use function array_fill, array_push, array_unshift, floor, max, min, range;
 
 /**
  * Holds a numerical representation of the final QR Code;
@@ -26,228 +24,75 @@ use function array_fill, array_key_exists, array_push, array_unshift, count, flo
 final class QRMatrix{
 
 	/** @var int */
-	public const M_NULL       = 0x00;
+	public const M_NULL       = 0b000000000000;
 	/** @var int */
-	public const M_DARKMODULE = 0x02;
+	public const M_DARKMODULE = 0b000000000001;
 	/** @var int */
-	public const M_DATA       = 0x04;
+	public const M_DATA       = 0b000000000010;
 	/** @var int */
-	public const M_FINDER     = 0x06;
+	public const M_FINDER     = 0b000000000100;
 	/** @var int */
-	public const M_SEPARATOR  = 0x08;
+	public const M_SEPARATOR  = 0b000000001000;
 	/** @var int */
-	public const M_ALIGNMENT  = 0x0a;
+	public const M_ALIGNMENT  = 0b000000010000;
 	/** @var int */
-	public const M_TIMING     = 0x0c;
+	public const M_TIMING     = 0b000000100000;
 	/** @var int */
-	public const M_FORMAT     = 0x0e;
+	public const M_FORMAT     = 0b000001000000;
 	/** @var int */
-	public const M_VERSION    = 0x10;
+	public const M_VERSION    = 0b000010000000;
 	/** @var int */
-	public const M_QUIETZONE  = 0x12;
+	public const M_QUIETZONE  = 0b000100000000;
 	/** @var int */
-	public const M_LOGO       = 0x14;
+	public const M_LOGO       = 0b001000000000;
 	/** @var int */
-	public const M_FINDER_DOT = 0x16;
+	public const M_FINDER_DOT = 0b010000000000;
 	/** @var int */
-	public const M_TEST       = 0xff;
+	public const M_TEST       = 0b011111111111;
+	/** @var int */
+	public const IS_DARK      = 0b100000000000;
 
 	/**
-	 * ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns
-	 *
-	 * version -> pattern
-	 *
-	 * @var int[][]
+	 * the used mask pattern, set via QRMatrix::mask()
 	 */
-	protected const alignmentPattern = [
-		1  => [],
-		2  => [6, 18],
-		3  => [6, 22],
-		4  => [6, 26],
-		5  => [6, 30],
-		6  => [6, 34],
-		7  => [6, 22, 38],
-		8  => [6, 24, 42],
-		9  => [6, 26, 46],
-		10 => [6, 28, 50],
-		11 => [6, 30, 54],
-		12 => [6, 32, 58],
-		13 => [6, 34, 62],
-		14 => [6, 26, 46, 66],
-		15 => [6, 26, 48, 70],
-		16 => [6, 26, 50, 74],
-		17 => [6, 30, 54, 78],
-		18 => [6, 30, 56, 82],
-		19 => [6, 30, 58, 86],
-		20 => [6, 34, 62, 90],
-		21 => [6, 28, 50, 72,  94],
-		22 => [6, 26, 50, 74,  98],
-		23 => [6, 30, 54, 78, 102],
-		24 => [6, 28, 54, 80, 106],
-		25 => [6, 32, 58, 84, 110],
-		26 => [6, 30, 58, 86, 114],
-		27 => [6, 34, 62, 90, 118],
-		28 => [6, 26, 50, 74,  98, 122],
-		29 => [6, 30, 54, 78, 102, 126],
-		30 => [6, 26, 52, 78, 104, 130],
-		31 => [6, 30, 56, 82, 108, 134],
-		32 => [6, 34, 60, 86, 112, 138],
-		33 => [6, 30, 58, 86, 114, 142],
-		34 => [6, 34, 62, 90, 118, 146],
-		35 => [6, 30, 54, 78, 102, 126, 150],
-		36 => [6, 24, 50, 76, 102, 128, 154],
-		37 => [6, 28, 54, 80, 106, 132, 158],
-		38 => [6, 32, 58, 84, 110, 136, 162],
-		39 => [6, 26, 54, 82, 110, 138, 166],
-		40 => [6, 30, 58, 86, 114, 142, 170],
-	];
+	private ?MaskPattern $maskPattern = null;
 
 	/**
-	 * ISO/IEC 18004:2000 Annex D, Table D.1 - Version information bit stream for each version
-	 *
-	 * no version pattern for QR Codes < 7
-	 *
-	 * @var int[]
+	 * the size (side length) of the matrix, including quiet zone (if created)
 	 */
-	protected const versionPattern = [
-		7  => 0b000111110010010100,
-		8  => 0b001000010110111100,
-		9  => 0b001001101010011001,
-		10 => 0b001010010011010011,
-		11 => 0b001011101111110110,
-		12 => 0b001100011101100010,
-		13 => 0b001101100001000111,
-		14 => 0b001110011000001101,
-		15 => 0b001111100100101000,
-		16 => 0b010000101101111000,
-		17 => 0b010001010001011101,
-		18 => 0b010010101000010111,
-		19 => 0b010011010100110010,
-		20 => 0b010100100110100110,
-		21 => 0b010101011010000011,
-		22 => 0b010110100011001001,
-		23 => 0b010111011111101100,
-		24 => 0b011000111011000100,
-		25 => 0b011001000111100001,
-		26 => 0b011010111110101011,
-		27 => 0b011011000010001110,
-		28 => 0b011100110000011010,
-		29 => 0b011101001100111111,
-		30 => 0b011110110101110101,
-		31 => 0b011111001001010000,
-		32 => 0b100000100111010101,
-		33 => 0b100001011011110000,
-		34 => 0b100010100010111010,
-		35 => 0b100011011110011111,
-		36 => 0b100100101100001011,
-		37 => 0b100101010000101110,
-		38 => 0b100110101001100100,
-		39 => 0b100111010101000001,
-		40 => 0b101000110001101001,
-	];
+	private int $moduleCount;
 
 	/**
-	 * ISO/IEC 18004:2000 Section 8.9 - Format Information
-	 *
-	 * ECC level -> mask pattern
+	 * the actual matrix data array
 	 *
 	 * @var int[][]
 	 */
-	protected const formatPattern = [
-		[ // L
-			0b111011111000100,
-			0b111001011110011,
-			0b111110110101010,
-			0b111100010011101,
-			0b110011000101111,
-			0b110001100011000,
-			0b110110001000001,
-			0b110100101110110,
-		],
-		[ // M
-			0b101010000010010,
-			0b101000100100101,
-			0b101111001111100,
-			0b101101101001011,
-			0b100010111111001,
-			0b100000011001110,
-			0b100111110010111,
-			0b100101010100000,
-		],
-		[ // Q
-			0b011010101011111,
-			0b011000001101000,
-			0b011111100110001,
-			0b011101000000110,
-			0b010010010110100,
-			0b010000110000011,
-			0b010111011011010,
-			0b010101111101101,
-		],
-		[ // H
-			0b001011010001001,
-			0b001001110111110,
-			0b001110011100111,
-			0b001100111010000,
-			0b000011101100010,
-			0b000001001010101,
-			0b000110100001100,
-			0b000100000111011,
-		],
-	];
-
-	/**
-	 * the current QR Code version number
-	 */
-	protected int $version;
+	private array $matrix;
 
 	/**
 	 * the current ECC level
 	 */
-	protected int $eclevel;
-
-	/**
-	 * the used mask pattern, set via QRMatrix::mapData()
-	 */
-	protected int $maskPattern = QRCode::MASK_PATTERN_AUTO;
+	private EccLevel $eccLevel;
 
 	/**
-	 * the size (side length) of the matrix
+	 * a Version instance
 	 */
-	protected int $moduleCount;
-
-	/**
-	 * the actual matrix data array
-	 *
-	 * @var int[][]
-	 */
-	protected array $matrix;
+	private Version $version;
 
 	/**
 	 * QRMatrix constructor.
-	 *
-	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
 	 */
-	public function __construct(int $version, int $eclevel){
-
-		if(!in_array($version, range(1, 40), true)){
-			throw new QRCodeDataException('invalid QR Code version');
-		}
-
-		if(!array_key_exists($eclevel, QRCode::ECC_MODES)){
-			throw new QRCodeDataException('invalid ecc level');
-		}
-
+	public function __construct(Version $version, EccLevel $eccLevel){
 		$this->version     = $version;
-		$this->eclevel     = $eclevel;
-		$this->moduleCount = $this->version * 4 + 17;
+		$this->eccLevel    = $eccLevel;
+		$this->moduleCount = $this->version->getDimension();
 		$this->matrix      = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL));
 	}
 
 	/**
 	 * shortcut to initialize the matrix
 	 */
-	public function init(int $maskPattern, bool $test = null):QRMatrix{
+	public function init(MaskPattern $maskPattern, bool $test = null):QRMatrix{
 		return $this
 			->setFinderPattern()
 			->setSeparators()
@@ -276,7 +121,7 @@ final class QRMatrix{
 			$matrix[$y] = [];
 
 			foreach($row as $x => $val){
-				$matrix[$y][$x] = ($val >> 8) > 0;
+				$matrix[$y][$x] = ($val & $this::IS_DARK) === $this::IS_DARK;
 			}
 		}
 
@@ -286,21 +131,21 @@ final class QRMatrix{
 	/**
 	 * Returns the current version number
 	 */
-	public function version():int{
+	public function version():Version{
 		return $this->version;
 	}
 
 	/**
 	 * Returns the current ECC level
 	 */
-	public function eccLevel():int{
-		return $this->eclevel;
+	public function eccLevel():EccLevel{
+		return $this->eccLevel;
 	}
 
 	/**
 	 * Returns the current mask pattern
 	 */
-	public function maskPattern():int{
+	public function maskPattern():?MaskPattern{
 		return $this->maskPattern;
 	}
 
@@ -323,34 +168,48 @@ final class QRMatrix{
 	/**
 	 * Sets the $M_TYPE value for the module at position [$x, $y]
 	 *
-	 *   true  => $M_TYPE << 8
+	 *   true  => $M_TYPE | 0x800
 	 *   false => $M_TYPE
 	 */
 	public function set(int $x, int $y, bool $value, int $M_TYPE):QRMatrix{
-		$this->matrix[$y][$x] = $M_TYPE << ($value ? 8 : 0);
+		$this->matrix[$y][$x] = $M_TYPE | ($value ? $this::IS_DARK : 0);
 
 		return $this;
 	}
 
 	/**
-	 * Checks whether a module is true (dark) or false (light)
+	 * Flips the value of the module
+	 */
+	public function flip(int $x, int $y):QRMatrix{
+		$this->matrix[$y][$x] ^= $this::IS_DARK;
+
+		return $this;
+	}
+
+	/**
+	 * Checks whether a module is of the given $M_TYPE
 	 *
-	 *   true  => $value >> 8 === $M_TYPE
-	 *            $value >> 8 > 0
+	 *   true => $value & $M_TYPE === $M_TYPE
+	 */
+	public function checkType(int $x, int $y, int $M_TYPE):bool{
+		return ($this->matrix[$y][$x] & $M_TYPE) === $M_TYPE;
+	}
+
+	/**
+	 * Checks whether a module is true (dark) or false (light)
 	 *
-	 *   false => $value === $M_TYPE
-	 *            $value >> 8 === 0
+	 *   true  => $value & 0x800 === 0x800
+	 *   false => $value & 0x800 === 0
 	 */
 	public function check(int $x, int $y):bool{
-		return ($this->matrix[$y][$x] >> 8) > 0;
+		return $this->checkType($x, $y, $this::IS_DARK);
 	}
 
-
 	/**
 	 * Sets the "dark module", that is always on the same position 1x1px away from the bottom left finder
 	 */
 	public function setDarkModule():QRMatrix{
-		$this->set(8, 4 * $this->version + 9, true, $this::M_DARKMODULE);
+		$this->set(8, 4 * $this->version->getVersionNumber() + 9, true, $this::M_DARKMODULE);
 
 		return $this;
 	}
@@ -426,9 +285,10 @@ final class QRMatrix{
 	 * ISO/IEC 18004:2000 Section 7.3.5
 	 */
 	public function setAlignmentPattern():QRMatrix{
+		$alignmentPattern = $this->version->getAlignmentPattern();
 
-		foreach($this::alignmentPattern[$this->version] as $y){
-			foreach($this::alignmentPattern[$this->version] as $x){
+		foreach($alignmentPattern as $y){
+			foreach($alignmentPattern as $x){
 
 				// skip existing patterns
 				if($this->matrix[$y][$x] !== $this::M_NULL){
@@ -478,12 +338,12 @@ final class QRMatrix{
 	 * ISO/IEC 18004:2000 Section 8.10
 	 */
 	public function setVersionNumber(bool $test = null):QRMatrix{
-		$bits = $this::versionPattern[$this->version] ?? false;
+		$bits = $this->version->getVersionPattern();
 
-		if($bits !== false){
+		if($bits !== null){
 
 			for($i = 0; $i < 18; $i++){
-				$a = (int)floor($i / 3);
+				$a = (int)($i / 3);
 				$b = $i % 3 + $this->moduleCount - 8 - 3;
 				$v = !$test && (($bits >> $i) & 1) === 1;
 
@@ -501,8 +361,8 @@ final class QRMatrix{
 	 *
 	 * ISO/IEC 18004:2000 Section 8.9
 	 */
-	public function setFormatInfo(int $maskPattern, bool $test = null):QRMatrix{
-		$bits = $this::formatPattern[QRCode::ECC_MODES[$this->eclevel]][$maskPattern] ?? 0;
+	public function setFormatInfo(MaskPattern $maskPattern, bool $test = null):QRMatrix{
+		$bits = $this->eccLevel->getformatPattern($maskPattern);
 
 		for($i = 0; $i < 15; $i++){
 			$v = !$test && (($bits >> $i) & 1) === 1;
@@ -591,7 +451,7 @@ final class QRMatrix{
 	public function setLogoSpace(int $width, int $height, int $startX = null, int $startY = null):QRMatrix{
 
 		// for logos we operate in ECC H (30%) only
-		if($this->eclevel !== QRCode::ECC_H){
+		if($this->eccLevel->getLevel() !== EccLevel::H){
 			throw new QRCodeDataException('ECC level "H" required to add logo space');
 		}
 
@@ -605,7 +465,7 @@ final class QRMatrix{
 		}
 
 		// $this->moduleCount includes the quiet zone (if created), we need the QR size here
-		$length = $this->version * 4 + 17;
+		$length = $this->version->getDimension();
 
 		// throw if the logo space exceeds the maximum error correction capacity
 		if($width * $height > floor($length * $length * 0.2)){
@@ -641,24 +501,21 @@ final class QRMatrix{
 	}
 
 	/**
-	 * Maps the binary $data array from QRDataInterface::maskECC() on the matrix,
+	 * Maps the binary $data array from QRData::maskECC() on the matrix,
 	 * masking the data using $maskPattern (ISO/IEC 18004:2000 Section 8.8)
 	 *
-	 * @see \chillerlan\QRCode\Data\QRDataAbstract::maskECC()
+	 * @see \chillerlan\QRCode\Data\QRData::maskECC()
 	 *
-	 * @param int[] $data
-	 * @param int   $maskPattern
+	 * @param \SplFixedArray<int> $data
 	 *
 	 * @return \chillerlan\QRCode\Data\QRMatrix
 	 */
-	public function mapData(array $data, int $maskPattern):QRMatrix{
-		$this->maskPattern = $maskPattern;
-		$byteCount         = count($data);
+	public function mapData(SplFixedArray $data):QRMatrix{
+		$byteCount         = $data->count();
 		$y                 = $this->moduleCount - 1;
 		$inc               = -1;
 		$byteIndex         = 0;
 		$bitIndex          = 7;
-		$mask              = $this->getMask($this->maskPattern);
 
 		for($i = $y; $i > 0; $i -= 2){
 
@@ -677,11 +534,7 @@ final class QRMatrix{
 							$v = (($data[$byteIndex] >> $bitIndex) & 1) === 1;
 						}
 
-						if($mask($x, $y) === 0){
-							$v = !$v;
-						}
-
-						$this->matrix[$y][$x] = $this::M_DATA << ($v ? 8 : 0);
+						$this->matrix[$y][$x] = $this::M_DATA | ($v ? $this::IS_DARK : 0);
 						$bitIndex--;
 
 						if($bitIndex === -1){
@@ -708,33 +561,23 @@ final class QRMatrix{
 	}
 
 	/**
-	 * ISO/IEC 18004:2000 Section 8.8.1
-	 *
-	 * Note that some versions of the QR code standard have had errors in the section about mask patterns.
-	 * The information below has been corrected. (https://www.thonky.com/qr-code-tutorial/mask-patterns)
-	 *
-	 * @see \chillerlan\QRCode\QRMatrix::mapData()
-	 *
-	 * @internal
+	 * Applies the mask pattern
 	 *
-	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 * ISO/IEC 18004:2000 Section 8.8.1
 	 */
-	protected function getMask(int $maskPattern):Closure{
+	public function mask(MaskPattern $maskPattern):QRMatrix{
+		$this->maskPattern = $maskPattern;
+		$mask              = $this->maskPattern->getMask();
 
-		if((0b111 & $maskPattern) !== $maskPattern){
-			throw new QRCodeDataException('invalid mask pattern'); // @codeCoverageIgnore
+		foreach($this->matrix as $y => &$row){
+			foreach($row as $x => &$val){
+				if($mask($x, $y) === 0 && ($val & $this::M_DATA) === $this::M_DATA){
+					$val ^= $this::IS_DARK;
+				}
+			}
 		}
 
-		return [
-			0b000 => fn($x, $y):int => ($x + $y) % 2,
-			0b001 => fn($x, $y):int => $y % 2,
-			0b010 => fn($x, $y):int => $x % 3,
-			0b011 => fn($x, $y):int => ($x + $y) % 3,
-			0b100 => fn($x, $y):int => ((int)($y / 2) + (int)($x / 3)) % 2,
-			0b101 => fn($x, $y):int => (($x * $y) % 2) + (($x * $y) % 3),
-			0b110 => fn($x, $y):int => ((($x * $y) % 2) + (($x * $y) % 3)) % 2,
-			0b111 => fn($x, $y):int => ((($x * $y) % 3) + (($x + $y) % 2)) % 2,
-		][$maskPattern];
+		return $this;
 	}
 
 }

+ 361 - 0
src/Decoder/Binarizer.php

@@ -0,0 +1,361 @@
+<?php
+/**
+ * Class Binarizer
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use RuntimeException;
+use function array_fill, count, max;
+
+/**
+ * This class implements a local thresholding algorithm, which while slower than the
+ * GlobalHistogramBinarizer, is fairly efficient for what it does. It is designed for
+ * high frequency images of barcodes with black data on white backgrounds. For this application,
+ * it does a much better job than a global blackpoint with severe shadows and gradients.
+ * However it tends to produce artifacts on lower frequency images and is therefore not
+ * a good general purpose binarizer for uses outside ZXing.
+ *
+ * This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers,
+ * and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already
+ * inherently local, and only fails for horizontal gradients. We can revisit that problem later,
+ * but for now it was not a win to use local blocks for 1D.
+ *
+ * This Binarizer is the default for the unit tests and the recommended class for library users.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+final class Binarizer{
+
+	// This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels.
+	// So this is the smallest dimension in each axis we can accept.
+	private const BLOCK_SIZE_POWER  = 3;
+	private const BLOCK_SIZE        = 8;  // ...0100...00
+	private const BLOCK_SIZE_MASK   = 7;  // ...0011...11
+	private const MINIMUM_DIMENSION = 40;
+	private const MIN_DYNAMIC_RANGE = 24;
+
+#	private const LUMINANCE_BITS    = 5;
+	private const LUMINANCE_SHIFT   = 3;
+	private const LUMINANCE_BUCKETS = 32;
+
+	private LuminanceSource $source;
+
+	public function __construct(LuminanceSource $source){
+		$this->source = $source;
+	}
+
+	/**
+	 * @throws \RuntimeException
+	 */
+	private function estimateBlackPoint(array $buckets):int{
+		// Find the tallest peak in the histogram.
+		$numBuckets     = count($buckets);
+		$maxBucketCount = 0;
+		$firstPeak      = 0;
+		$firstPeakSize  = 0;
+
+		for($x = 0; $x < $numBuckets; $x++){
+
+			if($buckets[$x] > $firstPeakSize){
+				$firstPeak     = $x;
+				$firstPeakSize = $buckets[$x];
+			}
+
+			if($buckets[$x] > $maxBucketCount){
+				$maxBucketCount = $buckets[$x];
+			}
+		}
+
+		// Find the second-tallest peak which is somewhat far from the tallest peak.
+		$secondPeak      = 0;
+		$secondPeakScore = 0;
+
+		for($x = 0; $x < $numBuckets; $x++){
+			$distanceToBiggest = $x - $firstPeak;
+			// Encourage more distant second peaks by multiplying by square of distance.
+			$score = $buckets[$x] * $distanceToBiggest * $distanceToBiggest;
+
+			if($score > $secondPeakScore){
+				$secondPeak      = $x;
+				$secondPeakScore = $score;
+			}
+		}
+
+		// Make sure firstPeak corresponds to the black peak.
+		if($firstPeak > $secondPeak){
+			$temp       = $firstPeak;
+			$firstPeak  = $secondPeak;
+			$secondPeak = $temp;
+		}
+
+		// If there is too little contrast in the image to pick a meaningful black point, throw rather
+		// than waste time trying to decode the image, and risk false positives.
+		if($secondPeak - $firstPeak <= $numBuckets / 16){
+			throw new RuntimeException('no meaningful dark point found');
+		}
+
+		// Find a valley between them that is low and closer to the white peak.
+		$bestValley      = $secondPeak - 1;
+		$bestValleyScore = -1;
+
+		for($x = $secondPeak - 1; $x > $firstPeak; $x--){
+			$fromFirst = $x - $firstPeak;
+			$score     = $fromFirst * $fromFirst * ($secondPeak - $x) * ($maxBucketCount - $buckets[$x]);
+
+			if($score > $bestValleyScore){
+				$bestValley      = $x;
+				$bestValleyScore = $score;
+			}
+		}
+
+		return $bestValley << self::LUMINANCE_SHIFT;
+	}
+
+	/**
+	 * Calculates the final BitMatrix once for all requests. This could be called once from the
+	 * constructor instead, but there are some advantages to doing it lazily, such as making
+	 * profiling easier, and not doing heavy lifting when callers don't expect it.
+	 *
+	 * Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive
+	 * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or
+	 * may not apply sharpening. Therefore, a row from this matrix may not be identical to one
+	 * fetched using getBlackRow(), so don't mix and match between them.
+	 *
+	 * @return \chillerlan\QRCode\Decoder\BitMatrix The 2D array of bits for the image (true means black).
+	 */
+	public function getBlackMatrix():BitMatrix{
+		$width  = $this->source->getWidth();
+		$height = $this->source->getHeight();
+
+		if($width >= self::MINIMUM_DIMENSION && $height >= self::MINIMUM_DIMENSION){
+			$subWidth = $width >> self::BLOCK_SIZE_POWER;
+
+			if(($width & self::BLOCK_SIZE_MASK) !== 0){
+				$subWidth++;
+			}
+
+			$subHeight = $height >> self::BLOCK_SIZE_POWER;
+
+			if(($height & self::BLOCK_SIZE_MASK) !== 0){
+				$subHeight++;
+			}
+
+			return $this->calculateThresholdForBlock($subWidth, $subHeight, $width, $height);
+		}
+
+		// If the image is too small, fall back to the global histogram approach.
+		return $this->getHistogramBlackMatrix($width, $height);
+	}
+
+	public function getHistogramBlackMatrix(int $width, int $height):BitMatrix{
+		$matrix = new BitMatrix(max($width, $height));
+
+		// Quickly calculates the histogram by sampling four rows from the image. This proved to be
+		// more robust on the blackbox tests than sampling a diagonal as we used to do.
+		$buckets = array_fill(0, self::LUMINANCE_BUCKETS, 0);
+
+		for($y = 1; $y < 5; $y++){
+			$row             = (int)($height * $y / 5);
+			$localLuminances = $this->source->getRow($row);
+			$right           = (int)(($width * 4) / 5);
+
+			for($x = (int)($width / 5); $x < $right; $x++){
+				$pixel = $localLuminances[(int)$x] & 0xff;
+				$buckets[$pixel >> self::LUMINANCE_SHIFT]++;
+			}
+		}
+
+		$blackPoint = $this->estimateBlackPoint($buckets);
+
+		// We delay reading the entire image luminance until the black point estimation succeeds.
+		// Although we end up reading four rows twice, it is consistent with our motto of
+		// "fail quickly" which is necessary for continuous scanning.
+		$localLuminances = $this->source->getMatrix();
+
+		for($y = 0; $y < $height; $y++){
+			$offset = $y * $width;
+
+			for($x = 0; $x < $width; $x++){
+				$pixel = (int)($localLuminances[$offset + $x] & 0xff);
+
+				if($pixel < $blackPoint){
+					$matrix->set($x, $y);
+				}
+			}
+		}
+
+		return $matrix;
+	}
+
+	/**
+	 * Calculates a single black point for each block of pixels and saves it away.
+	 * See the following thread for a discussion of this algorithm:
+	 *
+	 * @see http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0
+	 */
+	private function calculateBlackPoints(array $luminances, int $subWidth, int $subHeight, int $width, int $height):array{
+		$blackPoints = array_fill(0, $subHeight, 0);
+
+		foreach($blackPoints as $key => $point){
+			$blackPoints[$key] = array_fill(0, $subWidth, 0);
+		}
+
+		for($y = 0; $y < $subHeight; $y++){
+			$yoffset    = ($y << self::BLOCK_SIZE_POWER);
+			$maxYOffset = $height - self::BLOCK_SIZE;
+
+			if($yoffset > $maxYOffset){
+				$yoffset = $maxYOffset;
+			}
+
+			for($x = 0; $x < $subWidth; $x++){
+				$xoffset    = ($x << self::BLOCK_SIZE_POWER);
+				$maxXOffset = $width - self::BLOCK_SIZE;
+
+				if($xoffset > $maxXOffset){
+					$xoffset = $maxXOffset;
+				}
+
+				$sum = 0;
+				$min = 255;
+				$max = 0;
+
+				for($yy = 0, $offset = $yoffset * $width + $xoffset; $yy < self::BLOCK_SIZE; $yy++, $offset += $width){
+
+					for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){
+						$pixel = (int)($luminances[(int)($offset + $xx)]) & 0xff;
+						$sum   += $pixel;
+						// still looking for good contrast
+						if($pixel < $min){
+							$min = $pixel;
+						}
+
+						if($pixel > $max){
+							$max = $pixel;
+						}
+					}
+
+					// short-circuit min/max tests once dynamic range is met
+					if($max - $min > self::MIN_DYNAMIC_RANGE){
+						// finish the rest of the rows quickly
+						for($yy++, $offset += $width; $yy < self::BLOCK_SIZE; $yy++, $offset += $width){
+							for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){
+								$sum += $luminances[$offset + $xx] & 0xff;
+							}
+						}
+					}
+				}
+
+				// The default estimate is the average of the values in the block.
+				$average = $sum >> (self::BLOCK_SIZE_POWER * 2);
+
+				if($max - $min <= self::MIN_DYNAMIC_RANGE){
+					// If variation within the block is low, assume this is a block with only light or only
+					// dark pixels. In that case we do not want to use the average, as it would divide this
+					// low contrast area into black and white pixels, essentially creating data out of noise.
+					//
+					// The default assumption is that the block is light/background. Since no estimate for
+					// the level of dark pixels exists locally, use half the min for the block.
+					$average = (int)($min / 2);
+
+					if($y > 0 && $x > 0){
+						// Correct the "white background" assumption for blocks that have neighbors by comparing
+						// the pixels in this block to the previously calculated black points. This is based on
+						// the fact that dark barcode symbology is always surrounded by some amount of light
+						// background for which reasonable black point estimates were made. The bp estimated at
+						// the boundaries is used for the interior.
+
+						// The (min < bp) is arbitrary but works better than other heuristics that were tried.
+						$averageNeighborBlackPoint = (int)(($blackPoints[$y - 1][$x] + (2 * $blackPoints[$y][$x - 1]) + $blackPoints[$y - 1][$x - 1]) / 4);
+
+						if($min < $averageNeighborBlackPoint){
+							$average = $averageNeighborBlackPoint;
+						}
+					}
+				}
+
+				$blackPoints[$y][$x] = (int)($average);
+			}
+		}
+
+		return $blackPoints;
+	}
+
+	/**
+	 * For each block in the image, calculate the average black point using a 5x5 grid
+	 * of the blocks around it. Also handles the corner cases (fractional blocks are computed based
+	 * on the last pixels in the row/column which are also used in the previous block).
+	 */
+	private function calculateThresholdForBlock(
+		int $subWidth,
+		int $subHeight,
+		int $width,
+		int $height
+	):BitMatrix{
+		$matrix      = new BitMatrix(max($width, $height));
+		$luminances  = $this->source->getMatrix();
+		$blackPoints = $this->calculateBlackPoints($luminances, $subWidth, $subHeight, $width, $height);
+
+		for($y = 0; $y < $subHeight; $y++){
+			$yoffset    = ($y << self::BLOCK_SIZE_POWER);
+			$maxYOffset = $height - self::BLOCK_SIZE;
+
+			if($yoffset > $maxYOffset){
+				$yoffset = $maxYOffset;
+			}
+
+			for($x = 0; $x < $subWidth; $x++){
+				$xoffset    = ($x << self::BLOCK_SIZE_POWER);
+				$maxXOffset = $width - self::BLOCK_SIZE;
+
+				if($xoffset > $maxXOffset){
+					$xoffset = $maxXOffset;
+				}
+
+				$left = $this->cap($x, 2, $subWidth - 3);
+				$top  = $this->cap($y, 2, $subHeight - 3);
+				$sum  = 0;
+
+				for($z = -2; $z <= 2; $z++){
+					$blackRow = $blackPoints[$top + $z];
+					$sum      += $blackRow[$left - 2] + $blackRow[$left - 1] + $blackRow[$left] + $blackRow[$left + 1] + $blackRow[$left + 2];
+				}
+
+				$average = (int)($sum / 25);
+
+				// Applies a single threshold to a block of pixels.
+				for($j = 0, $o = $yoffset * $width + $xoffset; $j < self::BLOCK_SIZE; $j++, $o += $width){
+					for($i = 0; $i < self::BLOCK_SIZE; $i++){
+						// Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0.
+						if(($luminances[$o + $i] & 0xff) <= $average){
+							$matrix->set($xoffset + $i, $yoffset + $j);
+						}
+					}
+				}
+			}
+		}
+
+		return $matrix;
+	}
+
+	private function cap(int $value, int $min, int $max):int{
+
+		if($value < $min){
+			return $min;
+		}
+
+		if($value > $max){
+			return $max;
+		}
+
+		return $value;
+	}
+
+}

+ 203 - 0
src/Decoder/BitMatrix.php

@@ -0,0 +1,203 @@
+<?php
+/**
+ * Class BitMatrix
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use chillerlan\QRCode\Common\{MaskPattern, Version};
+use InvalidArgumentException;
+use function array_fill, count;
+
+final class BitMatrix{
+
+	private int   $dimension;
+	private int   $rowSize;
+	private array $bits;
+
+	public function __construct(int $dimension){
+		$this->dimension = $dimension;
+		$this->rowSize   = ((int)(($this->dimension + 0x1f) / 0x20));
+		$this->bits      = array_fill(0, $this->rowSize * $this->dimension, 0);
+	}
+
+	/**
+	 * <p>Sets the given bit to true.</p>
+	 *
+	 * @param int $x ;  The horizontal component (i.e. which column)
+	 * @param int $y ;  The vertical component (i.e. which row)
+	 */
+	public function set(int $x, int $y):void{
+		$offset = (int)($y * $this->rowSize + ($x / 0x20));
+
+		$this->bits[$offset] ??= 0;
+		$this->bits[$offset] |= ($this->bits[$offset] |= 1 << ($x & 0x1f));
+	}
+
+	/**
+	 * <p>Flips the given bit. 1 << (0xf9 & 0x1f)</p>
+	 *
+	 * @param int $x ;  The horizontal component (i.e. which column)
+	 * @param int $y ;  The vertical component (i.e. which row)
+	 */
+	public function flip(int $x, int $y):void{
+		$offset = $y * $this->rowSize + (int)($x / 0x20);
+
+		$this->bits[$offset] = ($this->bits[$offset] ^ (1 << ($x & 0x1f)));
+	}
+
+	/**
+	 * <p>Sets a square region of the bit matrix to true.</p>
+	 *
+	 * @param int $left   ;  The horizontal position to begin at (inclusive)
+	 * @param int $top    ;  The vertical position to begin at (inclusive)
+	 * @param int $width  ;  The width of the region
+	 * @param int $height ;  The height of the region
+	 *
+	 * @throws \InvalidArgumentException
+	 */
+	public function setRegion(int $left, int $top, int $width, int $height):void{
+
+		if($top < 0 || $left < 0){
+			throw new InvalidArgumentException('Left and top must be nonnegative');
+		}
+
+		if($height < 1 || $width < 1){
+			throw new InvalidArgumentException('Height and width must be at least 1');
+		}
+
+		$right  = $left + $width;
+		$bottom = $top + $height;
+
+		if($bottom > $this->dimension || $right > $this->dimension){
+			throw new InvalidArgumentException('The region must fit inside the matrix');
+		}
+
+		for($y = $top; $y < $bottom; $y++){
+			$yOffset = $y * $this->rowSize;
+
+			for($x = $left; $x < $right; $x++){
+				$xOffset              = $yOffset + (int)($x / 0x20);
+				$this->bits[$xOffset] = ($this->bits[$xOffset] |= 1 << ($x & 0x1f));
+			}
+		}
+	}
+
+	/**
+	 * @return int The dimension (width/height) of the matrix
+	 */
+	public function getDimension():int{
+		return $this->dimension;
+	}
+
+	/**
+	 * <p>Gets the requested bit, where true means black.</p>
+	 *
+	 * @param int $x The horizontal component (i.e. which column)
+	 * @param int $y The vertical component (i.e. which row)
+	 *
+	 * @return bool value of given bit in matrix
+	 */
+	public function get(int $x, int $y):bool{
+		$offset = (int)($y * $this->rowSize + ($x / 0x20));
+
+		$this->bits[$offset] ??= 0;
+
+		return (BitMatrixParser::uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0;
+	}
+
+	/**
+	 * See ISO 18004:2006 Annex E
+	 */
+	public function buildFunctionPattern(Version $version):BitMatrix{
+		$dimension = $version->getDimension();
+		// @todo
+		$bitMatrix = new self($dimension);
+
+		// Top left finder pattern + separator + format
+		$bitMatrix->setRegion(0, 0, 9, 9);
+		// Top right finder pattern + separator + format
+		$bitMatrix->setRegion($dimension - 8, 0, 8, 9);
+		// Bottom left finder pattern + separator + format
+		$bitMatrix->setRegion(0, $dimension - 8, 9, 8);
+
+		// Alignment patterns
+		$apc = $version->getAlignmentPattern();
+		$max = count($apc);
+
+		for($x = 0; $x < $max; $x++){
+			$i = $apc[$x] - 2;
+
+			for($y = 0; $y < $max; $y++){
+				if(($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)){
+					// No alignment patterns near the three finder paterns
+					continue;
+				}
+
+				$bitMatrix->setRegion($apc[$y] - 2, $i, 5, 5);
+			}
+		}
+
+		// Vertical timing pattern
+		$bitMatrix->setRegion(6, 9, 1, $dimension - 17);
+		// Horizontal timing pattern
+		$bitMatrix->setRegion(9, 6, $dimension - 17, 1);
+
+		if($version->getVersionNumber() > 6){
+			// Version info, top right
+			$bitMatrix->setRegion($dimension - 11, 0, 3, 6);
+			// Version info, bottom left
+			$bitMatrix->setRegion(0, $dimension - 11, 6, 3);
+		}
+
+		return $bitMatrix;
+	}
+
+	/**
+	 * Mirror the bit matrix in order to attempt a second reading.
+	 */
+	public function mirror():void{
+
+		for($x = 0; $x < $this->dimension; $x++){
+			for($y = $x + 1; $y < $this->dimension; $y++){
+				if($this->get($x, $y) !== $this->get($y, $x)){
+					$this->flip($y, $x);
+					$this->flip($x, $y);
+				}
+			}
+		}
+
+	}
+
+	/**
+	 * <p>Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. Implementations
+	 * of this class can un-mask a raw BitMatrix. For simplicity, they will unmask the entire BitMatrix,
+	 * including areas used for finder patterns, timing patterns, etc. These areas should be unused
+	 * after the point they are unmasked anyway.</p>
+	 *
+	 * <p>Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position
+	 * and j is row position. In fact, as the text says, i is row position and j is column position.</p>
+	 *
+	 * <p>Implementations of this method reverse the data masking process applied to a QR Code and
+	 * make its bits ready to read.</p>
+	 */
+	public function unmask(int $dimension, MaskPattern $maskPattern):void{
+		$mask = $maskPattern->getMask();
+
+		for($y = 0; $y < $dimension; $y++){
+			for($x = 0; $x < $dimension; $x++){
+				if($mask($x, $y) === 0){
+					$this->flip($x, $y);
+				}
+			}
+		}
+
+	}
+
+}

+ 362 - 0
src/Decoder/BitMatrixParser.php

@@ -0,0 +1,362 @@
+<?php
+/**
+ * Class BitMatrixParser
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use RuntimeException;
+use chillerlan\QRCode\Common\{Version, FormatInformation};
+use const PHP_INT_MAX, PHP_INT_SIZE;
+
+/**
+ * @author Sean Owen
+ */
+final class BitMatrixParser{
+
+	private BitMatrix          $bitMatrix;
+	private ?Version           $parsedVersion    = null;
+	private ?FormatInformation $parsedFormatInfo = null;
+	private bool               $mirror           = false;
+
+	/**
+	 * @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix
+	 *
+	 * @throws \RuntimeException if dimension is not >= 21 and 1 mod 4
+	 */
+	public function __construct(BitMatrix $bitMatrix){
+		$dimension = $bitMatrix->getDimension();
+
+		if($dimension < 21 || ($dimension % 4) !== 1){
+			throw new RuntimeException('dimension is not >= 21, dimension mod 4 not 1');
+		}
+
+		$this->bitMatrix = $bitMatrix;
+	}
+
+	/**
+	 * Prepare the parser for a mirrored operation.
+	 * This flag has effect only on the {@link #readFormatInformation()} and the
+	 * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the
+	 * {@link #mirror()} method should be called.
+	 *
+	 * @param bool $mirror Whether to read version and format information mirrored.
+	 */
+	public function setMirror(bool $mirror):void{
+		$this->parsedVersion    = null;
+		$this->parsedFormatInfo = null;
+		$this->mirror           = $mirror;
+	}
+
+	/**
+	 * Mirror the bit matrix in order to attempt a second reading.
+	 */
+	public function mirror():void{
+		$this->bitMatrix->mirror();
+	}
+
+	private function copyBit(int $i, int $j, int $versionBits):int{
+
+		$bit = $this->mirror
+			? $this->bitMatrix->get($j, $i)
+			: $this->bitMatrix->get($i, $j);
+
+		return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1;
+	}
+
+	/**
+	 * <p>Reads the bits in the {@link BitMatrix} representing the finder pattern in the
+	 * correct order in order to reconstruct the codewords bytes contained within the
+	 * QR Code.</p>
+	 *
+	 * @return array bytes encoded within the QR Code
+	 * @throws \RuntimeException if the exact number of bytes expected is not read
+	 */
+	public function readCodewords():array{
+		$formatInfo = $this->readFormatInformation();
+		$version    = $this->readVersion();
+
+		// Get the data mask for the format used in this QR Code. This will exclude
+		// some bits from reading as we wind through the bit matrix.
+		$dimension = $this->bitMatrix->getDimension();
+		$this->bitMatrix->unmask($dimension, $formatInfo->getDataMask());
+		$functionPattern = $this->bitMatrix->buildFunctionPattern($version);
+
+		$readingUp    = true;
+		$result       = [];
+		$resultOffset = 0;
+		$currentByte  = 0;
+		$bitsRead     = 0;
+		// Read columns in pairs, from right to left
+		for($j = $dimension - 1; $j > 0; $j -= 2){
+
+			if($j === 6){
+				// Skip whole column with vertical alignment pattern;
+				// saves time and makes the other code proceed more cleanly
+				$j--;
+			}
+			// Read alternatingly from bottom to top then top to bottom
+			for($count = 0; $count < $dimension; $count++){
+				$i = $readingUp ? $dimension - 1 - $count : $count;
+
+				for($col = 0; $col < 2; $col++){
+					// Ignore bits covered by the function pattern
+					if(!$functionPattern->get($j - $col, $i)){
+						// Read a bit
+						$bitsRead++;
+						$currentByte <<= 1;
+
+						if($this->bitMatrix->get($j - $col, $i)){
+							$currentByte |= 1;
+						}
+						// If we've made a whole byte, save it off
+						if($bitsRead === 8){
+							$result[$resultOffset++] = $currentByte; //(byte)
+							$bitsRead                = 0;
+							$currentByte             = 0;
+						}
+					}
+				}
+			}
+
+			$readingUp = !$readingUp; // switch directions
+		}
+
+		if($resultOffset !== $version->getTotalCodewords()){
+			throw new RuntimeException('offset differs from total codewords for version');
+		}
+
+		return $result;
+	}
+
+	/**
+	 * <p>Reads format information from one of its two locations within the QR Code.</p>
+	 *
+	 * @return \chillerlan\QRCode\Common\FormatInformation encapsulating the QR Code's format info
+	 * @throws \RuntimeException                           if both format information locations cannot be parsed as
+	 *                                                     the valid encoding of format information
+	 */
+	public function readFormatInformation():FormatInformation{
+
+		if($this->parsedFormatInfo !== null){
+			return $this->parsedFormatInfo;
+		}
+
+		// Read top-left format info bits
+		$formatInfoBits1 = 0;
+
+		for($i = 0; $i < 6; $i++){
+			$formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1);
+		}
+
+		// .. and skip a bit in the timing pattern ...
+		$formatInfoBits1 = $this->copyBit(7, 8, $formatInfoBits1);
+		$formatInfoBits1 = $this->copyBit(8, 8, $formatInfoBits1);
+		$formatInfoBits1 = $this->copyBit(8, 7, $formatInfoBits1);
+		// .. and skip a bit in the timing pattern ...
+		for($j = 5; $j >= 0; $j--){
+			$formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1);
+		}
+
+		// Read the top-right/bottom-left pattern too
+		$dimension       = $this->bitMatrix->getDimension();
+		$formatInfoBits2 = 0;
+		$jMin            = $dimension - 7;
+
+		for($j = $dimension - 1; $j >= $jMin; $j--){
+			$formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2);
+		}
+
+		for($i = $dimension - 8; $i < $dimension; $i++){
+			$formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2);
+		}
+
+		$this->parsedFormatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2);
+
+		if($this->parsedFormatInfo !== null){
+			return $this->parsedFormatInfo;
+		}
+
+		// Should return null, but, some QR codes apparently do not mask this info.
+		// Try again by actually masking the pattern first.
+		$this->parsedFormatInfo = $this->doDecodeFormatInformation(
+			$formatInfoBits1 ^ FormatInformation::MASK_QR,
+			$formatInfoBits2 ^ FormatInformation::MASK_QR
+		);
+
+		if($this->parsedFormatInfo !== null){
+			return $this->parsedFormatInfo;
+		}
+
+		throw new RuntimeException('failed to read format info');
+	}
+
+	/**
+	 * @param int $maskedFormatInfo1 format info indicator, with mask still applied
+	 * @param int $maskedFormatInfo2 second copy of same info; both are checked at the same time
+	 *                               to establish best match
+	 *
+	 * @return \chillerlan\QRCode\Common\FormatInformation|null information about the format it specifies, or null
+	 *                                                          if doesn't seem to match any known pattern
+	 */
+	private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?FormatInformation{
+		// Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing
+		$bestDifference = PHP_INT_MAX;
+		$bestFormatInfo = 0;
+
+		foreach(FormatInformation::DECODE_LOOKUP as $decodeInfo){
+			[$maskedBits, $dataBits] = $decodeInfo;
+
+			if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){
+				// Found an exact match
+				return new FormatInformation($maskedBits);
+			}
+
+			$bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $dataBits);
+
+			if($bitsDifference < $bestDifference){
+				$bestFormatInfo = $maskedBits;
+				$bestDifference = $bitsDifference;
+			}
+
+			if($maskedFormatInfo1 !== $maskedFormatInfo2){
+				// also try the other option
+				$bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $dataBits);
+
+				if($bitsDifference < $bestDifference){
+					$bestFormatInfo = $maskedBits;
+					$bestDifference = $bitsDifference;
+				}
+			}
+		}
+		// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match
+		if($bestDifference <= 3){
+			return new FormatInformation($bestFormatInfo);
+		}
+
+		return null;
+	}
+
+	/**
+	 * <p>Reads version information from one of its two locations within the QR Code.</p>
+	 *
+	 * @return \chillerlan\QRCode\Common\Version encapsulating the QR Code's version
+	 * @throws \RuntimeException                 if both version information locations cannot be parsed as
+	 *                                           the valid encoding of version information
+	 */
+	public function readVersion():Version{
+
+		if($this->parsedVersion !== null){
+			return $this->parsedVersion;
+		}
+
+		$dimension          = $this->bitMatrix->getDimension();
+		$provisionalVersion = ($dimension - 17) / 4;
+
+		if($provisionalVersion <= 6){
+			return new Version($provisionalVersion);
+		}
+
+		// Read top-right version info: 3 wide by 6 tall
+		$versionBits = 0;
+		$ijMin       = $dimension - 11;
+
+		for($j = 5; $j >= 0; $j--){
+			for($i = $dimension - 9; $i >= $ijMin; $i--){
+				$versionBits = $this->copyBit($i, $j, $versionBits);
+			}
+		}
+
+		$this->parsedVersion = $this->decodeVersionInformation($versionBits);
+
+		if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){
+			return $this->parsedVersion;
+		}
+
+		// Hmm, failed. Try bottom left: 6 wide by 3 tall
+		$versionBits = 0;
+
+		for($i = 5; $i >= 0; $i--){
+			for($j = $dimension - 9; $j >= $ijMin; $j--){
+				$versionBits = $this->copyBit($i, $j, $versionBits);
+			}
+		}
+
+		$this->parsedVersion = $this->decodeVersionInformation($versionBits);
+
+		if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){
+			return $this->parsedVersion;
+		}
+
+		throw new RuntimeException('failed to read version');
+	}
+
+	/**
+	 * @param int $versionBits
+	 *
+	 * @return \chillerlan\QRCode\Common\Version|null
+	 */
+	private function decodeVersionInformation(int $versionBits):?Version{
+		$bestDifference = PHP_INT_MAX;
+		$bestVersion    = 0;
+
+		for($i = 7; $i <= 40; $i++){
+			$targetVersion        = new Version($i);
+			$targetVersionPattern = $targetVersion->getVersionPattern();
+
+			// Do the version info bits match exactly? done.
+			if($targetVersionPattern === $versionBits){
+				return $targetVersion;
+			}
+
+			// Otherwise see if this is the closest to a real version info bit string
+			// we have seen so far
+			/** @phan-suppress-next-line PhanTypeMismatchArgumentNullable ($targetVersionPattern is never null here) */
+			$bitsDifference = self::numBitsDiffering($versionBits, $targetVersionPattern);
+
+			if($bitsDifference < $bestDifference){
+				$bestVersion    = $i;
+				$bestDifference = $bitsDifference;
+			}
+		}
+		// We can tolerate up to 3 bits of error since no two version info codewords will
+		// differ in less than 8 bits.
+		if($bestDifference <= 3){
+			return new Version($bestVersion);
+		}
+
+		// If we didn't find a close enough match, fail
+		return null;
+	}
+
+	public static function uRShift(int $a, int $b):int{
+
+		if($b === 0){
+			return $a;
+		}
+
+		return ($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1));
+	}
+
+	private static function numBitsDiffering(int $a, int $b):int{
+		// a now has a 1 bit exactly where its bit differs with b's
+		$a ^= $b;
+		// Offset i holds the number of 1 bits in the binary representation of i
+		$BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
+		// Count bits set quickly with a series of lookups:
+		$count = 0;
+
+		for($i = 0; $i < 32; $i += 4){
+			$count += $BITS_SET_IN_HALF_BYTE[self::uRShift($a, $i) & 0x0F];
+		}
+
+		return $count;
+	}
+
+}

+ 337 - 0
src/Decoder/Decoder.php

@@ -0,0 +1,337 @@
+<?php
+/**
+ * Class Decoder
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use Exception, InvalidArgumentException, RuntimeException;
+use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, ReedSolomonDecoder, Version};
+use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number};
+use chillerlan\QRCode\Detector\Detector;
+use function count, array_fill, mb_convert_encoding, mb_detect_encoding;
+
+/**
+ * <p>The main class which implements QR Code decoding -- as opposed to locating and extracting
+ * the QR Code from an image.</p>
+ *
+ * @author Sean Owen
+ */
+final class Decoder{
+
+#	private const GB2312_SUBSET = 1;
+
+	/**
+	 * <p>Decodes a QR Code represented as a {@link \chillerlan\QRCode\Decoder\BitMatrix}.
+	 * A 1 or "true" is taken to mean a black module.</p>
+	 *
+	 * @param \chillerlan\QRCode\Decoder\LuminanceSource $source
+	 *
+	 * @return \chillerlan\QRCode\Decoder\DecoderResult text and bytes encoded within the QR Code
+	 * @throws \Exception if the QR Code cannot be decoded
+	 */
+	public function decode(LuminanceSource $source):DecoderResult{
+		$matrix    = (new Binarizer($source))->getBlackMatrix();
+		$bitMatrix = (new Detector($matrix))->detect();
+
+		$fe = null;
+
+		try{
+			// Construct a parser and read version, error-correction level
+			// clone the BitMatrix to avoid errors in case we run into mirroring
+			return $this->decodeParser(new BitMatrixParser(clone $bitMatrix));
+		}
+		catch(Exception $e){
+			$fe = $e;
+		}
+
+		try{
+			$parser = new BitMatrixParser(clone $bitMatrix);
+
+			// Will be attempting a mirrored reading of the version and format info.
+			$parser->setMirror(true);
+
+			// Preemptively read the version.
+#			$parser->readVersion();
+
+			// Preemptively read the format information.
+#			$parser->readFormatInformation();
+
+			/*
+			 * Since we're here, this means we have successfully detected some kind
+			 * of version and format information when mirrored. This is a good sign,
+			 * that the QR code may be mirrored, and we should try once more with a
+			 * mirrored content.
+			 */
+			// Prepare for a mirrored reading.
+			$parser->mirror();
+
+			return $this->decodeParser($parser);
+		}
+		catch(Exception $e){
+			// Throw the exception from the original reading
+			if($fe instanceof Exception){
+				throw $fe;
+			}
+
+			throw $e;
+		}
+
+	}
+
+	/**
+	 * @param \chillerlan\QRCode\Decoder\BitMatrixParser $parser
+	 *
+	 * @return \chillerlan\QRCode\Decoder\DecoderResult
+	 */
+	private function decodeParser(BitMatrixParser $parser):DecoderResult{
+		$version  = $parser->readVersion();
+		$eccLevel = $parser->readFormatInformation()->getErrorCorrectionLevel();
+
+		// Read raw codewords
+		$rawCodewords  = $parser->readCodewords();
+		// Separate into data blocks
+		$dataBlocks = $this->getDataBlocks($rawCodewords, $version, $eccLevel);
+
+		$resultBytes  = [];
+		$resultOffset = 0;
+
+		// Error-correct and copy data blocks together into a stream of bytes
+		foreach($dataBlocks as $dataBlock){
+			[$numDataCodewords, $codewordBytes] = $dataBlock;
+
+			$corrected = $this->correctErrors($codewordBytes, $numDataCodewords);
+
+			for($i = 0; $i < $numDataCodewords; $i++){
+				$resultBytes[$resultOffset++] = $corrected[$i];
+			}
+		}
+
+		// Decode the contents of that stream of bytes
+		return $this->decodeBitStream($resultBytes, $version, $eccLevel);
+	}
+
+	/**
+	 * <p>When QR Codes use multiple data blocks, they are actually interleaved.
+	 * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This
+	 * method will separate the data into original blocks.</p>
+	 *
+	 * @param array                              $rawCodewords bytes as read directly from the QR Code
+	 * @param \chillerlan\QRCode\Common\Version  $version      version of the QR Code
+	 * @param \chillerlan\QRCode\Common\EccLevel $eccLevel     error-correction level of the QR Code
+	 *
+	 * @return array DataBlocks containing original bytes, "de-interleaved" from representation in the QR Code
+	 * @throws \InvalidArgumentException
+	 */
+	private function getDataBlocks(array $rawCodewords, Version $version, EccLevel $eccLevel):array{
+
+		if(count($rawCodewords) !== $version->getTotalCodewords()){
+			throw new InvalidArgumentException('$rawCodewords differ from total codewords for version');
+		}
+
+		// Figure out the number and size of data blocks used by this version and
+		// error correction level
+		[$numEccCodewords, $eccBlocks] = $version->getRSBlocks($eccLevel);
+
+		// Now establish DataBlocks of the appropriate size and number of data codewords
+		$result          = [];//new DataBlock[$totalBlocks];
+		$numResultBlocks = 0;
+
+		foreach($eccBlocks as $blockData){
+			[$numEccBlocks, $eccPerBlock] = $blockData;
+
+			for($i = 0; $i < $numEccBlocks; $i++, $numResultBlocks++){
+				$result[$numResultBlocks] = [$eccPerBlock, array_fill(0, $numEccCodewords + $eccPerBlock, 0)];
+			}
+		}
+
+		// All blocks have the same amount of data, except that the last n
+		// (where n may be 0) have 1 more byte. Figure out where these start.
+		/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
+		$shorterBlocksTotalCodewords = count($result[0][1]);
+		$longerBlocksStartAt         = count($result) - 1;
+
+		while($longerBlocksStartAt >= 0){
+			$numCodewords = count($result[$longerBlocksStartAt][1]);
+
+			if($numCodewords == $shorterBlocksTotalCodewords){
+				break;
+			}
+
+			$longerBlocksStartAt--;
+		}
+
+		$longerBlocksStartAt++;
+
+		$shorterBlocksNumDataCodewords = $shorterBlocksTotalCodewords - $numEccCodewords;
+		// The last elements of result may be 1 element longer;
+		// first fill out as many elements as all of them have
+		$rawCodewordsOffset = 0;
+
+		for($i = 0; $i < $shorterBlocksNumDataCodewords; $i++){
+			for($j = 0; $j < $numResultBlocks; $j++){
+				$result[$j][1][$i] = $rawCodewords[$rawCodewordsOffset++];
+			}
+		}
+
+		// Fill out the last data block in the longer ones
+		for($j = $longerBlocksStartAt; $j < $numResultBlocks; $j++){
+			$result[$j][1][$shorterBlocksNumDataCodewords] = $rawCodewords[$rawCodewordsOffset++];
+		}
+
+		// Now add in error correction blocks
+		/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
+		$max = count($result[0][1]);
+
+		for($i = $shorterBlocksNumDataCodewords; $i < $max; $i++){
+			for($j = 0; $j < $numResultBlocks; $j++){
+				$iOffset                 = $j < $longerBlocksStartAt ? $i : $i + 1;
+				$result[$j][1][$iOffset] = $rawCodewords[$rawCodewordsOffset++];
+			}
+		}
+
+		return $result;
+	}
+
+	/**
+	 * <p>Given data and error-correction codewords received, possibly corrupted by errors, attempts to
+	 * correct the errors in-place using Reed-Solomon error correction.</p>
+	 */
+	private function correctErrors(array $codewordBytes, int $numDataCodewords):array{
+		// First read into an array of ints
+		$codewordsInts = [];
+
+		foreach($codewordBytes as $i => $codewordByte){
+			$codewordsInts[$i] = $codewordByte & 0xFF;
+		}
+
+		$decoded = (new ReedSolomonDecoder)->decode($codewordsInts, (count($codewordBytes) - $numDataCodewords));
+
+		// Copy back into array of bytes -- only need to worry about the bytes that were data
+		// We don't care about errors in the error-correction codewords
+		for($i = 0; $i < $numDataCodewords; $i++){
+			$codewordBytes[$i] = $decoded[$i];
+		}
+
+		return $codewordBytes;
+	}
+
+	/**
+	 * @throws \RuntimeException
+	 */
+	private function decodeBitStream(array $bytes, Version $version, EccLevel $ecLevel):DecoderResult{
+		$bits           = new BitBuffer($bytes);
+		$symbolSequence = -1;
+		$parityData     = -1;
+		$versionNumber  = $version->getVersionNumber();
+
+		$result      = '';
+		$eciCharset  = null;
+#		$fc1InEffect = false;
+
+		// While still another segment to read...
+		while($bits->available() >= 4){
+			$datamode = $bits->read(4); // mode is encoded by 4 bits
+
+			// OK, assume we're done. Really, a TERMINATOR mode should have been recorded here
+			if($datamode === Mode::DATA_TERMINATOR){
+				break;
+			}
+
+			if($datamode === Mode::DATA_ECI){
+				// Count doesn't apply to ECI
+				$eciCharset = ECI::parseValue($bits);
+			}
+			/** @noinspection PhpStatementHasEmptyBodyInspection */
+			elseif($datamode === Mode::DATA_FNC1_FIRST || $datamode === Mode::DATA_FNC1_SECOND){
+				// We do little with FNC1 except alter the parsed result a bit according to the spec
+#				$fc1InEffect = true;
+			}
+			elseif($datamode === Mode::DATA_STRCTURED_APPEND){
+				if($bits->available() < 16){
+					throw new RuntimeException('structured append: not enough bits left');
+				}
+				// sequence number and parity is added later to the result metadata
+				// Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue
+				$symbolSequence = $bits->read(8);
+				$parityData     = $bits->read(8);
+			}
+			else{
+				// First handle Hanzi mode which does not start with character count
+/*				if($datamode === Mode::DATA_HANZI){
+					//chinese mode contains a sub set indicator right after mode indicator
+					$subset = $bits->read(4);
+					$length = $bits->read(Mode::getLengthBitsForVersion($datamode, $versionNumber));
+					if($subset === self::GB2312_SUBSET){
+						$result .= $this->decodeHanziSegment($bits, $length);
+					}
+				}*/
+#				else{
+					// "Normal" QR code modes:
+					if($datamode === Mode::DATA_NUMBER){
+						$result .= Number::decodeSegment($bits, $versionNumber);
+					}
+					elseif($datamode === Mode::DATA_ALPHANUM){
+						$str = AlphaNum::decodeSegment($bits, $versionNumber);
+
+						// See section 6.4.8.1, 6.4.8.2
+/*						if($fc1InEffect){
+							$start = \strlen($str);
+							// We need to massage the result a bit if in an FNC1 mode:
+							for($i = $start; $i < $start; $i++){
+								if($str[$i] === '%'){
+									if($i < $start - 1 && $str[$i + 1] === '%'){
+										// %% is rendered as %
+										$str = \substr_replace($str, '', $i + 1, 1);//deleteCharAt(i + 1);
+									}
+#									else{
+										// In alpha mode, % should be converted to FNC1 separator 0x1D @todo
+#										$str = setCharAt($i, \chr(0x1D)); // ???
+#									}
+								}
+							}
+						}
+*/
+						$result .= $str;
+					}
+					elseif($datamode === Mode::DATA_BYTE){
+						$str = Byte::decodeSegment($bits, $versionNumber);
+
+						if($eciCharset !== null){
+							$encoding = $eciCharset->getName();
+
+							if($encoding === null){
+								// The spec isn't clear on this mode; see
+								// section 6.4.5: t does not say which encoding to assuming
+								// upon decoding. I have seen ISO-8859-1 used as well as
+								// Shift_JIS -- without anything like an ECI designator to
+								// give a hint.
+								$encoding = mb_detect_encoding($str, ['ISO-8859-1', 'SJIS', 'UTF-8']);
+							}
+
+							$eciCharset = null;
+							$str = mb_convert_encoding($str, $encoding);
+						}
+
+						$result .= $str;
+					}
+					elseif($datamode === Mode::DATA_KANJI){
+						$result .= Kanji::decodeSegment($bits, $versionNumber);
+					}
+					else{
+						throw new RuntimeException('invalid data mode');
+					}
+#				}
+			}
+		}
+
+		return new DecoderResult($bytes, $result, $version, $ecLevel, $symbolSequence, $parityData);
+	}
+
+}

+ 86 - 0
src/Decoder/DecoderResult.php

@@ -0,0 +1,86 @@
+<?php
+/**
+ * Class DecoderResult
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use chillerlan\QRCode\Common\{EccLevel, Version};
+
+/**
+ * <p>Encapsulates the result of decoding a matrix of bits. This typically
+ * applies to 2D barcode formats. For now it contains the raw bytes obtained,
+ * as well as a String interpretation of those bytes, if applicable.</p>
+ *
+ * @author Sean Owen
+ */
+final class DecoderResult{
+
+	private array    $rawBytes;
+	private string   $text;
+	private Version  $version;
+	private EccLevel $eccLevel;
+	private int      $structuredAppendParity;
+	private int      $structuredAppendSequenceNumber;
+
+	public function __construct(
+		array $rawBytes,
+		string $text,
+		Version $version,
+		EccLevel $eccLevel,
+		int $saSequence = -1,
+		int $saParity = -1
+	){
+		$this->rawBytes                       = $rawBytes;
+		$this->text                           = $text;
+		$this->version                        = $version;
+		$this->eccLevel                       = $eccLevel;
+		$this->structuredAppendParity         = $saParity;
+		$this->structuredAppendSequenceNumber = $saSequence;
+	}
+
+	/**
+	 * @return int[] raw bytes encoded by the barcode, if applicable, otherwise {@code null}
+	 */
+	public function getRawBytes():array{
+		return $this->rawBytes;
+	}
+
+	/**
+	 * @return string raw text encoded by the barcode
+	 */
+	public function getText():string{
+		return $this->text;
+	}
+
+	public function __toString():string{
+		return $this->text;
+	}
+
+	public function getVersion():Version{
+		return $this->version;
+	}
+
+	public function getEccLevel():EccLevel{
+		return $this->eccLevel;
+	}
+
+	public function hasStructuredAppend():bool{
+		return $this->structuredAppendParity >= 0 && $this->structuredAppendSequenceNumber >= 0;
+	}
+
+	public function getStructuredAppendParity():int{
+		return $this->structuredAppendParity;
+	}
+
+	public function getStructuredAppendSequenceNumber():int{
+		return $this->structuredAppendSequenceNumber;
+	}
+
+}

+ 66 - 0
src/Decoder/GDLuminanceSource.php

@@ -0,0 +1,66 @@
+<?php
+/**
+ * Class GDLuminanceSource
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ *
+ * @noinspection PhpComposerExtensionStubsInspection
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use InvalidArgumentException;
+use function get_resource_type, imagecolorat, imagecolorsforindex, imagesx, imagesy, is_resource;
+use const PHP_MAJOR_VERSION;
+
+/**
+ * This class is used to help decode images from files which arrive as GD Resource
+ * It does not support rotation.
+ */
+final class GDLuminanceSource extends LuminanceSource{
+
+	/**
+	 * @var resource|\GdImage
+	 */
+	private $gdImage;
+
+	/**
+	 * GDLuminanceSource constructor.
+	 *
+	 * @param resource|\GdImage $gdImage
+	 *
+	 * @throws \InvalidArgumentException
+	 */
+	public function __construct($gdImage){
+
+		/** @noinspection PhpFullyQualifiedNameUsageInspection */
+		if(
+			(PHP_MAJOR_VERSION >= 8 && !$gdImage instanceof \GdImage)
+			|| (PHP_MAJOR_VERSION < 8 && (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd'))
+		){
+			throw new InvalidArgumentException('Invalid GD image source.');
+		}
+
+		parent::__construct(imagesx($gdImage), imagesy($gdImage));
+
+		$this->gdImage = $gdImage;
+
+		$this->setLuminancePixels();
+	}
+
+	private function setLuminancePixels():void{
+		for($j = 0; $j < $this->height; $j++){
+			for($i = 0; $i < $this->width; $i++){
+				$argb  = imagecolorat($this->gdImage, $i, $j);
+				$pixel = imagecolorsforindex($this->gdImage, $argb);
+
+				$this->setLuminancePixel($pixel['red'], $pixel['green'], $pixel['blue']);
+			}
+		}
+	}
+
+}

+ 53 - 0
src/Decoder/IMagickLuminanceSource.php

@@ -0,0 +1,53 @@
+<?php
+/**
+ * Class IMagickLuminanceSource
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ *
+ * @noinspection PhpComposerExtensionStubsInspection
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use Imagick;
+use function count;
+
+/**
+ * This class is used to help decode images from files which arrive as Imagick Resource
+ * It does not support rotation.
+ */
+final class IMagickLuminanceSource extends LuminanceSource{
+
+	private Imagick $imagick;
+
+	/**
+	 * IMagickLuminanceSource constructor.
+	 *
+	 * @param \Imagick $imagick
+	 *
+	 * @throws \InvalidArgumentException
+	 */
+	public function __construct(Imagick $imagick){
+		parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight());
+
+		$this->imagick = $imagick;
+
+		$this->setLuminancePixels();
+	}
+
+	private function setLuminancePixels():void{
+		$this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY);
+		$pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR);
+
+		$countPixels = count($pixels);
+
+		for($i = 0; $i < $countPixels; $i += 3){
+			$this->setLuminancePixel($pixels[$i] & 0xff, $pixels[$i + 1] & 0xff, $pixels[$i + 2] & 0xff);
+		}
+	}
+
+}

+ 107 - 0
src/Decoder/LuminanceSource.php

@@ -0,0 +1,107 @@
+<?php
+/**
+ * Class LuminanceSource
+ *
+ * @created      24.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use InvalidArgumentException;
+use function array_slice, array_splice;
+
+/**
+ * The purpose of this class hierarchy is to abstract different bitmap implementations across
+ * platforms into a standard interface for requesting greyscale luminance values. The interface
+ * only provides immutable methods; therefore crop and rotation create copies. This is to ensure
+ * that one Reader does not modify the original luminance source and leave it in an unknown state
+ * for other Readers in the chain.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+abstract class LuminanceSource{
+
+	protected array $luminances;
+	protected int   $width;
+	protected int   $height;
+
+	public function __construct(int $width, int $height){
+		$this->width  = $width;
+		$this->height = $height;
+		// In order to measure pure decoding speed, we convert the entire image to a greyscale array
+		// up front, which is the same as the Y channel of the YUVLuminanceSource in the real app.
+		$this->luminances = [];
+		// @todo: grayscale?
+		//$this->luminances = $this->grayScaleToBitmap($this->grayscale());
+	}
+
+	/**
+	 * Fetches luminance data for the underlying bitmap. Values should be fetched using:
+	 * {@code int luminance = array[y * width + x] & 0xff}
+	 *
+	 * @return array A row-major 2D array of luminance values. Do not use result.length as it may be
+	 *         larger than width * height bytes on some platforms. Do not modify the contents
+	 *         of the result.
+	 */
+	public function getMatrix():array{
+		return $this->luminances;
+	}
+
+	/**
+	 * @return int The width of the bitmap.
+	 */
+	public function getWidth():int{
+		return $this->width;
+	}
+
+	/**
+	 * @return int The height of the bitmap.
+	 */
+	public function getHeight():int{
+		return $this->height;
+	}
+
+	/**
+	 * Fetches one row of luminance data from the underlying platform's bitmap. Values range from
+	 * 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have
+	 * to bitwise and with 0xff for each value. It is preferable for implementations of this method
+	 * to only fetch this row rather than the whole image, since no 2D Readers may be installed and
+	 * getMatrix() may never be called.
+	 *
+	 * @param int $y  The row to fetch, which must be in [0,getHeight())
+	 *
+	 * @return array An array containing the luminance data.
+	 */
+	public function getRow(int $y):array{
+
+		if($y < 0 || $y >= $this->getHeight()){
+			throw new InvalidArgumentException('Requested row is outside the image: '.$y);
+		}
+
+		$arr = [];
+
+		array_splice($arr, 0, $this->width, array_slice($this->luminances, $y * $this->width, $this->width));
+
+		return $arr;
+	}
+
+	/**
+	 * @param int $r
+	 * @param int $g
+	 * @param int $b
+	 *
+	 * @return void
+	 */
+	protected function setLuminancePixel(int $r, int $g, int $b):void{
+		$this->luminances[] = $r === $g && $g === $b
+			// Image is already greyscale, so pick any channel.
+			? $r // (($r + 128) % 256) - 128;
+			// Calculate luminance cheaply, favoring green.
+			: ($r + 2 * $g + $b) / 4; // (((($r + 2 * $g + $b) / 4) + 128) % 256) - 128;
+	}
+
+}

+ 34 - 0
src/Detector/AlignmentPattern.php

@@ -0,0 +1,34 @@
+<?php
+/**
+ * Class AlignmentPattern
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+/**
+ * <p>Encapsulates an alignment pattern, which are the smaller square patterns found in
+ * all but the simplest QR Codes.</p>
+ *
+ * @author Sean Owen
+ */
+final class AlignmentPattern extends ResultPoint{
+
+	/**
+	 * Combines this object's current estimate of a finder pattern position and module size
+	 * with a new estimate. It returns a new {@code FinderPattern} containing an average of the two.
+	 */
+	public function combineEstimate(float $i, float $j, float $newModuleSize):AlignmentPattern{
+		return new self(
+			($this->x + $j) / 2.0,
+			($this->y + $i) / 2.0,
+			($this->estimatedModuleSize + $newModuleSize) / 2.0
+		);
+	}
+
+}

+ 284 - 0
src/Detector/AlignmentPatternFinder.php

@@ -0,0 +1,284 @@
+<?php
+/**
+ * Class AlignmentPatternFinder
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use chillerlan\QRCode\Decoder\BitMatrix;
+use function abs, count;
+
+/**
+ * <p>This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder
+ * patterns but are smaller and appear at regular intervals throughout the image.</p>
+ *
+ * <p>At the moment this only looks for the bottom-right alignment pattern.</p>
+ *
+ * <p>This is mostly a simplified copy of {@link FinderPatternFinder}. It is copied,
+ * pasted and stripped down here for maximum performance but does unfortunately duplicate
+ * some code.</p>
+ *
+ * <p>This class is thread-safe but not reentrant. Each thread must allocate its own object.</p>
+ *
+ * @author Sean Owen
+ */
+final class AlignmentPatternFinder{
+
+	private BitMatrix $bitMatrix;
+	private float     $moduleSize;
+	/** @var \chillerlan\QRCode\Detector\AlignmentPattern[] */
+	private array $possibleCenters;
+
+	/**
+	 * <p>Creates a finder that will look in a portion of the whole image.</p>
+	 *
+	 * @param \chillerlan\QRCode\Decoder\BitMatrix $image      image to search
+	 * @param float                                $moduleSize estimated module size so far
+	 */
+	public function __construct(BitMatrix $image, float $moduleSize){
+		$this->bitMatrix            = $image;
+		$this->moduleSize           = $moduleSize;
+		$this->possibleCenters      = [];
+	}
+
+	/**
+	 * <p>This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since
+	 * it's pretty performance-critical and so is written to be fast foremost.</p>
+	 *
+	 * @param int $startX left column from which to start searching
+	 * @param int $startY top row from which to start searching
+	 * @param int $width  width of region to search
+	 * @param int $height height of region to search
+	 *
+	 * @return \chillerlan\QRCode\Detector\AlignmentPattern|null
+	 */
+	public function find(int $startX, int $startY, int $width, int $height):?AlignmentPattern{
+		$maxJ       = $startX + $width;
+		$middleI    = $startY + ($height / 2);
+		$stateCount = [];
+
+		// We are looking for black/white/black modules in 1:1:1 ratio;
+		// this tracks the number of black/white/black modules seen so far
+		for($iGen = 0; $iGen < $height; $iGen++){
+			// Search from middle outwards
+			$i             = (int)($middleI + (($iGen & 0x01) === 0 ? ($iGen + 1) / 2 : -(($iGen + 1) / 2)));
+			$stateCount[0] = 0;
+			$stateCount[1] = 0;
+			$stateCount[2] = 0;
+			$j             = $startX;
+			// Burn off leading white pixels before anything else; if we start in the middle of
+			// a white run, it doesn't make sense to count its length, since we don't know if the
+			// white run continued to the left of the start point
+			while($j < $maxJ && !$this->bitMatrix->get($j, $i)){
+				$j++;
+			}
+
+			$currentState = 0;
+
+			while($j < $maxJ){
+
+				if($this->bitMatrix->get($j, $i)){
+					// Black pixel
+					if($currentState === 1){ // Counting black pixels
+						$stateCount[$currentState]++;
+					}
+					// Counting white pixels
+					else{
+						// A winner?
+						if($currentState === 2){
+							// Yes
+							if($this->foundPatternCross($stateCount)){
+								$confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
+
+								if($confirmed !== null){
+									return $confirmed;
+								}
+							}
+
+							$stateCount[0] = $stateCount[2];
+							$stateCount[1] = 1;
+							$stateCount[2] = 0;
+							$currentState  = 1;
+						}
+						else{
+							$stateCount[++$currentState]++;
+						}
+					}
+				}
+				// White pixel
+				else{
+					// Counting black pixels
+					if($currentState === 1){
+						$currentState++;
+					}
+
+					$stateCount[$currentState]++;
+				}
+
+				$j++;
+			}
+
+			if($this->foundPatternCross($stateCount)){
+				$confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ);
+
+				if($confirmed !== null){
+					return $confirmed;
+				}
+			}
+
+		}
+
+		// Hmm, nothing we saw was observed and confirmed twice. If we had
+		// any guess at all, return it.
+		if(count($this->possibleCenters)){
+			return $this->possibleCenters[0];
+		}
+
+		return null;
+	}
+
+	/**
+	 * @param int[] $stateCount count of black/white/black pixels just read
+	 *
+	 * @return bool true if the proportions of the counts is close enough to the 1/1/1 ratios
+	 *         used by alignment patterns to be considered a match
+	 */
+	private function foundPatternCross(array $stateCount):bool{
+		$moduleSize  = $this->moduleSize;
+		$maxVariance = $moduleSize / 2.0;
+
+		for($i = 0; $i < 3; $i++){
+			if(abs($moduleSize - $stateCount[$i]) >= $maxVariance){
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * <p>This is called when a horizontal scan finds a possible alignment pattern. It will
+	 * cross check with a vertical scan, and if successful, will see if this pattern had been
+	 * found on a previous horizontal scan. If so, we consider it confirmed and conclude we have
+	 * found the alignment pattern.</p>
+	 *
+	 * @param int[] $stateCount reading state module counts from horizontal scan
+	 * @param int   $i          row where alignment pattern may be found
+	 * @param int   $j          end of possible alignment pattern in row
+	 *
+	 * @return \chillerlan\QRCode\Detector\AlignmentPattern|null if we have found the same pattern twice, or null if not
+	 */
+	private function handlePossibleCenter(array $stateCount, int $i, int $j):?AlignmentPattern{
+		$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2];
+		$centerJ         = $this->centerFromEnd($stateCount, $j);
+		$centerI         = $this->crossCheckVertical($i, (int)$centerJ, 2 * $stateCount[1], $stateCountTotal);
+
+		if($centerI !== null){
+			$estimatedModuleSize = (float)($stateCount[0] + $stateCount[1] + $stateCount[2]) / 3.0;
+
+			foreach($this->possibleCenters as $center){
+				// Look for about the same center and module size:
+				if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){
+					return $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
+				}
+			}
+
+			// Hadn't found this before; save it
+			$point                   = new AlignmentPattern($centerJ, $centerI, $estimatedModuleSize);
+			$this->possibleCenters[] = $point;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Given a count of black/white/black pixels just seen and an end position,
+	 * figures the location of the center of this black/white/black run.
+	 *
+	 * @param int[] $stateCount
+	 * @param int   $end
+	 *
+	 * @return float
+	 */
+	private function centerFromEnd(array $stateCount, int $end):float{
+		return (float)(($end - $stateCount[2]) - $stateCount[1] / 2.0);
+	}
+
+	/**
+	 * <p>After a horizontal scan finds a potential alignment pattern, this method
+	 * "cross-checks" by scanning down vertically through the center of the possible
+	 * alignment pattern to see if the same proportion is detected.</p>
+	 *
+	 * @param int $startI   row where an alignment pattern was detected
+	 * @param int $centerJ  center of the section that appears to cross an alignment pattern
+	 * @param int $maxCount maximum reasonable number of modules that should be
+	 *                      observed in any reading state, based on the results of the horizontal scan
+	 * @param int $originalStateCountTotal
+	 *
+	 * @return float|null vertical center of alignment pattern, or null if not found
+	 */
+	private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{
+		$maxI          = $this->bitMatrix->getDimension();
+		$stateCount    = [];
+		$stateCount[0] = 0;
+		$stateCount[1] = 0;
+		$stateCount[2] = 0;
+
+		// Start counting up from center
+		$i = $startI;
+		while($i >= 0 && $this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){
+			$stateCount[1]++;
+			$i--;
+		}
+		// If already too many modules in this state or ran off the edge:
+		if($i < 0 || $stateCount[1] > $maxCount){
+			return null;
+		}
+
+		while($i >= 0 && !$this->bitMatrix->get($centerJ, $i) && $stateCount[0] <= $maxCount){
+			$stateCount[0]++;
+			$i--;
+		}
+
+		if($stateCount[0] > $maxCount){
+			return null;
+		}
+
+		// Now also count down from center
+		$i = $startI + 1;
+		while($i < $maxI && $this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){
+			$stateCount[1]++;
+			$i++;
+		}
+
+		if($i == $maxI || $stateCount[1] > $maxCount){
+			return null;
+		}
+
+		while($i < $maxI && !$this->bitMatrix->get($centerJ, $i) && $stateCount[2] <= $maxCount){
+			$stateCount[2]++;
+			$i++;
+		}
+
+		if($stateCount[2] > $maxCount){
+			return null;
+		}
+
+		if(5 * abs(($stateCount[0] + $stateCount[1] + $stateCount[2]) - $originalStateCountTotal) >= 2 * $originalStateCountTotal){
+			return null;
+		}
+
+		if(!$this->foundPatternCross($stateCount)){
+			return null;
+		}
+
+		return $this->centerFromEnd($stateCount, $i);
+	}
+
+}

+ 357 - 0
src/Detector/Detector.php

@@ -0,0 +1,357 @@
+<?php
+/**
+ * Class Detector
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use RuntimeException;
+use chillerlan\QRCode\Common\Version;
+use chillerlan\QRCode\Decoder\BitMatrix;
+use function abs, is_nan, max, min, round;
+use const NAN;
+
+/**
+ * <p>Encapsulates logic that can detect a QR Code in an image, even if the QR Code
+ * is rotated or skewed, or partially obscured.</p>
+ *
+ * @author Sean Owen
+ */
+final class Detector{
+
+	private BitMatrix $bitMatrix;
+
+	/**
+	 * Detector constructor.
+	 */
+	public function __construct(BitMatrix $image){
+		$this->bitMatrix = $image;
+	}
+
+	/**
+	 * <p>Detects a QR Code in an image.</p>
+	 */
+	public function detect():BitMatrix{
+		[$bottomLeft, $topLeft, $topRight] = (new FinderPatternFinder($this->bitMatrix))->find();
+
+		$moduleSize         = (float)$this->calculateModuleSize($topLeft, $topRight, $bottomLeft);
+		$dimension          = $this->computeDimension($topLeft, $topRight, $bottomLeft, $moduleSize);
+		$provisionalVersion = new Version((int)(($dimension - 17) / 4));
+		$alignmentPattern   = null;
+
+		// Anything above version 1 has an alignment pattern
+		if(!empty($provisionalVersion->getAlignmentPattern())){
+			// Guess where a "bottom right" finder pattern would have been
+			$bottomRightX = $topRight->getX() - $topLeft->getX() + $bottomLeft->getX();
+			$bottomRightY = $topRight->getY() - $topLeft->getY() + $bottomLeft->getY();
+
+			// Estimate that alignment pattern is closer by 3 modules
+			// from "bottom right" to known top left location
+			$correctionToTopLeft = 1.0 - 3.0 / (float)($provisionalVersion->getDimension() - 7);
+			$estAlignmentX       = (int)($topLeft->getX() + $correctionToTopLeft * ($bottomRightX - $topLeft->getX()));
+			$estAlignmentY       = (int)($topLeft->getY() + $correctionToTopLeft * ($bottomRightY - $topLeft->getY()));
+
+			// Kind of arbitrary -- expand search radius before giving up
+			for($i = 4; $i <= 16; $i <<= 1){//??????????
+				$alignmentPattern = $this->findAlignmentInRegion($moduleSize, $estAlignmentX, $estAlignmentY, (float)$i);
+
+				if($alignmentPattern !== null){
+					break;
+				}
+			}
+			// If we didn't find alignment pattern... well try anyway without it
+		}
+
+		$transform = $this->createTransform($topLeft, $topRight, $bottomLeft, $dimension, $alignmentPattern);
+
+		return (new GridSampler)->sampleGrid($this->bitMatrix, $dimension, $transform);
+	}
+
+	/**
+	 * <p>Computes an average estimated module size based on estimated derived from the positions
+	 * of the three finder patterns.</p>
+	 *
+	 * @throws \RuntimeException
+	 */
+	private function calculateModuleSize(FinderPattern $topLeft, FinderPattern $topRight, FinderPattern $bottomLeft):float{
+		// Take the average
+		$moduleSize = (
+			$this->calculateModuleSizeOneWay($topLeft, $topRight) +
+			$this->calculateModuleSizeOneWay($topLeft, $bottomLeft)
+		) / 2.0;
+
+		if($moduleSize < 1.0){
+			throw new RuntimeException('module size < 1.0');
+		}
+
+		return $moduleSize;
+	}
+
+	/**
+	 * <p>Estimates module size based on two finder patterns -- it uses
+	 * {@link #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int)} to figure the
+	 * width of each, measuring along the axis between their centers.</p>
+	 */
+	private function calculateModuleSizeOneWay(FinderPattern $pattern, FinderPattern $otherPattern):float{
+
+		$moduleSizeEst1 = $this->sizeOfBlackWhiteBlackRunBothWays(
+			$pattern->getX(),
+			$pattern->getY(),
+			$otherPattern->getX(),
+			$otherPattern->getY()
+		);
+
+		$moduleSizeEst2 = $this->sizeOfBlackWhiteBlackRunBothWays(
+			$otherPattern->getX(),
+			$otherPattern->getY(),
+			$pattern->getX(),
+			$pattern->getY()
+		);
+
+		if(is_nan($moduleSizeEst1)){
+			return $moduleSizeEst2 / 7.0;
+		}
+
+		if(is_nan($moduleSizeEst2)){
+			return $moduleSizeEst1 / 7.0;
+		}
+		// Average them, and divide by 7 since we've counted the width of 3 black modules,
+		// and 1 white and 1 black module on either side. Ergo, divide sum by 14.
+		return ($moduleSizeEst1 + $moduleSizeEst2) / 14.0;
+	}
+
+	/**
+	 * See {@link #sizeOfBlackWhiteBlackRun(int, int, int, int)}; computes the total width of
+	 * a finder pattern by looking for a black-white-black run from the center in the direction
+	 * of another po$(another finder pattern center), and in the opposite direction too.</p>
+	 */
+	private function sizeOfBlackWhiteBlackRunBothWays(float $fromX, float $fromY, float $toX, float $toY):float{
+		$result    = $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, (int)$toX, (int)$toY);
+		$dimension = $this->bitMatrix->getDimension();
+		// Now count other way -- don't run off image though of course
+		$scale     = 1.0;
+		$otherToX  = $fromX - ($toX - $fromX);
+
+		if($otherToX < 0){
+			$scale    = $fromX / ($fromX - $otherToX);
+			$otherToX = 0;
+		}
+		elseif($otherToX >= $dimension){
+			$scale    = ($dimension - 1 - $fromX) / ($otherToX - $fromX);
+			$otherToX = $dimension - 1;
+		}
+
+		$otherToY = (int)($fromY - ($toY - $fromY) * $scale);
+		$scale    = 1.0;
+
+		if($otherToY < 0){
+			$scale    = $fromY / ($fromY - $otherToY);
+			$otherToY = 0;
+		}
+		elseif($otherToY >= $dimension){
+			$scale    = ($dimension - 1 - $fromY) / ($otherToY - $fromY);
+			$otherToY = $dimension - 1;
+		}
+
+		$otherToX = (int)($fromX + ($otherToX - $fromX) * $scale);
+		$result   += $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, (int)$otherToX, (int)$otherToY);
+
+		// Middle pixel is double-counted this way; subtract 1
+		return $result - 1.0;
+	}
+
+	/**
+	 * <p>This method traces a line from a po$in the image, in the direction towards another point.
+	 * It begins in a black region, and keeps going until it finds white, then black, then white again.
+	 * It reports the distance from the start to this point.</p>
+	 *
+	 * <p>This is used when figuring out how wide a finder pattern is, when the finder pattern
+	 * may be skewed or rotated.</p>
+	 */
+	private function sizeOfBlackWhiteBlackRun(int $fromX, int $fromY, int $toX, int $toY):float{
+		// Mild variant of Bresenham's algorithm;
+		// see http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
+		$steep = abs($toY - $fromY) > abs($toX - $fromX);
+
+		if($steep){
+			$temp  = $fromX;
+			$fromX = $fromY;
+			$fromY = $temp;
+			$temp  = $toX;
+			$toX   = $toY;
+			$toY   = $temp;
+		}
+
+		$dx    = abs($toX - $fromX);
+		$dy    = abs($toY - $fromY);
+		$error = -$dx / 2;
+		$xstep = $fromX < $toX ? 1 : -1;
+		$ystep = $fromY < $toY ? 1 : -1;
+
+		// In black pixels, looking for white, first or second time.
+		$state  = 0;
+		// Loop up until x == toX, but not beyond
+		$xLimit = $toX + $xstep;
+
+		for($x = $fromX, $y = $fromY; $x !== $xLimit; $x += $xstep){
+			$realX = $steep ? $y : $x;
+			$realY = $steep ? $x : $y;
+
+			// Does current pixel mean we have moved white to black or vice versa?
+			// Scanning black in state 0,2 and white in state 1, so if we find the wrong
+			// color, advance to next state or end if we are in state 2 already
+			if(($state === 1) === $this->bitMatrix->get($realX, $realY)){
+
+				if($state === 2){
+					return FinderPattern::distance($x, $y, $fromX, $fromY);
+				}
+
+				$state++;
+			}
+
+			$error += $dy;
+
+			if($error > 0){
+
+				if($y === $toY){
+					break;
+				}
+
+				$y     += $ystep;
+				$error -= $dx;
+			}
+		}
+
+		// Found black-white-black; give the benefit of the doubt that the next pixel outside the image
+		// is "white" so this last po$at (toX+xStep,toY) is the right ending. This is really a
+		// small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this.
+		if($state === 2){
+			return FinderPattern::distance($toX + $xstep, $toY, $fromX, $fromY);
+		}
+
+		// else we didn't find even black-white-black; no estimate is really possible
+		return NAN;
+	}
+
+	/**
+	 * <p>Computes the dimension (number of modules on a size) of the QR Code based on the position
+	 * of the finder patterns and estimated module size.</p>
+	 *
+	 * @throws \RuntimeException
+	 */
+	private function computeDimension(
+		FinderPattern $topLeft,
+		FinderPattern $topRight,
+		FinderPattern $bottomLeft,
+		float $moduleSize
+	):int{
+		$tltrCentersDimension = (int)round($topLeft->getDistance($topRight) / $moduleSize);
+		$tlblCentersDimension = (int)round($topLeft->getDistance($bottomLeft) / $moduleSize);
+		$dimension            = (int)((($tltrCentersDimension + $tlblCentersDimension) / 2) + 7);
+
+		switch($dimension % 4){
+			case 0:
+				$dimension++;
+				break;
+			// 1? do nothing
+			case 2:
+				$dimension--;
+				break;
+			case 3:
+				throw new RuntimeException('estimated dimension: '.$dimension);
+		}
+
+		if($dimension % 4 !== 1){
+			throw new RuntimeException('dimension mod 4 is not 1');
+		}
+
+		return $dimension;
+	}
+
+	/**
+	 * <p>Attempts to locate an alignment pattern in a limited region of the image, which is
+	 * guessed to contain it.</p>
+	 *
+	 * @param float $overallEstModuleSize estimated module size so far
+	 * @param int   $estAlignmentX        x coordinate of center of area probably containing alignment pattern
+	 * @param int   $estAlignmentY        y coordinate of above
+	 * @param float $allowanceFactor      number of pixels in all directions to search from the center
+	 *
+	 * @return \chillerlan\QRCode\Detector\AlignmentPattern|null if found, or null otherwise
+	 */
+	private function findAlignmentInRegion(
+		float $overallEstModuleSize,
+		int $estAlignmentX,
+		int $estAlignmentY,
+		float $allowanceFactor
+	):?AlignmentPattern{
+		// Look for an alignment pattern (3 modules in size) around where it should be
+		$dimension           = $this->bitMatrix->getDimension();
+		$allowance           = (int)($allowanceFactor * $overallEstModuleSize);
+		$alignmentAreaLeftX  = max(0, $estAlignmentX - $allowance);
+		$alignmentAreaRightX = min($dimension - 1, $estAlignmentX + $allowance);
+
+		if($alignmentAreaRightX - $alignmentAreaLeftX < $overallEstModuleSize * 3){
+			return null;
+		}
+
+		$alignmentAreaTopY    = max(0, $estAlignmentY - $allowance);
+		$alignmentAreaBottomY = min($dimension - 1, $estAlignmentY + $allowance);
+
+		if($alignmentAreaBottomY - $alignmentAreaTopY < $overallEstModuleSize * 3){
+			return null;
+		}
+
+		return (new AlignmentPatternFinder($this->bitMatrix, $overallEstModuleSize))->find(
+			$alignmentAreaLeftX,
+			$alignmentAreaTopY,
+			$alignmentAreaRightX - $alignmentAreaLeftX,
+			$alignmentAreaBottomY - $alignmentAreaTopY,
+		);
+	}
+
+	/**
+	 *
+	 */
+	private function createTransform(
+		FinderPattern $topLeft,
+		FinderPattern $topRight,
+		FinderPattern $bottomLeft,
+		int $dimension,
+		AlignmentPattern $alignmentPattern = null
+	):PerspectiveTransform{
+		$dimMinusThree = (float)$dimension - 3.5;
+
+		if($alignmentPattern instanceof AlignmentPattern){
+			$bottomRightX       = $alignmentPattern->getX();
+			$bottomRightY       = $alignmentPattern->getY();
+			$sourceBottomRightX = $dimMinusThree - 3.0;
+			$sourceBottomRightY = $sourceBottomRightX;
+		}
+		else{
+			// Don't have an alignment pattern, just make up the bottom-right point
+			$bottomRightX       = ($topRight->getX() - $topLeft->getX()) + $bottomLeft->getX();
+			$bottomRightY       = ($topRight->getY() - $topLeft->getY()) + $bottomLeft->getY();
+			$sourceBottomRightX = $dimMinusThree;
+			$sourceBottomRightY = $dimMinusThree;
+		}
+
+		return (new PerspectiveTransform)->quadrilateralToQuadrilateral(
+			3.5, 3.5,
+			$dimMinusThree, 3.5,
+			$sourceBottomRightX, $sourceBottomRightY,
+			3.5, $dimMinusThree,
+			$topLeft->getX(), $topLeft->getY(),
+			$topRight->getX(), $topRight->getY(),
+			$bottomRightX, $bottomRightY,
+			$bottomLeft->getX(), $bottomLeft->getY()
+		);
+	}
+
+}

+ 80 - 0
src/Detector/FinderPattern.php

@@ -0,0 +1,80 @@
+<?php
+/**
+ * Class FinderPattern
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use function sqrt;
+
+/**
+ * <p>Encapsulates a finder pattern, which are the three square patterns found in
+ * the corners of QR Codes. It also encapsulates a count of similar finder patterns,
+ * as a convenience to the finder's bookkeeping.</p>
+ *
+ * @author Sean Owen
+ */
+final class FinderPattern extends ResultPoint{
+
+	private int $count;
+
+	public function __construct(float $posX, float $posY, float $estimatedModuleSize, int $count = null){
+		parent::__construct($posX, $posY, $estimatedModuleSize);
+
+		$this->count = $count ?? 1;
+	}
+
+	public function getCount():int{
+		return $this->count;
+	}
+
+	/**
+	 * @param \chillerlan\QRCode\Detector\FinderPattern $b second pattern
+	 *
+	 * @return float distance between two points
+	 */
+	public function getDistance(FinderPattern $b):float{
+		return self::distance($this->x, $this->y, $b->x, $b->y);
+	}
+
+	/**
+	 * Get square of distance between a and b.
+	 */
+	public function getSquaredDistance(FinderPattern $b):float{
+		return self::squaredDistance($this->x, $this->y, $b->x, $b->y);
+	}
+
+	/**
+	 * Combines this object's current estimate of a finder pattern position and module size
+	 * with a new estimate. It returns a new {@code FinderPattern} containing a weighted average
+	 * based on count.
+	 */
+	public function combineEstimate(float $i, float $j, float $newModuleSize):FinderPattern{
+		$combinedCount = $this->count + 1;
+
+		return new self(
+			($this->count * $this->x + $j) / $combinedCount,
+			($this->count * $this->y + $i) / $combinedCount,
+			($this->count * $this->estimatedModuleSize + $newModuleSize) / $combinedCount,
+			$combinedCount
+		);
+	}
+
+	private static function squaredDistance(float $aX, float $aY, float $bX, float $bY):float{
+		$xDiff = $aX - $bX;
+		$yDiff = $aY - $bY;
+
+		return $xDiff * $xDiff + $yDiff * $yDiff;
+	}
+
+	public static function distance(float $aX, float $aY, float $bX, float $bY):float{
+		return sqrt(self::squaredDistance($aX, $aY, $bX, $bY));
+	}
+
+}

+ 772 - 0
src/Detector/FinderPatternFinder.php

@@ -0,0 +1,772 @@
+<?php
+/**
+ * Class FinderPatternFinder
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ *
+ * @phan-file-suppress PhanTypePossiblyInvalidDimOffset
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use RuntimeException;
+use chillerlan\QRCode\Decoder\BitMatrix;
+use function abs, count, usort;
+use const PHP_FLOAT_MAX;
+
+/**
+ * <p>This class attempts to find finder patterns in a QR Code. Finder patterns are the square
+ * markers at three corners of a QR Code.</p>
+ *
+ * <p>This class is thread-safe but not reentrant. Each thread must allocate its own object.
+ *
+ * @author Sean Owen
+ */
+final class FinderPatternFinder{
+
+	private const MIN_SKIP      = 2;
+	private const MAX_MODULES   = 177; // 1 pixel/module times 3 modules/center
+	private const CENTER_QUORUM = 2; // support up to version 10 for mobile clients
+	private BitMatrix $bitMatrix;
+	/** @var \chillerlan\QRCode\Detector\FinderPattern[] */
+	private array $possibleCenters;
+	private bool  $hasSkipped = false;
+
+	/**
+	 * <p>Creates a finder that will search the image for three finder patterns.</p>
+	 *
+	 * @param BitMatrix $bitMatrix image to search
+	 */
+	public function __construct(BitMatrix $bitMatrix){
+		$this->bitMatrix       = $bitMatrix;
+		$this->possibleCenters = [];
+	}
+
+	/**
+	 * @return \chillerlan\QRCode\Detector\FinderPattern[]
+	 */
+	public function find():array{
+		$dimension = $this->bitMatrix->getDimension();
+
+		// We are looking for black/white/black/white/black modules in
+		// 1:1:3:1:1 ratio; this tracks the number of such modules seen so far
+		// Let's assume that the maximum version QR Code we support takes up 1/4 the height of the
+		// image, and then account for the center being 3 modules in size. This gives the smallest
+		// number of pixels the center could be, so skip this often.
+		$iSkip = (int)((3 * $dimension) / (4 * self::MAX_MODULES));
+
+		if($iSkip < self::MIN_SKIP){
+			$iSkip = self::MIN_SKIP;
+		}
+
+		$done = false;
+
+		for($i = $iSkip - 1; $i < $dimension && !$done; $i += $iSkip){
+			// Get a row of black/white values
+			$stateCount   = $this->getCrossCheckStateCount();
+			$currentState = 0;
+
+			for($j = 0; $j < $dimension; $j++){
+
+				// Black pixel
+				if($this->bitMatrix->get($j, $i)){
+					// Counting white pixels
+					if(($currentState & 1) === 1){
+						$currentState++;
+					}
+
+					$stateCount[$currentState]++;
+				}
+				// White pixel
+				else{
+					// Counting black pixels
+					if(($currentState & 1) === 0){
+						// A winner?
+						if($currentState === 4){
+							// Yes
+							if($this->foundPatternCross($stateCount)){
+								$confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
+
+								if($confirmed){
+									// Start examining every other line. Checking each line turned out to be too
+									// expensive and didn't improve performance.
+									$iSkip = 3;
+
+									if($this->hasSkipped){
+										$done = $this->haveMultiplyConfirmedCenters();
+									}
+									else{
+										$rowSkip = $this->findRowSkip();
+
+										if($rowSkip > $stateCount[2]){
+											// Skip rows between row of lower confirmed center
+											// and top of presumed third confirmed center
+											// but back up a bit to get a full chance of detecting
+											// it, entire width of center of finder pattern
+
+											// Skip by rowSkip, but back off by $stateCount[2] (size of last center
+											// of pattern we saw) to be conservative, and also back off by iSkip which
+											// is about to be re-added
+											$i += $rowSkip - $stateCount[2] - $iSkip;
+											$j = $dimension - 1;
+										}
+									}
+								}
+								else{
+									$stateCount   = $this->doShiftCounts2($stateCount);
+									$currentState = 3;
+
+									continue;
+								}
+								// Clear state to start looking again
+								$currentState = 0;
+								$stateCount   = $this->getCrossCheckStateCount();
+							}
+							// No, shift counts back by two
+							else{
+								$stateCount   = $this->doShiftCounts2($stateCount);
+								$currentState = 3;
+							}
+						}
+						else{
+							$stateCount[++$currentState]++;
+						}
+					}
+					// Counting white pixels
+					else{
+						$stateCount[$currentState]++;
+					}
+				}
+			}
+
+			if($this->foundPatternCross($stateCount)){
+				$confirmed = $this->handlePossibleCenter($stateCount, $i, $dimension);
+
+				if($confirmed){
+					$iSkip = $stateCount[0];
+
+					if($this->hasSkipped){
+						// Found a third one
+						$done = $this->haveMultiplyConfirmedCenters();
+					}
+				}
+			}
+		}
+
+		return $this->orderBestPatterns($this->selectBestPatterns());
+	}
+
+	/**
+	 * @return int[]
+	 */
+	private function getCrossCheckStateCount():array{
+		return [0, 0, 0, 0, 0];
+	}
+
+	/**
+	 * @param int[] $stateCount
+	 *
+	 * @return int[]
+	 */
+	private function doShiftCounts2(array $stateCount):array{
+		$stateCount[0] = $stateCount[2];
+		$stateCount[1] = $stateCount[3];
+		$stateCount[2] = $stateCount[4];
+		$stateCount[3] = 1;
+		$stateCount[4] = 0;
+
+		return $stateCount;
+	}
+
+	/**
+	 * Given a count of black/white/black/white/black pixels just seen and an end position,
+	 * figures the location of the center of this run.
+	 *
+	 * @param int[] $stateCount
+	 *
+	 * @return float
+	 */
+	private function centerFromEnd(array $stateCount, int $end):float{
+		return (float)(($end - $stateCount[4] - $stateCount[3]) - $stateCount[2] / 2.0);
+	}
+
+	/**
+	 * @param int[] $stateCount
+	 *
+	 * @return bool
+	 */
+	private function foundPatternCross(array $stateCount):bool{
+		// Allow less than 50% variance from 1-1-3-1-1 proportions
+		return $this->foundPatternVariance($stateCount, 2.0);
+	}
+
+	/**
+	 * @param int[] $stateCount
+	 *
+	 * @return bool
+	 */
+	private function foundPatternDiagonal(array $stateCount):bool{
+		// Allow less than 75% variance from 1-1-3-1-1 proportions
+		return $this->foundPatternVariance($stateCount, 1.333);
+	}
+
+	/**
+	 * @param int[] $stateCount count of black/white/black/white/black pixels just read
+	 *
+	 * @return bool true if the proportions of the counts is close enough to the 1/1/3/1/1 ratios
+	 *              used by finder patterns to be considered a match
+	 */
+	private function foundPatternVariance(array $stateCount, float $variance):bool{
+		$totalModuleSize = 0;
+
+		for($i = 0; $i < 5; $i++){
+			$count = $stateCount[$i];
+
+			if($count === 0){
+				return false;
+			}
+
+			$totalModuleSize += $count;
+		}
+
+		if($totalModuleSize < 7){
+			return false;
+		}
+
+		$moduleSize  = $totalModuleSize / 7.0;
+		$maxVariance = $moduleSize / $variance;
+
+		return
+			abs($moduleSize - $stateCount[0]) < $maxVariance
+			&& abs($moduleSize - $stateCount[1]) < $maxVariance
+			&& abs(3.0 * $moduleSize - $stateCount[2]) < 3 * $maxVariance
+			&& abs($moduleSize - $stateCount[3]) < $maxVariance
+			&& abs($moduleSize - $stateCount[4]) < $maxVariance;
+	}
+
+	/**
+	 * After a vertical and horizontal scan finds a potential finder pattern, this method
+	 * "cross-cross-cross-checks" by scanning down diagonally through the center of the possible
+	 * finder pattern to see if the same proportion is detected.
+	 *
+	 * @param $centerI ;  row where a finder pattern was detected
+	 * @param $centerJ ; center of the section that appears to cross a finder pattern
+	 *
+	 * @return bool true if proportions are withing expected limits
+	 */
+	private function crossCheckDiagonal(int $centerI, int $centerJ):bool{
+		$stateCount = $this->getCrossCheckStateCount();
+
+		// Start counting up, left from center finding black center mass
+		$i = 0;
+
+		while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){
+			$stateCount[2]++;
+			$i++;
+		}
+
+		if($stateCount[2] === 0){
+			return false;
+		}
+
+		// Continue up, left finding white space
+		while($centerI >= $i && $centerJ >= $i && !$this->bitMatrix->get($centerJ - $i, $centerI - $i)){
+			$stateCount[1]++;
+			$i++;
+		}
+
+		if($stateCount[1] === 0){
+			return false;
+		}
+
+		// Continue up, left finding black border
+		while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){
+			$stateCount[0]++;
+			$i++;
+		}
+
+		if($stateCount[0] === 0){
+			return false;
+		}
+
+		$dimension = $this->bitMatrix->getDimension();
+
+		// Now also count down, right from center
+		$i = 1;
+		while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){
+			$stateCount[2]++;
+			$i++;
+		}
+
+		while($centerI + $i < $dimension && $centerJ + $i < $dimension && !$this->bitMatrix->get($centerJ + $i, $centerI + $i)){
+			$stateCount[3]++;
+			$i++;
+		}
+
+		if($stateCount[3] === 0){
+			return false;
+		}
+
+		while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){
+			$stateCount[4]++;
+			$i++;
+		}
+
+		if($stateCount[4] === 0){
+			return false;
+		}
+
+		return $this->foundPatternDiagonal($stateCount);
+	}
+
+	/**
+	 * <p>After a horizontal scan finds a potential finder pattern, this method
+	 * "cross-checks" by scanning down vertically through the center of the possible
+	 * finder pattern to see if the same proportion is detected.</p>
+	 *
+	 * @param int $startI   ;  row where a finder pattern was detected
+	 * @param int $centerJ  ; center of the section that appears to cross a finder pattern
+	 * @param int $maxCount ; maximum reasonable number of modules that should be
+	 *                      observed in any reading state, based on the results of the horizontal scan
+	 * @param int $originalStateCountTotal
+	 *
+	 * @return float|null vertical center of finder pattern, or null if not found
+	 */
+	private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{
+		$maxI       = $this->bitMatrix->getDimension();
+		$stateCount = $this->getCrossCheckStateCount();
+
+		// Start counting up from center
+		$i = $startI;
+		while($i >= 0 && $this->bitMatrix->get($centerJ, $i)){
+			$stateCount[2]++;
+			$i--;
+		}
+
+		if($i < 0){
+			return null;
+		}
+
+		while($i >= 0 && !$this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){
+			$stateCount[1]++;
+			$i--;
+		}
+
+		// If already too many modules in this state or ran off the edge:
+		if($i < 0 || $stateCount[1] > $maxCount){
+			return null;
+		}
+
+		while($i >= 0 && $this->bitMatrix->get($centerJ, $i) && $stateCount[0] <= $maxCount){
+			$stateCount[0]++;
+			$i--;
+		}
+
+		if($stateCount[0] > $maxCount){
+			return null;
+		}
+
+		// Now also count down from center
+		$i = $startI + 1;
+		while($i < $maxI && $this->bitMatrix->get($centerJ, $i)){
+			$stateCount[2]++;
+			$i++;
+		}
+
+		if($i === $maxI){
+			return null;
+		}
+
+		while($i < $maxI && !$this->bitMatrix->get($centerJ, $i) && $stateCount[3] < $maxCount){
+			$stateCount[3]++;
+			$i++;
+		}
+
+		if($i === $maxI || $stateCount[3] >= $maxCount){
+			return null;
+		}
+
+		while($i < $maxI && $this->bitMatrix->get($centerJ, $i) && $stateCount[4] < $maxCount){
+			$stateCount[4]++;
+			$i++;
+		}
+
+		if($stateCount[4] >= $maxCount){
+			return null;
+		}
+
+		// If we found a finder-pattern-like section, but its size is more than 40% different than
+		// the original, assume it's a false positive
+		$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
+
+		if(5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal){
+			return null;
+		}
+
+		if(!$this->foundPatternCross($stateCount)){
+			return null;
+		}
+
+		return $this->centerFromEnd($stateCount, $i);
+	}
+
+	/**
+	 * <p>Like {@link #crossCheckVertical(int, int, int, int)}, and in fact is basically identical,
+	 * except it reads horizontally instead of vertically. This is used to cross-cross
+	 * check a vertical cross check and locate the real center of the alignment pattern.</p>
+	 */
+	private function crossCheckHorizontal(int $startJ, int $centerI, int $maxCount, int $originalStateCountTotal):?float{
+		$maxJ       = $this->bitMatrix->getDimension();
+		$stateCount = $this->getCrossCheckStateCount();
+
+		$j = $startJ;
+		while($j >= 0 && $this->bitMatrix->get($j, $centerI)){
+			$stateCount[2]++;
+			$j--;
+		}
+
+		if($j < 0){
+			return null;
+		}
+
+		while($j >= 0 && !$this->bitMatrix->get($j, $centerI) && $stateCount[1] <= $maxCount){
+			$stateCount[1]++;
+			$j--;
+		}
+
+		if($j < 0 || $stateCount[1] > $maxCount){
+			return null;
+		}
+
+		while($j >= 0 && $this->bitMatrix->get($j, $centerI) && $stateCount[0] <= $maxCount){
+			$stateCount[0]++;
+			$j--;
+		}
+
+		if($stateCount[0] > $maxCount){
+			return null;
+		}
+
+		$j = $startJ + 1;
+		while($j < $maxJ && $this->bitMatrix->get($j, $centerI)){
+			$stateCount[2]++;
+			$j++;
+		}
+
+		if($j === $maxJ){
+			return null;
+		}
+
+		while($j < $maxJ && !$this->bitMatrix->get($j, $centerI) && $stateCount[3] < $maxCount){
+			$stateCount[3]++;
+			$j++;
+		}
+
+		if($j === $maxJ || $stateCount[3] >= $maxCount){
+			return null;
+		}
+
+		while($j < $maxJ && $this->bitMatrix->get($j, $centerI) && $stateCount[4] < $maxCount){
+			$stateCount[4]++;
+			$j++;
+		}
+
+		if($stateCount[4] >= $maxCount){
+			return null;
+		}
+
+		// If we found a finder-pattern-like section, but its size is significantly different than
+		// the original, assume it's a false positive
+		$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
+
+		if(5 * abs($stateCountTotal - $originalStateCountTotal) >= $originalStateCountTotal){
+			return null;
+		}
+
+		if(!$this->foundPatternCross($stateCount)){
+			return null;
+		}
+
+		return $this->centerFromEnd($stateCount, $j);
+	}
+
+	/**
+	 * <p>This is called when a horizontal scan finds a possible alignment pattern. It will
+	 * cross check with a vertical scan, and if successful, will, ah, cross-cross-check
+	 * with another horizontal scan. This is needed primarily to locate the real horizontal
+	 * center of the pattern in cases of extreme skew.
+	 * And then we cross-cross-cross check with another diagonal scan.</p>
+	 *
+	 * <p>If that succeeds the finder pattern location is added to a list that tracks
+	 * the number of times each location has been nearly-matched as a finder pattern.
+	 * Each additional find is more evidence that the location is in fact a finder
+	 * pattern center
+	 *
+	 * @param int[] $stateCount reading state module counts from horizontal scan
+	 * @param int   $i          row where finder pattern may be found
+	 * @param int   $j          end of possible finder pattern in row
+	 *
+	 * @return bool if a finder pattern candidate was found this time
+	 */
+	private function handlePossibleCenter(array $stateCount, int $i, int $j):bool{
+		$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
+		$centerJ         = $this->centerFromEnd($stateCount, $j);
+		$centerI         = $this->crossCheckVertical($i, (int)$centerJ, $stateCount[2], $stateCountTotal);
+
+		if($centerI !== null){
+			// Re-cross check
+			$centerJ = $this->crossCheckHorizontal((int)$centerJ, (int)$centerI, $stateCount[2], $stateCountTotal);
+			if($centerJ !== null && ($this->crossCheckDiagonal((int)$centerI, (int)$centerJ))){
+				$estimatedModuleSize = $stateCountTotal / 7.0;
+				$found               = false;
+
+				for($index = 0; $index < count($this->possibleCenters); $index++){
+					$center = $this->possibleCenters[$index];
+					// Look for about the same center and module size:
+					if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){
+						$this->possibleCenters[$index] = $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
+						$found                         = true;
+						break;
+					}
+				}
+
+				if(!$found){
+					$point                   = new FinderPattern($centerJ, $centerI, $estimatedModuleSize);
+					$this->possibleCenters[] = $point;
+				}
+
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * @return int number of rows we could safely skip during scanning, based on the first
+	 *         two finder patterns that have been located. In some cases their position will
+	 *         allow us to infer that the third pattern must lie below a certain point farther
+	 *         down in the image.
+	 */
+	private function findRowSkip():int{
+		$max = count($this->possibleCenters);
+
+		if($max <= 1){
+			return 0;
+		}
+
+		$firstConfirmedCenter = null;
+
+		foreach($this->possibleCenters as $center){
+
+			if($center->getCount() >= self::CENTER_QUORUM){
+
+				if($firstConfirmedCenter === null){
+					$firstConfirmedCenter = $center;
+				}
+				else{
+					// We have two confirmed centers
+					// How far down can we skip before resuming looking for the next
+					// pattern? In the worst case, only the difference between the
+					// difference in the x / y coordinates of the two centers.
+					// This is the case where you find top left last.
+					$this->hasSkipped = true;
+
+					return (int)((abs($firstConfirmedCenter->getX() - $center->getX()) -
+					              abs($firstConfirmedCenter->getY() - $center->getY())) / 2);
+				}
+			}
+		}
+
+		return 0;
+	}
+
+	/**
+	 * @return bool true if we have found at least 3 finder patterns that have been detected
+	 *              at least {@link #CENTER_QUORUM} times each, and, the estimated module size of the
+	 *              candidates is "pretty similar"
+	 */
+	private function haveMultiplyConfirmedCenters():bool{
+		$confirmedCount  = 0;
+		$totalModuleSize = 0.0;
+		$max             = count($this->possibleCenters);
+
+		foreach($this->possibleCenters as $pattern){
+			if($pattern->getCount() >= self::CENTER_QUORUM){
+				$confirmedCount++;
+				$totalModuleSize += $pattern->getEstimatedModuleSize();
+			}
+		}
+
+		if($confirmedCount < 3){
+			return false;
+		}
+		// OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive"
+		// and that we need to keep looking. We detect this by asking if the estimated module sizes
+		// vary too much. We arbitrarily say that when the total deviation from average exceeds
+		// 5% of the total module size estimates, it's too much.
+		$average        = $totalModuleSize / (float)$max;
+		$totalDeviation = 0.0;
+
+		foreach($this->possibleCenters as $pattern){
+			$totalDeviation += abs($pattern->getEstimatedModuleSize() - $average);
+		}
+
+		return $totalDeviation <= 0.05 * $totalModuleSize;
+	}
+
+	/**
+	 * @return \chillerlan\QRCode\Detector\FinderPattern[] the 3 best {@link FinderPattern}s from our list of candidates. The "best" are
+	 *         those that have been detected at least {@link #CENTER_QUORUM} times, and whose module
+	 *         size differs from the average among those patterns the least
+	 * @throws \RuntimeException if 3 such finder patterns do not exist
+	 */
+	private function selectBestPatterns():array{
+		$startSize = count($this->possibleCenters);
+
+		if($startSize < 3){
+			throw new RuntimeException('could not find enough finder patterns');
+		}
+
+		usort(
+			$this->possibleCenters,
+			fn(FinderPattern $a, FinderPattern $b) => $a->getEstimatedModuleSize() <=> $b->getEstimatedModuleSize()
+		);
+
+		$distortion   = PHP_FLOAT_MAX;
+		$bestPatterns = [];
+
+		for($i = 0; $i < $startSize - 2; $i++){
+			$fpi           = $this->possibleCenters[$i];
+			$minModuleSize = $fpi->getEstimatedModuleSize();
+
+			for($j = $i + 1; $j < $startSize - 1; $j++){
+				$fpj      = $this->possibleCenters[$j];
+				$squares0 = $fpi->getSquaredDistance($fpj);
+
+				for($k = $j + 1; $k < $startSize; $k++){
+					$fpk           = $this->possibleCenters[$k];
+					$maxModuleSize = $fpk->getEstimatedModuleSize();
+
+					// module size is not similar
+					if($maxModuleSize > $minModuleSize * 1.4){
+						continue;
+					}
+
+					$a = $squares0;
+					$b = $fpj->getSquaredDistance($fpk);
+					$c = $fpi->getSquaredDistance($fpk);
+
+					// sorts ascending - inlined
+					if($a < $b){
+						if($b > $c){
+							if($a < $c){
+								$temp = $b;
+								$b    = $c;
+								$c    = $temp;
+							}
+							else{
+								$temp = $a;
+								$a    = $c;
+								$c    = $b;
+								$b    = $temp;
+							}
+						}
+					}
+					else{
+						if($b < $c){
+							if($a < $c){
+								$temp = $a;
+								$a    = $b;
+								$b    = $temp;
+							}
+							else{
+								$temp = $a;
+								$a    = $b;
+								$b    = $c;
+								$c    = $temp;
+							}
+						}
+						else{
+							$temp = $a;
+							$a    = $c;
+							$c    = $temp;
+						}
+					}
+
+					// a^2 + b^2 = c^2 (Pythagorean theorem), and a = b (isosceles triangle).
+					// Since any right triangle satisfies the formula c^2 - b^2 - a^2 = 0,
+					// we need to check both two equal sides separately.
+					// The value of |c^2 - 2 * b^2| + |c^2 - 2 * a^2| increases as dissimilarity
+					// from isosceles right triangle.
+					$d = abs($c - 2 * $b) + abs($c - 2 * $a);
+
+					if($d < $distortion){
+						$distortion   = $d;
+						$bestPatterns = [$fpi, $fpj, $fpk];
+					}
+				}
+			}
+		}
+
+		if($distortion === PHP_FLOAT_MAX){
+			throw new RuntimeException('finder patterns may be too distorted');
+		}
+
+		return $bestPatterns;
+	}
+
+	/**
+	 * Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC
+	 * and BC is less than AC, and the angle between BC and BA is less than 180 degrees.
+	 *
+	 * @param \chillerlan\QRCode\Detector\FinderPattern[] $patterns array of three FinderPattern to order
+	 *
+	 * @return \chillerlan\QRCode\Detector\FinderPattern[]
+	 */
+	private function orderBestPatterns(array $patterns):array{
+
+		// Find distances between pattern centers
+		$zeroOneDistance = $patterns[0]->getDistance($patterns[1]);
+		$oneTwoDistance  = $patterns[1]->getDistance($patterns[2]);
+		$zeroTwoDistance = $patterns[0]->getDistance($patterns[2]);
+
+		// Assume one closest to other two is B; A and C will just be guesses at first
+		if($oneTwoDistance >= $zeroOneDistance && $oneTwoDistance >= $zeroTwoDistance){
+			[$pointB, $pointA, $pointC] = $patterns;
+		}
+		elseif($zeroTwoDistance >= $oneTwoDistance && $zeroTwoDistance >= $zeroOneDistance){
+			[$pointA, $pointB, $pointC] = $patterns;
+		}
+		else{
+			[$pointA, $pointC, $pointB] = $patterns;
+		}
+
+		// Use cross product to figure out whether A and C are correct or flipped.
+		// This asks whether BC x BA has a positive z component, which is the arrangement
+		// we want for A, B, C. If it's negative, then we've got it flipped around and
+		// should swap A and C.
+		if($this->crossProductZ($pointA, $pointB, $pointC) < 0.0){
+			$temp   = $pointA;
+			$pointA = $pointC;
+			$pointC = $temp;
+		}
+
+		return [$pointA, $pointB, $pointC];
+	}
+
+	/**
+	 * Returns the z component of the cross product between vectors BC and BA.
+	 */
+	private function crossProductZ(FinderPattern $pointA, FinderPattern $pointB, FinderPattern $pointC):float{
+		$bX = $pointB->getX();
+		$bY = $pointB->getY();
+
+		return (($pointC->getX() - $bX) * ($pointA->getY() - $bY)) - (($pointC->getY() - $bY) * ($pointA->getX() - $bX));
+	}
+
+}

+ 171 - 0
src/Detector/GridSampler.php

@@ -0,0 +1,171 @@
+<?php
+/**
+ * Class GridSampler
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use Exception, RuntimeException;
+use chillerlan\QRCode\Decoder\BitMatrix;
+use function array_fill, count, sprintf;
+
+/**
+ * Implementations of this class can, given locations of finder patterns for a QR code in an
+ * image, sample the right points in the image to reconstruct the QR code, accounting for
+ * perspective distortion. It is abstracted since it is relatively expensive and should be allowed
+ * to take advantage of platform-specific optimized implementations, like Sun's Java Advanced
+ * Imaging library, but which may not be available in other environments such as J2ME, and vice
+ * versa.
+ *
+ * The implementation used can be controlled by calling {@link #setGridSampler(GridSampler)}
+ * with an instance of a class which implements this interface.
+ *
+ * @author Sean Owen
+ */
+final class GridSampler{
+
+	/**
+	 * <p>Checks a set of points that have been transformed to sample points on an image against
+	 * the image's dimensions to see if the point are even within the image.</p>
+	 *
+	 * <p>This method will actually "nudge" the endpoints back onto the image if they are found to be
+	 * barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder
+	 * patterns in an image where the QR Code runs all the way to the image border.</p>
+	 *
+	 * <p>For efficiency, the method will check points from either end of the line until one is found
+	 * to be within the image. Because the set of points are assumed to be linear, this is valid.</p>
+	 *
+	 * @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix image into which the points should map
+	 * @param float[]                  $points    actual points in x1,y1,...,xn,yn form
+	 *
+	 * @throws \RuntimeException if an endpoint is lies outside the image boundaries
+	 */
+	private function checkAndNudgePoints(BitMatrix $bitMatrix, array $points):void{
+		$dimension = $bitMatrix->getDimension();
+		$nudged    = true;
+		$max       = count($points);
+
+		// Check and nudge points from start until we see some that are OK:
+		for($offset = 0; $offset < $max && $nudged; $offset += 2){
+			$x = (int)$points[$offset];
+			$y = (int)$points[$offset + 1];
+
+			if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){
+				throw new RuntimeException(sprintf('checkAndNudgePoints 1, x: %s, y: %s, d: %s', $x, $y, $dimension));
+			}
+
+			$nudged = false;
+
+			if($x === -1){
+				$points[$offset] = 0.0;
+				$nudged          = true;
+			}
+			elseif($x === $dimension){
+				$points[$offset] = $dimension - 1;
+				$nudged          = true;
+			}
+			if($y === -1){
+				$points[$offset + 1] = 0.0;
+				$nudged              = true;
+			}
+			elseif($y === $dimension){
+				$points[$offset + 1] = $dimension - 1;
+				$nudged              = true;
+			}
+		}
+		// Check and nudge points from end:
+		$nudged = true;
+
+		for($offset = count($points) - 2; $offset >= 0 && $nudged; $offset -= 2){
+			$x = (int)$points[$offset];
+			$y = (int)$points[$offset + 1];
+
+			if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){
+				throw new RuntimeException(sprintf('checkAndNudgePoints 2, x: %s, y: %s, d: %s', $x, $y, $dimension));
+			}
+
+			$nudged = false;
+
+			if($x === -1){
+				$points[$offset] = 0.0;
+				$nudged          = true;
+			}
+			elseif($x === $dimension){
+				$points[$offset] = $dimension - 1;
+				$nudged          = true;
+			}
+			if($y === -1){
+				$points[$offset + 1] = 0.0;
+				$nudged              = true;
+			}
+			elseif($y === $dimension){
+				$points[$offset + 1] = $dimension - 1;
+				$nudged              = true;
+			}
+		}
+	}
+
+	/**
+	 * Samples an image for a rectangular matrix of bits of the given dimension. The sampling
+	 * transformation is determined by the coordinates of 4 points, in the original and transformed
+	 * image space.
+	 *
+	 * @return \chillerlan\QRCode\Decoder\BitMatrix representing a grid of points sampled from the image within a region
+	 *   defined by the "from" parameters
+	 * @throws \RuntimeException if image can't be sampled, for example, if the transformation defined
+	 *   by the given points is invalid or results in sampling outside the image boundaries
+	 */
+	public function sampleGrid(BitMatrix $image, int $dimension, PerspectiveTransform $transform):BitMatrix{
+
+		if($dimension <= 0){
+			throw new RuntimeException('invalid matrix size');
+		}
+
+		$bits   = new BitMatrix($dimension);
+		$points = array_fill(0, 2 * $dimension, 0.0);
+
+		for($y = 0; $y < $dimension; $y++){
+			$max    = count($points);
+			$iValue = $y + 0.5;
+
+			for($x = 0; $x < $max; $x += 2){
+				$points[$x]     = ($x / 2) + 0.5;
+				$points[$x + 1] = $iValue;
+			}
+
+			$transform->transformPoints($points);
+			// Quick check to see if points transformed to something inside the image;
+			// sufficient to check the endpoints
+			$this->checkAndNudgePoints($image, $points);
+
+			try{
+				for($x = 0; $x < $max; $x += 2){
+					if($image->get((int)$points[$x], (int)$points[$x + 1])){
+						// Black(-ish) pixel
+						$bits->set($x / 2, $y);
+					}
+				}
+			}
+			catch(Exception $aioobe){//ArrayIndexOutOfBoundsException
+				// This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting
+				// transform gets "twisted" such that it maps a straight line of points to a set of points
+				// whose endpoints are in bounds, but others are not. There is probably some mathematical
+				// way to detect this about the transformation that I don't know yet.
+				// This results in an ugly runtime exception despite our clever checks above -- can't have
+				// that. We could check each point's coordinates but that feels duplicative. We settle for
+				// catching and wrapping ArrayIndexOutOfBoundsException.
+				throw new RuntimeException('ArrayIndexOutOfBoundsException');
+			}
+
+		}
+
+		return $bits;
+	}
+
+}

+ 153 - 0
src/Detector/PerspectiveTransform.php

@@ -0,0 +1,153 @@
+<?php
+/**
+ * Class PerspectiveTransform
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use function count;
+
+/**
+ * <p>This class implements a perspective transform in two dimensions. Given four source and four
+ * destination points, it will compute the transformation implied between them. The code is based
+ * directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56.</p>
+ *
+ * @author Sean Owen
+ */
+final class PerspectiveTransform{
+
+	private float $a11;
+	private float $a12;
+	private float $a13;
+	private float $a21;
+	private float $a22;
+	private float $a23;
+	private float $a31;
+	private float $a32;
+	private float $a33;
+
+	private function set(
+		float $a11, float $a21, float $a31,
+		float $a12, float $a22, float $a32,
+		float $a13, float $a23, float $a33
+	):PerspectiveTransform{
+		$this->a11 = $a11;
+		$this->a12 = $a12;
+		$this->a13 = $a13;
+		$this->a21 = $a21;
+		$this->a22 = $a22;
+		$this->a23 = $a23;
+		$this->a31 = $a31;
+		$this->a32 = $a32;
+		$this->a33 = $a33;
+
+		return $this;
+	}
+
+	public function quadrilateralToQuadrilateral(
+		float $x0, float $y0, float $x1, float $y1, float $x2, float $y2, float $x3, float $y3,
+		float $x0p, float $y0p, float $x1p, float $y1p, float $x2p, float $y2p, float $x3p, float $y3p
+	):PerspectiveTransform{
+		return (new self)
+			->squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p)
+			->times($this->quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3));
+	}
+
+	private function quadrilateralToSquare(
+		float $x0, float $y0, float $x1, float $y1,
+		float $x2, float $y2, float $x3, float $y3
+	):PerspectiveTransform{
+		// Here, the adjoint serves as the inverse:
+		return $this
+			->squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)
+			->buildAdjoint();
+	}
+
+	private function buildAdjoint():PerspectiveTransform{
+		// Adjoint is the transpose of the cofactor matrix:
+		return $this->set(
+			$this->a22 * $this->a33 - $this->a23 * $this->a32,
+			$this->a23 * $this->a31 - $this->a21 * $this->a33,
+			$this->a21 * $this->a32 - $this->a22 * $this->a31,
+			$this->a13 * $this->a32 - $this->a12 * $this->a33,
+			$this->a11 * $this->a33 - $this->a13 * $this->a31,
+			$this->a12 * $this->a31 - $this->a11 * $this->a32,
+			$this->a12 * $this->a23 - $this->a13 * $this->a22,
+			$this->a13 * $this->a21 - $this->a11 * $this->a23,
+			$this->a11 * $this->a22 - $this->a12 * $this->a21
+		);
+	}
+
+	private function squareToQuadrilateral(
+		float $x0, float $y0, float $x1, float $y1,
+		float $x2, float $y2, float $x3, float $y3
+	):PerspectiveTransform{
+		$dx3 = $x0 - $x1 + $x2 - $x3;
+		$dy3 = $y0 - $y1 + $y2 - $y3;
+
+		if($dx3 === 0.0 && $dy3 === 0.0){
+			// Affine
+			return $this->set($x1 - $x0, $x2 - $x1, $x0, $y1 - $y0, $y2 - $y1, $y0, 0.0, 0.0, 1.0);
+		}
+
+		$dx1         = $x1 - $x2;
+		$dx2         = $x3 - $x2;
+		$dy1         = $y1 - $y2;
+		$dy2         = $y3 - $y2;
+		$denominator = $dx1 * $dy2 - $dx2 * $dy1;
+		$a13         = ($dx3 * $dy2 - $dx2 * $dy3) / $denominator;
+		$a23         = ($dx1 * $dy3 - $dx3 * $dy1) / $denominator;
+
+		return $this->set(
+			$x1 - $x0 + $a13 * $x1, $x3 - $x0 + $a23 * $x3, $x0,
+			$y1 - $y0 + $a13 * $y1, $y3 - $y0 + $a23 * $y3, $y0,
+			$a13, $a23, 1.0
+		);
+	}
+
+	private function times(PerspectiveTransform $other):PerspectiveTransform{
+		return $this->set(
+			$this->a11 * $other->a11 + $this->a21 * $other->a12 + $this->a31 * $other->a13,
+			$this->a11 * $other->a21 + $this->a21 * $other->a22 + $this->a31 * $other->a23,
+			$this->a11 * $other->a31 + $this->a21 * $other->a32 + $this->a31 * $other->a33,
+			$this->a12 * $other->a11 + $this->a22 * $other->a12 + $this->a32 * $other->a13,
+			$this->a12 * $other->a21 + $this->a22 * $other->a22 + $this->a32 * $other->a23,
+			$this->a12 * $other->a31 + $this->a22 * $other->a32 + $this->a32 * $other->a33,
+			$this->a13 * $other->a11 + $this->a23 * $other->a12 + $this->a33 * $other->a13,
+			$this->a13 * $other->a21 + $this->a23 * $other->a22 + $this->a33 * $other->a23,
+			$this->a13 * $other->a31 + $this->a23 * $other->a32 + $this->a33 * $other->a33
+		);
+	}
+
+	public function transformPoints(array &$xValues, array &$yValues = null):void{
+		$max = count($xValues);
+
+		if($yValues !== null){
+
+			for($i = 0; $i < $max; $i++){
+				$x           = $xValues[$i];
+				$y           = $yValues[$i];
+				$denominator = $this->a13 * $x + $this->a23 * $y + $this->a33;
+				$xValues[$i] = ($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator;
+				$yValues[$i] = ($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator;
+			}
+
+			return;
+		}
+
+		for($i = 0; $i < $max; $i += 2){
+			$x               = $xValues[$i];
+			$y               = $xValues[$i + 1];
+			$denominator     = $this->a13 * $x + $this->a23 * $y + $this->a33;
+			$xValues[$i]     = ($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator;
+			$xValues[$i + 1] = ($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator;
+		}
+	}
+
+}

+ 61 - 0
src/Detector/ResultPoint.php

@@ -0,0 +1,61 @@
+<?php
+/**
+ * Class ResultPoint
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use function abs;
+
+/**
+ * <p>Encapsulates a point of interest in an image containing a barcode. Typically, this
+ * would be the location of a finder pattern or the corner of the barcode, for example.</p>
+ *
+ * @author Sean Owen
+ */
+abstract class ResultPoint{
+
+	protected float $x;
+	protected float $y;
+	protected float $estimatedModuleSize;
+
+	public function __construct(float $x, float $y, float $estimatedModuleSize){
+		$this->x                   = $x;
+		$this->y                   = $y;
+		$this->estimatedModuleSize = $estimatedModuleSize;
+	}
+
+	public function getX():float{
+		return $this->x;
+	}
+
+	public function getY():float{
+		return $this->y;
+	}
+
+	public function getEstimatedModuleSize():float{
+		return $this->estimatedModuleSize;
+	}
+
+	/**
+	 * <p>Determines if this finder pattern "about equals" a finder pattern at the stated
+	 * position and size -- meaning, it is at nearly the same center with nearly the same size.</p>
+	 */
+	public function aboutEquals(float $moduleSize, float $i, float $j):bool{
+
+		if(abs($i - $this->y) <= $moduleSize && abs($j - $this->x) <= $moduleSize){
+			$moduleSizeDiff = abs($moduleSize - $this->estimatedModuleSize);
+
+			return $moduleSizeDiff <= 1.0 || $moduleSizeDiff <= $this->estimatedModuleSize;
+		}
+
+		return false;
+	}
+
+}

+ 0 - 89
src/Helpers/BitBuffer.php

@@ -1,89 +0,0 @@
-<?php
-/**
- * Class BitBuffer
- *
- * @filesource   BitBuffer.php
- * @created      25.11.2015
- * @package      chillerlan\QRCode\Helpers
- * @author       Smiley <smiley@chillerlan.net>
- * @copyright    2015 Smiley
- * @license      MIT
- */
-
-namespace chillerlan\QRCode\Helpers;
-
-use function count, floor;
-
-/**
- * Holds the raw binary data
- */
-final class BitBuffer{
-
-	/**
-	 * The buffer content
-	 *
-	 * @var int[]
-	 */
-	protected array $buffer = [];
-
-	/**
-	 * Length of the content (bits)
-	 */
-	protected int $length = 0;
-
-	/**
-	 * clears the buffer
-	 */
-	public function clear():BitBuffer{
-		$this->buffer = [];
-		$this->length = 0;
-
-		return $this;
-	}
-
-	/**
-	 * appends a sequence of bits
-	 */
-	public function put(int $num, int $length):BitBuffer{
-
-		for($i = 0; $i < $length; $i++){
-			$this->putBit((($num >> ($length - $i - 1)) & 1) === 1);
-		}
-
-		return $this;
-	}
-
-	/**
-	 * appends a single bit
-	 */
-	public function putBit(bool $bit):BitBuffer{
-		$bufIndex = floor($this->length / 8);
-
-		if(count($this->buffer) <= $bufIndex){
-			$this->buffer[] = 0;
-		}
-
-		if($bit === true){
-			$this->buffer[(int)$bufIndex] |= (0x80 >> ($this->length % 8));
-		}
-
-		$this->length++;
-
-		return $this;
-	}
-
-	/**
-	 * returns the current buffer length
-	 */
-	public function getLength():int{
-		return $this->length;
-	}
-
-	/**
-	 * returns the buffer content
-	 */
-	public function getBuffer():array{
-		return $this->buffer;
-	}
-
-}

+ 0 - 178
src/Helpers/Polynomial.php

@@ -1,178 +0,0 @@
-<?php
-/**
- * Class Polynomial
- *
- * @filesource   Polynomial.php
- * @created      25.11.2015
- * @package      chillerlan\QRCode\Helpers
- * @author       Smiley <smiley@chillerlan.net>
- * @copyright    2015 Smiley
- * @license      MIT
- */
-
-namespace chillerlan\QRCode\Helpers;
-
-use chillerlan\QRCode\QRCodeException;
-
-use function array_fill, count, sprintf;
-
-/**
- * Polynomial long division helpers
- *
- * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
- */
-final class Polynomial{
-
-	/**
-	 * @see http://www.thonky.com/qr-code-tutorial/log-antilog-table
-	 */
-	protected const table = [
-		[  1,   0], [  2,   0], [  4,   1], [  8,  25], [ 16,   2], [ 32,  50], [ 64,  26], [128, 198],
-		[ 29,   3], [ 58, 223], [116,  51], [232, 238], [205,  27], [135, 104], [ 19, 199], [ 38,  75],
-		[ 76,   4], [152, 100], [ 45, 224], [ 90,  14], [180,  52], [117, 141], [234, 239], [201, 129],
-		[143,  28], [  3, 193], [  6, 105], [ 12, 248], [ 24, 200], [ 48,   8], [ 96,  76], [192, 113],
-		[157,   5], [ 39, 138], [ 78, 101], [156,  47], [ 37, 225], [ 74,  36], [148,  15], [ 53,  33],
-		[106,  53], [212, 147], [181, 142], [119, 218], [238, 240], [193,  18], [159, 130], [ 35,  69],
-		[ 70,  29], [140, 181], [  5, 194], [ 10, 125], [ 20, 106], [ 40,  39], [ 80, 249], [160, 185],
-		[ 93, 201], [186, 154], [105,   9], [210, 120], [185,  77], [111, 228], [222, 114], [161, 166],
-		[ 95,   6], [190, 191], [ 97, 139], [194,  98], [153, 102], [ 47, 221], [ 94,  48], [188, 253],
-		[101, 226], [202, 152], [137,  37], [ 15, 179], [ 30,  16], [ 60, 145], [120,  34], [240, 136],
-		[253,  54], [231, 208], [211, 148], [187, 206], [107, 143], [214, 150], [177, 219], [127, 189],
-		[254, 241], [225, 210], [223,  19], [163,  92], [ 91, 131], [182,  56], [113,  70], [226,  64],
-		[217,  30], [175,  66], [ 67, 182], [134, 163], [ 17, 195], [ 34,  72], [ 68, 126], [136, 110],
-		[ 13, 107], [ 26,  58], [ 52,  40], [104,  84], [208, 250], [189, 133], [103, 186], [206,  61],
-		[129, 202], [ 31,  94], [ 62, 155], [124, 159], [248,  10], [237,  21], [199, 121], [147,  43],
-		[ 59,  78], [118, 212], [236, 229], [197, 172], [151, 115], [ 51, 243], [102, 167], [204,  87],
-		[133,   7], [ 23, 112], [ 46, 192], [ 92, 247], [184, 140], [109, 128], [218,  99], [169,  13],
-		[ 79, 103], [158,  74], [ 33, 222], [ 66, 237], [132,  49], [ 21, 197], [ 42, 254], [ 84,  24],
-		[168, 227], [ 77, 165], [154, 153], [ 41, 119], [ 82,  38], [164, 184], [ 85, 180], [170, 124],
-		[ 73,  17], [146,  68], [ 57, 146], [114, 217], [228,  35], [213,  32], [183, 137], [115,  46],
-		[230,  55], [209,  63], [191, 209], [ 99,  91], [198, 149], [145, 188], [ 63, 207], [126, 205],
-		[252, 144], [229, 135], [215, 151], [179, 178], [123, 220], [246, 252], [241, 190], [255,  97],
-		[227, 242], [219,  86], [171, 211], [ 75, 171], [150,  20], [ 49,  42], [ 98,  93], [196, 158],
-		[149, 132], [ 55,  60], [110,  57], [220,  83], [165,  71], [ 87, 109], [174,  65], [ 65, 162],
-		[130,  31], [ 25,  45], [ 50,  67], [100, 216], [200, 183], [141, 123], [  7, 164], [ 14, 118],
-		[ 28, 196], [ 56,  23], [112,  73], [224, 236], [221, 127], [167,  12], [ 83, 111], [166, 246],
-		[ 81, 108], [162, 161], [ 89,  59], [178,  82], [121,  41], [242, 157], [249,  85], [239, 170],
-		[195, 251], [155,  96], [ 43, 134], [ 86, 177], [172, 187], [ 69, 204], [138,  62], [  9,  90],
-		[ 18, 203], [ 36,  89], [ 72,  95], [144, 176], [ 61, 156], [122, 169], [244, 160], [245,  81],
-		[247,  11], [243, 245], [251,  22], [235, 235], [203, 122], [139, 117], [ 11,  44], [ 22, 215],
-		[ 44,  79], [ 88, 174], [176, 213], [125, 233], [250, 230], [233, 231], [207, 173], [131, 232],
-		[ 27, 116], [ 54, 214], [108, 244], [216, 234], [173, 168], [ 71,  80], [142,  88], [  1, 175],
-	];
-
-	/**
-	 * @var int[]
-	 */
-	protected array $num = [];
-
-	/**
-	 * Polynomial constructor.
-	 */
-	public function __construct(array $num = null, int $shift = null){
-		$this->setNum($num ?? [1], $shift);
-	}
-
-	/**
-	 *
-	 */
-	public function getNum():array{
-		return $this->num;
-	}
-
-	/**
-	 * @param int[]    $num
-	 * @param int|null $shift
-	 *
-	 * @return \chillerlan\QRCode\Helpers\Polynomial
-	 */
-	public function setNum(array $num, int $shift = null):Polynomial{
-		$offset = 0;
-		$numCount = count($num);
-
-		while($offset < $numCount && $num[$offset] === 0){
-			$offset++;
-		}
-
-		$this->num = array_fill(0, $numCount - $offset + ($shift ?? 0), 0);
-
-		for($i = 0; $i < $numCount - $offset; $i++){
-			$this->num[$i] = $num[$i + $offset];
-		}
-
-		return $this;
-	}
-
-	/**
-	 * @param int[] $e
-	 *
-	 * @return \chillerlan\QRCode\Helpers\Polynomial
-	 */
-	public function multiply(array $e):Polynomial{
-		$n = array_fill(0, count($this->num) + count($e) - 1, 0);
-
-		foreach($this->num as $i => $vi){
-			$vi = $this->glog($vi);
-
-			foreach($e as $j => $vj){
-				$n[$i + $j] ^= $this->gexp($vi + $this->glog($vj));
-			}
-
-		}
-
-		$this->setNum($n);
-
-		return $this;
-	}
-
-	/**
-	 * @param int[] $e
-	 *
-	 * @return \chillerlan\QRCode\Helpers\Polynomial
-	 */
-	public function mod(array $e):Polynomial{
-		$n = $this->num;
-
-		if(count($n) - count($e) < 0){
-			return $this;
-		}
-
-		$ratio = $this->glog($n[0]) - $this->glog($e[0]);
-
-		foreach($e as $i => $v){
-			$n[$i] ^= $this->gexp($this->glog($v) + $ratio);
-		}
-
-		$this->setNum($n)->mod($e);
-
-		return $this;
-	}
-
-	/**
-	 * @throws \chillerlan\QRCode\QRCodeException
-	 */
-	public function glog(int $n):int{
-
-		if($n < 1){
-			throw new QRCodeException(sprintf('log(%s)', $n));
-		}
-
-		return Polynomial::table[$n][1];
-	}
-
-	/**
-	 *
-	 */
-	public function gexp(int $n):int{
-
-		if($n < 0){
-			$n += 255;
-		}
-		elseif($n >= 256){
-			$n -= 255;
-		}
-
-		return Polynomial::table[$n][0];
-	}
-
-}

+ 0 - 2
src/Output/QRCodeOutputException.php

@@ -2,9 +2,7 @@
 /**
  * Class QRCodeOutputException
  *
- * @filesource   QRCodeOutputException.php
  * @created      09.12.2015
- * @package      chillerlan\QRCode\Output
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT

+ 3 - 6
src/Output/QRFpdf.php

@@ -2,14 +2,11 @@
 /**
  * Class QRFpdf
  *
- * https://github.com/chillerlan/php-qrcode/pull/49
- *
- * @filesource   QRFpdf.php
  * @created      03.06.2020
- * @package      chillerlan\QRCode\Output
  * @author       Maximilian Kresse
- *
  * @license      MIT
+ *
+ * @see https://github.com/chillerlan/php-qrcode/pull/49
  */
 
 namespace chillerlan\QRCode\Output;
@@ -104,7 +101,7 @@ class QRFpdf extends QROutputAbstract{
 		}
 
 		if($this->options->imageBase64){
-			$pdfData = sprintf('data:application/pdf;base64,%s', base64_encode($pdfData));
+			$pdfData = $this->base64encode($pdfData, 'application/pdf');
 		}
 
 		return $pdfData;

+ 3 - 7
src/Output/QRImage.php

@@ -2,9 +2,7 @@
 /**
  * Class QRImage
  *
- * @filesource   QRImage.php
  * @created      05.12.2015
- * @package      chillerlan\QRCode\Output
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT
@@ -19,9 +17,9 @@ use chillerlan\QRCode\{QRCode, QRCodeException};
 use chillerlan\Settings\SettingsContainerInterface;
 use Exception;
 
-use function array_values, base64_encode, call_user_func, count, extension_loaded, imagecolorallocate, imagecolortransparent,
+use function array_values, call_user_func, count, extension_loaded, imagecolorallocate, imagecolortransparent,
 	imagecreatetruecolor, imagedestroy, imagefilledrectangle, imagegif, imagejpeg, imagepng, in_array,
-	is_array, ob_end_clean, ob_get_contents, ob_start, range, sprintf;
+	is_array, ob_end_clean, ob_get_contents, ob_start, range;
 
 /**
  * Converts the matrix into GD images, raw or base64 output (requires ext-gd)
@@ -47,8 +45,6 @@ class QRImage extends QROutputAbstract{
 	 *
 	 * @see imagecreatetruecolor()
 	 * @var resource|\GdImage
-	 *
-	 * @phan-suppress PhanUndeclaredTypeProperty
 	 */
 	protected $image;
 
@@ -128,7 +124,7 @@ class QRImage extends QROutputAbstract{
 		}
 
 		if($this->options->imageBase64){
-			$imageData = sprintf('data:image/%s;base64,%s', $this->options->outputType, base64_encode($imageData));
+			$imageData = $this->base64encode($imageData, 'image/'.$this->options->outputType);
 		}
 
 		return $imageData;

+ 0 - 2
src/Output/QRImagick.php

@@ -2,9 +2,7 @@
 /**
  * Class QRImagick
  *
- * @filesource   QRImagick.php
  * @created      04.07.2018
- * @package      chillerlan\QRCode\Output
  * @author       smiley <smiley@chillerlan.net>
  * @copyright    2018 smiley
  * @license      MIT

+ 1 - 3
src/Output/QRMarkup.php

@@ -2,9 +2,7 @@
 /**
  * Class QRMarkup
  *
- * @filesource   QRMarkup.php
  * @created      17.12.2016
- * @package      chillerlan\QRCode\Output
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2016 Smiley
  * @license      MIT
@@ -151,7 +149,7 @@ class QRMarkup extends QROutputAbstract{
 		}
 
 		if($this->options->imageBase64){
-			$svg = sprintf('data:image/svg+xml;base64,%s', base64_encode($svg));
+			$svg = $this->base64encode($svg, 'image/svg+xml');
 		}
 
 		return $svg;

+ 10 - 4
src/Output/QROutputAbstract.php

@@ -2,9 +2,7 @@
 /**
  * Class QROutputAbstract
  *
- * @filesource   QROutputAbstract.php
  * @created      09.12.2015
- * @package      chillerlan\QRCode\Output
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT
@@ -14,8 +12,7 @@ namespace chillerlan\QRCode\Output;
 
 use chillerlan\QRCode\{Data\QRMatrix, QRCode};
 use chillerlan\Settings\SettingsContainerInterface;
-
-use function call_user_func_array, dirname, file_put_contents, get_called_class, in_array, is_writable, sprintf;
+use function base64_encode, call_user_func_array, dirname, file_put_contents, get_called_class, in_array, is_writable, sprintf;
 
 /**
  * common output abstract
@@ -92,6 +89,13 @@ abstract class QROutputAbstract implements QROutputInterface{
 	 */
 	abstract protected function setModuleValues():void;
 
+	/**
+	 * Returns a base64 data URI for the given string and mime type
+	 */
+	protected function base64encode(string $data, string $mime):string{
+		return sprintf('data:%s;base64,%s', $mime, base64_encode($data));
+	}
+
 	/**
 	 * saves the qr data to a file
 	 *
@@ -111,6 +115,8 @@ abstract class QROutputAbstract implements QROutputInterface{
 
 	/**
 	 * @inheritDoc
+	 *
+	 * @return mixed
 	 */
 	public function dump(string $file = null){
 		$file ??= $this->options->cachefile;

+ 20 - 22
src/Output/QROutputInterface.php

@@ -2,9 +2,7 @@
 /**
  * Interface QROutputInterface,
  *
- * @filesource   QROutputInterface.php
  * @created      02.12.2015
- * @package      chillerlan\QRCode\Output
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT
@@ -21,27 +19,27 @@ interface QROutputInterface{
 
 	const DEFAULT_MODULE_VALUES = [
 		// light
-		QRMatrix::M_NULL            => false, // 0
-		QRMatrix::M_DATA            => false, // 4
-		QRMatrix::M_FINDER          => false, // 6
-		QRMatrix::M_SEPARATOR       => false, // 8
-		QRMatrix::M_ALIGNMENT       => false, // 10
-		QRMatrix::M_TIMING          => false, // 12
-		QRMatrix::M_FORMAT          => false, // 14
-		QRMatrix::M_VERSION         => false, // 16
-		QRMatrix::M_QUIETZONE       => false, // 18
-		QRMatrix::M_LOGO            => false, // 20
-		QRMatrix::M_TEST            => false, // 255
+		QRMatrix::M_NULL                           => false,
+		QRMatrix::M_DATA                           => false,
+		QRMatrix::M_FINDER                         => false,
+		QRMatrix::M_SEPARATOR                      => false,
+		QRMatrix::M_ALIGNMENT                      => false,
+		QRMatrix::M_TIMING                         => false,
+		QRMatrix::M_FORMAT                         => false,
+		QRMatrix::M_VERSION                        => false,
+		QRMatrix::M_QUIETZONE                      => false,
+		QRMatrix::M_LOGO                           => false,
+		QRMatrix::M_TEST                           => false,
 		// dark
-		QRMatrix::M_DARKMODULE << 8 => true,  // 512
-		QRMatrix::M_DATA << 8       => true,  // 1024
-		QRMatrix::M_FINDER << 8     => true,  // 1536
-		QRMatrix::M_ALIGNMENT << 8  => true,  // 2560
-		QRMatrix::M_TIMING << 8     => true,  // 3072
-		QRMatrix::M_FORMAT << 8     => true,  // 3584
-		QRMatrix::M_VERSION << 8    => true,  // 4096
-		QRMatrix::M_FINDER_DOT << 8 => true,  // 5632
-		QRMatrix::M_TEST << 8       => true,  // 65280
+		QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK => true,
+		QRMatrix::M_DATA | QRMatrix::IS_DARK       => true,
+		QRMatrix::M_FINDER | QRMatrix::IS_DARK     => true,
+		QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK  => true,
+		QRMatrix::M_TIMING | QRMatrix::IS_DARK     => true,
+		QRMatrix::M_FORMAT | QRMatrix::IS_DARK     => true,
+		QRMatrix::M_VERSION | QRMatrix::IS_DARK    => true,
+		QRMatrix::M_FINDER_DOT | QRMatrix::IS_DARK => true,
+		QRMatrix::M_TEST | QRMatrix::IS_DARK       => true,
 	];
 
 	/**

+ 0 - 2
src/Output/QRString.php

@@ -2,9 +2,7 @@
 /**
  * Class QRString
  *
- * @filesource   QRString.php
  * @created      05.12.2015
- * @package      chillerlan\QRCode\Output
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT

+ 127 - 133
src/QRCode.php

@@ -2,9 +2,7 @@
 /**
  * Class QRCode
  *
- * @filesource   QRCode.php
  * @created      26.11.2015
- * @package      chillerlan\QRCode
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT
@@ -12,15 +10,11 @@
 
 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\QRCode\Common\{ECICharset, MaskPattern, MaskPatternTester, Mode};
+use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number, QRData, QRCodeDataException, QRDataModeInterface, 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;
+use function class_exists, class_implements, in_array, mb_convert_encoding, mb_detect_encoding;
 
 /**
  * Turns a text string into a Model 2 QR Code
@@ -38,58 +32,6 @@ class QRCode{
 	/** @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 */
@@ -134,21 +76,18 @@ class QRCode{
 			self::OUTPUT_IMAGICK,
 		],
 		QRFpdf::class => [
-			self::OUTPUT_FPDF
-		]
+			self::OUTPUT_FPDF,
+		],
 	];
 
 	/**
-	 * Map of data mode => interface
+	 * A collection of one or more data segments of [classname, data] to write
+	 *
+	 * @see \chillerlan\QRCode\Data\QRDataModeInterface
 	 *
-	 * @var string[]
+	 * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
 	 */
-	protected const DATA_INTERFACES = [
-		'number'   => Number::class,
-		'alphanum' => AlphaNum::class,
-		'kanji'    => Kanji::class,
-		'byte'     => Byte::class,
-	];
+	protected array $dataSegments = [];
 
 	/**
 	 * The settings container
@@ -160,7 +99,7 @@ class QRCode{
 	/**
 	 * The selected data interface (Number, AlphaNum, Kanji, Byte)
 	 */
-	protected QRDataInterface $dataInterface;
+	protected QRData $dataInterface;
 
 	/**
 	 * QRCode constructor.
@@ -176,8 +115,23 @@ class QRCode{
 	 *
 	 * @return mixed
 	 */
-	public function render(string $data, string $file = null){
-		return $this->initOutputInterface($data)->dump($file);
+	public function render(string $data = null, string $file = null){
+
+		if($data !== null){
+			/** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */
+			foreach(Mode::DATA_INTERFACES as $dataInterface){
+
+				if($dataInterface::validateString($data)){
+					$this->addSegment(new $dataInterface($data));
+
+					break;
+				}
+
+			}
+
+		}
+
+		return $this->initOutputInterface()->dump($file);
 	}
 
 	/**
@@ -185,21 +139,21 @@ class QRCode{
 	 *
 	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
 	 */
-	public function getMatrix(string $data):QRMatrix{
+	public function getMatrix():QRMatrix{
 
-		if(empty($data)){
+		if(empty($this->dataSegments)){
 			throw new QRCodeDataException('QRCode::getMatrix() No data given.');
 		}
 
-		$this->dataInterface = $this->initDataInterface($data);
+		$this->dataInterface = new QRData($this->options, $this->dataSegments);
 
 		$maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
 			? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern()
-			: $this->options->maskPattern;
+			: new MaskPattern($this->options->maskPattern);
 
-		$matrix = $this->dataInterface->initMatrix($maskPattern);
+		$matrix = $this->dataInterface->writeMatrix($maskPattern);
 
-		if((bool)$this->options->addQuietzone){
+		if($this->options->addQuietzone){
 			$matrix->setQuietZone($this->options->quietzoneSize);
 		}
 
@@ -207,107 +161,147 @@ class QRCode{
 	}
 
 	/**
-	 * returns a fresh QRDataInterface for the given $data
+	 * returns a fresh (built-in) QROutputInterface
 	 *
-	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
 	 */
-	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;
+	protected function initOutputInterface():QROutputInterface{
 
-		if($interface !== null){
-			return new $interface($this->options, $data);
+		if($this->options->outputType === $this::OUTPUT_CUSTOM){
+			return $this->initCustomOutputInterface();
 		}
 
-		foreach($this::DATA_INTERFACES as $mode => $dataInterface){
+		foreach($this::OUTPUT_MODES as $outputInterface => $modes){
 
-			if(call_user_func_array([$this, 'is'.$mode], [$data])){
-				return new $dataInterface($this->options, $data);
+			if(in_array($this->options->outputType, $modes)){
+				return new $outputInterface($this->options, $this->getMatrix());
 			}
 
 		}
 
-		throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore
+		throw new QRCodeOutputException('invalid output type');
 	}
 
 	/**
-	 * returns a fresh (built-in) QROutputInterface
+	 * initializes a custom output module after checking the existence of the class and if it implemnts the required interface
 	 *
 	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
 	 */
-	protected function initOutputInterface(string $data):QROutputInterface{
+	protected function initCustomOutputInterface():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));
+		if(!class_exists($this->options->outputInterface)){
+			throw new QRCodeOutputException('invalid custom output module');
 		}
 
-		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));
-			}
-
+		if(!in_array(QROutputInterface::class, class_implements($this->options->outputInterface))){
+			throw new QRCodeOutputException('custom output module does not implement QROutputInterface');
 		}
 
-		throw new QRCodeOutputException('invalid output type');
+		return new $this->options->outputInterface($this->options, $this->getMatrix());
 	}
 
 	/**
-	 * checks if a string qualifies as numeric
+	 * checks if a string qualifies as numeric (convenience method)
 	 */
 	public function isNumber(string $string):bool{
-		return $this->checkString($string, QRDataInterface::CHAR_MAP_NUMBER);
+		return Number::validateString($string);
 	}
 
 	/**
-	 * checks if a string qualifies as alphanumeric
+	 * checks if a string qualifies as alphanumeric (convenience method)
 	 */
 	public function isAlphaNum(string $string):bool{
-		return $this->checkString($string, QRDataInterface::CHAR_MAP_ALPHANUM);
+		return AlphaNum::validateString($string);
 	}
 
 	/**
-	 * checks is a given $string matches the characters of a given $charmap, returns false on the first invalid occurence.
+	 * checks if a string qualifies as Kanji (convenience method)
 	 */
-	protected function checkString(string $string, array $charmap):bool{
+	public function isKanji(string $string):bool{
+		return Kanji::validateString($string);
+	}
 
-		foreach(str_split($string) as $chr){
-			if(!isset($charmap[$chr])){
-				return false;
-			}
-		}
+	/**
+	 * a dummy (convenience method)
+	 */
+	public function isByte(string $string):bool{
+		return Byte::validateString($string);
+	}
 
-		return true;
+	/**
+	 * ISO/IEC 18004:2000 8.3.6 - Mixing modes
+	 * ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length
+	 */
+	protected function addSegment(QRDataModeInterface $segment):void{
+		$this->dataSegments[] = $segment;
 	}
 
 	/**
-	 * checks if a string qualifies as Kanji
+	 * ISO/IEC 18004:2000 8.3.2 - Numeric Mode
 	 */
-	public function isKanji(string $string):bool{
-		$i   = 0;
-		$len = strlen($string);
+	public function addNumberSegment(string $data):QRCode{
+		$this->addSegment(new Number($data));
 
-		while($i + 1 < $len){
-			$c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1]));
+		return $this;
+	}
 
-			if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){
-				return false;
-			}
+	/**
+	 * ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode
+	 */
+	public function addAlphaNumSegment(string $data):QRCode{
+		$this->addSegment(new AlphaNum($data));
 
-			$i += 2;
-		}
+		return $this;
+	}
+
+	/**
+	 * ISO/IEC 18004:2000 8.3.5 - Kanji Mode
+	 */
+	public function addKanjiSegment(string $data):QRCode{
+		$this->addSegment(new Kanji($data));
+
+		return $this;
+	}
+
+	/**
+	 * ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode
+	 */
+	public function addByteSegment(string $data):QRCode{
+		$this->addSegment(new Byte($data));
+
+		return $this;
+	}
+
+	/**
+	 * ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode
+	 */
+	public function addEciDesignator(int $encoding):QRCode{
+		$this->addSegment(new ECI($encoding));
 
-		return $i >= $len;
+		return $this;
 	}
 
 	/**
-	 * a dummy
+	 * i hate this somehow but i'll leave it for now
+	 *
+	 * @throws \chillerlan\QRCode\QRCodeException
 	 */
-	public function isByte(string $data):bool{
-		return !empty($data);
+	public function addEciSegment(int $encoding, string $data):QRCode{
+		// validate the encoding id
+		$eciCharset = new ECICharset($encoding);
+		// get charset name
+		$eciCharsetName = $eciCharset->getName();
+		// convert the string to the given charset
+		if($eciCharsetName !== null){
+			$data = mb_convert_encoding($data, $eciCharsetName, mb_detect_encoding($data));
+			// add ECI designator
+			$this->addSegment(new ECI($eciCharset->getID()));
+			$this->addSegment(new Byte($data));
+
+			return $this;
+		}
+
+		throw new QRCodeException('unable to add ECI segment');
 	}
 
 }

+ 0 - 2
src/QRCodeException.php

@@ -2,9 +2,7 @@
 /**
  * Class QRCodeException
  *
- * @filesource   QRCodeException.php
  * @created      27.11.2015
- * @package      chillerlan\QRCode
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT

+ 87 - 0
src/QRCodeReader.php

@@ -0,0 +1,87 @@
+<?php
+/**
+ * Class QRCodeReader
+ *
+ * @created      17.01.2021
+ * @author       ZXing Authors
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      Apache-2.0
+ *
+ * @noinspection PhpComposerExtensionStubsInspection
+ */
+
+namespace chillerlan\QRCode;
+
+use Imagick, InvalidArgumentException;
+use chillerlan\QRCode\Decoder\{Decoder, DecoderResult, GDLuminanceSource, IMagickLuminanceSource};
+use function extension_loaded, file_exists, file_get_contents, imagecreatefromstring, is_file, is_readable;
+
+final class QRCodeReader{
+
+	private bool $useImagickIfAvailable;
+
+	public function __construct(bool $useImagickIfAvailable = true){
+		$this->useImagickIfAvailable = $useImagickIfAvailable && extension_loaded('imagick');
+	}
+
+	/**
+	 * @param \Imagick|\GdImage|resource $im
+	 *
+	 * @return \chillerlan\QRCode\Decoder\DecoderResult
+	 */
+	private function decode($im):DecoderResult{
+
+		$source = $this->useImagickIfAvailable
+			? new IMagickLuminanceSource($im)
+			: new GDLuminanceSource($im);
+
+		return (new Decoder)->decode($source);
+	}
+
+	/**
+	 * @param string $imgFilePath
+	 *
+	 * @return \chillerlan\QRCode\Decoder\DecoderResult
+	 */
+	public function readFile(string $imgFilePath):DecoderResult{
+
+		if(!file_exists($imgFilePath) || !is_file($imgFilePath) || !is_readable($imgFilePath)){
+			throw new InvalidArgumentException('invalid file: '.$imgFilePath);
+		}
+
+		$im = $this->useImagickIfAvailable
+			? new Imagick($imgFilePath)
+			: imagecreatefromstring(file_get_contents($imgFilePath));
+
+		return $this->decode($im);
+	}
+
+	/**
+	 * @param string $imgBlob
+	 *
+	 * @return \chillerlan\QRCode\Decoder\DecoderResult
+	 */
+	public function readBlob(string $imgBlob):DecoderResult{
+
+		if($this->useImagickIfAvailable){
+			$im = new Imagick;
+			$im->readImageBlob($imgBlob);
+		}
+		else{
+			$im = imagecreatefromstring($imgBlob);
+		}
+
+		return $this->decode($im);
+	}
+
+	/**
+	 * @param \Imagick|\GdImage|resource $imgSource
+	 *
+	 * @return \chillerlan\QRCode\Decoder\DecoderResult
+	 */
+	public function readResource($imgSource):DecoderResult{
+		return $this->decode($imgSource);
+	}
+
+}

+ 0 - 3
src/QROptions.php

@@ -2,9 +2,7 @@
 /**
  * Class QROptions
  *
- * @filesource   QROptions.php
  * @created      08.12.2015
- * @package      chillerlan\QRCode
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT
@@ -24,7 +22,6 @@ use chillerlan\Settings\SettingsContainerAbstract;
  * @property int         $maskPattern
  * @property bool        $addQuietzone
  * @property int         $quietzoneSize
- * @property string|null $dataModeOverride
  * @property string      $outputType
  * @property string|null $outputInterface
  * @property string|null $cachefile

+ 4 - 13
src/QROptionsTrait.php

@@ -2,9 +2,7 @@
 /**
  * Trait QROptionsTrait
  *
- * @filesource   QROptionsTrait.php
  * @created      10.03.2018
- * @package      chillerlan\QRCode
  * @author       smiley <smiley@chillerlan.net>
  * @copyright    2018 smiley
  * @license      MIT
@@ -14,6 +12,8 @@
 
 namespace chillerlan\QRCode;
 
+use chillerlan\QRCode\Common\EccLevel;
+
 use function array_values, count, in_array, is_numeric, max, min, sprintf, strtolower;
 
 /**
@@ -50,7 +50,7 @@ trait QROptionsTrait{
 	 *   - Q => 25%
 	 *   - H => 30%
 	 */
-	protected int $eccLevel = QRCode::ECC_L;
+	protected int $eccLevel = EccLevel::L;
 
 	/**
 	 * Mask Pattern to use
@@ -71,15 +71,6 @@ trait QROptionsTrait{
 	 */
 	protected int $quietzoneSize = 4;
 
-	/**
-	 * Use this to circumvent the data mode detection and force the usage of the given mode.
-	 *
-	 * valid modes are: Number, AlphaNum, Kanji, Byte (case insensitive)
-	 *
-	 * @see https://github.com/chillerlan/php-qrcode/issues/39
-	 */
-	protected ?string $dataModeOverride = null;
-
 	/**
 	 * The output type
 	 *
@@ -260,7 +251,7 @@ trait QROptionsTrait{
 	 */
 	protected function set_eccLevel(int $eccLevel):void{
 
-		if(!isset(QRCode::ECC_MODES[$eccLevel])){
+		if(!isset(EccLevel::MODES[$eccLevel])){
 			throw new QRCodeException(sprintf('Invalid error correct level: %s', $eccLevel));
 		}
 

+ 6 - 9
tests/Helpers/BitBufferTest.php → tests/Common/BitBufferTest.php

@@ -2,18 +2,15 @@
 /**
  * Class BitBufferTest
  *
- * @filesource   BitBufferTest.php
  * @created      08.02.2016
- * @package      chillerlan\QRCodeTest\Helpers
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2015 Smiley
  * @license      MIT
  */
 
-namespace chillerlan\QRCodeTest\Helpers;
+namespace chillerlan\QRCodeTest\Common;
 
-use chillerlan\QRCode\QRCode;
-use chillerlan\QRCode\Helpers\BitBuffer;
+use chillerlan\QRCode\Common\{BitBuffer, Mode};
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -29,10 +26,10 @@ final class BitBufferTest extends TestCase{
 
 	public function bitProvider():array{
 		return [
-			'number'   => [QRCode::DATA_NUMBER, 16],
-			'alphanum' => [QRCode::DATA_ALPHANUM, 32],
-			'byte'     => [QRCode::DATA_BYTE, 64],
-			'kanji'    => [QRCode::DATA_KANJI, 128],
+			'number'   => [Mode::DATA_NUMBER, 16],
+			'alphanum' => [Mode::DATA_ALPHANUM, 32],
+			'byte'     => [Mode::DATA_BYTE, 64],
+			'kanji'    => [Mode::DATA_KANJI, 128],
 		];
 	}
 

+ 7 - 8
tests/Data/MaskPatternTesterTest.php → tests/Common/MaskPatternTesterTest.php

@@ -2,18 +2,17 @@
 /**
  * Class MaskPatternTesterTest
  *
- * @filesource   MaskPatternTesterTest.php
  * @created      24.11.2017
- * @package      chillerlan\QRCodeTest\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
  */
 
-namespace chillerlan\QRCodeTest\Data;
+namespace chillerlan\QRCodeTest\Common;
 
+use chillerlan\QRCode\Common\{MaskPattern, MaskPatternTester};
+use chillerlan\QRCode\Data\{Byte, QRData};
 use chillerlan\QRCode\QROptions;
-use chillerlan\QRCode\Data\{Byte, MaskPatternTester};
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -25,18 +24,18 @@ final class MaskPatternTesterTest extends TestCase{
 	 * Tests getting the best mask pattern
 	 */
 	public function testMaskpattern():void{
-		$dataInterface = new Byte(new QROptions(['version' => 10]), 'test');
+		$dataInterface = new QRData(new QROptions(['version' => 10]), [new Byte('test')]);
 
-		$this::assertSame(3, (new MaskPatternTester($dataInterface))->getBestMaskPattern());
+		$this::assertSame(3, (new MaskPatternTester($dataInterface))->getBestMaskPattern()->getPattern());
 	}
 
 	/**
 	 * Tests getting the penalty value for a given mask pattern
 	 */
 	public function testMaskpatternID():void{
-		$dataInterface = new Byte(new QROptions(['version' => 10]), 'test');
+		$dataInterface = new QRData(new QROptions(['version' => 10]), [new Byte('test')]);
 
-		$this::assertSame(4243, (new MaskPatternTester($dataInterface))->testPattern(3));
+		$this::assertSame(4243, (new MaskPatternTester($dataInterface))->testPattern(new MaskPattern(MaskPattern::PATTERN_011)));
 	}
 
 }

+ 5 - 14
tests/Data/AlphaNumTest.php

@@ -2,9 +2,7 @@
 /**
  * Class AlphaNumTest
  *
- * @filesource   AlphaNumTest.php
  * @created      24.11.2017
- * @package      chillerlan\QRCodeTest\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -12,8 +10,7 @@
 
 namespace chillerlan\QRCodeTest\Data;
 
-use chillerlan\QRCode\Data\{AlphaNum, QRCodeDataException, QRDataInterface};
-use chillerlan\QRCode\QROptions;
+use chillerlan\QRCode\Data\{AlphaNum, QRCodeDataException};
 
 /**
  * Tests the AlphaNum class
@@ -21,7 +18,7 @@ use chillerlan\QRCode\QROptions;
 final class AlphaNumTest extends DatainterfaceTestAbstract{
 
 	/** @internal */
-	protected string $testdata  = '0 $%*+-./:';
+	protected array $testdata = [AlphaNum::class, '0 $%*+-./:'];
 
 	/** @internal */
 	protected array  $expected  = [
@@ -40,14 +37,6 @@ final class AlphaNumTest extends DatainterfaceTestAbstract{
 		92, 112, 20, 198, 27
 	];
 
-	/**
-	 * @inheritDoc
-	 * @internal
-	 */
-	protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{
-		return new AlphaNum($options);
-	}
-
 	/**
 	 * Tests if an exception is thrown when an invalid character is encountered
 	 */
@@ -55,7 +44,9 @@ final class AlphaNumTest extends DatainterfaceTestAbstract{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('illegal char: "#" [35]');
 
-		$this->dataInterface->setData('#');
+		$this->testdata = [AlphaNum::class, '#'];
+
+		$this->setTestData();
 	}
 
 }

+ 1 - 13
tests/Data/ByteTest.php

@@ -2,9 +2,7 @@
 /**
  * Class ByteTest
  *
- * @filesource   ByteTest.php
  * @created      24.11.2017
- * @package      chillerlan\QRCodeTest\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -13,8 +11,6 @@
 namespace chillerlan\QRCodeTest\Data;
 
 use chillerlan\QRCode\Data\Byte;
-use chillerlan\QRCode\Data\QRDataInterface;
-use chillerlan\QRCode\QROptions;
 
 /**
  * Tests the Byte class
@@ -22,7 +18,7 @@ use chillerlan\QRCode\QROptions;
 final class ByteTest extends DatainterfaceTestAbstract{
 
 	/** @internal */
-	protected string $testdata = '[¯\_(ツ)_/¯]';
+	protected array $testdata = [Byte::class, '[¯\_(ツ)_/¯]'];
 
 	/** @internal */
 	protected array  $expected = [
@@ -41,12 +37,4 @@ final class ByteTest extends DatainterfaceTestAbstract{
 		21, 47, 250, 101
 	];
 
-	/**
-	 * @inheritDoc
-	 * @internal
-	 */
-	protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{
-		return new Byte($options);
-	}
-
 }

+ 31 - 25
tests/Data/DatainterfaceTestAbstract.php

@@ -2,9 +2,7 @@
 /**
  * Class DatainterfaceTestAbstract
  *
- * @filesource   DatainterfaceTestAbstract.php
  * @created      24.11.2017
- * @package      chillerlan\QRCodeTest\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -12,10 +10,11 @@
 
 namespace chillerlan\QRCodeTest\Data;
 
+use chillerlan\QRCode\Common\MaskPattern;
 use chillerlan\QRCode\QRCode;
 use chillerlan\QRCode\QROptions;
 use PHPUnit\Framework\TestCase;
-use chillerlan\QRCode\Data\{QRCodeDataException, QRDataInterface, QRMatrix};
+use chillerlan\QRCode\Data\{QRCodeDataException, QRData, QRMatrix};
 use ReflectionClass;
 
 use function str_repeat;
@@ -28,45 +27,47 @@ abstract class DatainterfaceTestAbstract extends TestCase{
 	/** @internal */
 	protected ReflectionClass $reflection;
 	/** @internal */
-	protected QRDataInterface $dataInterface;
+	protected QRData $dataInterface;
 	/** @internal */
-	protected string $testdata;
+	protected array $testdata;
 	/** @internal */
-	protected array  $expected;
+	protected array $expected;
 
 	/**
 	 * @internal
 	 */
 	protected function setUp():void{
-		$this->dataInterface = $this->getDataInterfaceInstance(new QROptions(['version' => 4]));
+		$this->dataInterface = new QRData(new QROptions(['version' => 4]), []);
 		$this->reflection    = new ReflectionClass($this->dataInterface);
 	}
 
-	/**
-	 * Returns a data interface instance
-	 *
-	 * @internal
-	 */
-	abstract protected function getDataInterfaceInstance(QROptions $options):QRDataInterface;
+	protected function setTestData():void{
+		[$class, $data] = $this->testdata;
+		$this->dataInterface->setData([new $class($data)]);
+	}
 
 	/**
 	 * Verifies the data interface instance
 	 */
 	public function testInstance():void{
-		$this::assertInstanceOf(QRDataInterface::class, $this->dataInterface);
+		$this::assertInstanceOf(QRData::class, $this->dataInterface);
 	}
 
 	/**
 	 * Tests ecc masking and verifies against a sample
 	 */
-	public function testMaskEcc():void{
-		$this->dataInterface->setData($this->testdata);
+/*	public function testMaskEcc():void{
+		$this->dataInterface->setData([$this->testdata]);
 
 		$maskECC = $this->reflection->getMethod('maskECC');
 		$maskECC->setAccessible(true);
 
-		$this::assertSame($this->expected, $maskECC->invoke($this->dataInterface));
-	}
+		$bitBuffer = $this->reflection->getProperty('bitBuffer');
+		$bitBuffer->setAccessible(true);
+		$bb = $bitBuffer->getValue($this->dataInterface);
+
+		$this::assertSame($this->expected, $maskECC->invokeArgs($this->dataInterface, [$bb->getBuffer()]));
+	}*/
 
 	/**
 	 * @see testInitMatrix()
@@ -83,19 +84,19 @@ abstract class DatainterfaceTestAbstract extends TestCase{
 	 * @dataProvider MaskPatternProvider
 	 */
 	public function testInitMatrix(int $maskPattern):void{
-		$this->dataInterface->setData($this->testdata);
+		$this->setTestData();
 
-		$matrix = $this->dataInterface->initMatrix($maskPattern);
+		$matrix = $this->dataInterface->writeMatrix(new MaskPattern($maskPattern));
 
 		$this::assertInstanceOf(QRMatrix::class, $matrix);
-		$this::assertSame($maskPattern, $matrix->maskPattern());
+		$this::assertSame($maskPattern, $matrix->maskPattern()->getPattern());
 	}
 
 	/**
 	 * Tests getting the minimum QR version for the given data
 	 */
 	public function testGetMinimumVersion():void{
-		$this->dataInterface->setData($this->testdata);
+		$this->setTestData();
 
 		$getMinimumVersion = $this->reflection->getMethod('getMinimumVersion');
 		$getMinimumVersion->setAccessible(true);
@@ -109,9 +110,12 @@ abstract class DatainterfaceTestAbstract extends TestCase{
 	public function testGetMinimumVersionException():void{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('data exceeds');
+		[$class, $data] = $this->testdata;
 
-		$this->dataInterface = $this->getDataInterfaceInstance(new QROptions(['version' => QRCode::VERSION_AUTO]));
-		$this->dataInterface->setData(str_repeat($this->testdata, 1337));
+		$this->dataInterface = new QRData(
+			new QROptions(['version' => QRCode::VERSION_AUTO]),
+			[new $class(str_repeat($data, 1337))]
+		);
 	}
 
 	/**
@@ -120,8 +124,10 @@ abstract class DatainterfaceTestAbstract extends TestCase{
 	public function testCodeLengthOverflowException():void{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('code length overflow');
+		[$class, $data] = $this->testdata;
+		$this->testdata = [$class, str_repeat($data, 1337)];
 
-		$this->dataInterface->setData(str_repeat($this->testdata, 1337));
+		$this->setTestData();
 	}
 
 }

+ 8 - 15
tests/Data/KanjiTest.php

@@ -2,9 +2,7 @@
 /**
  * Class KanjiTest
  *
- * @filesource   KanjiTest.php
  * @created      24.11.2017
- * @package      chillerlan\QRCodeTest\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -12,8 +10,7 @@
 
 namespace chillerlan\QRCodeTest\Data;
 
-use chillerlan\QRCode\QROptions;
-use chillerlan\QRCode\Data\{Kanji, QRCodeDataException, QRDataInterface};
+use chillerlan\QRCode\Data\{Kanji, QRCodeDataException};
 
 /**
  * Tests the Kanji class
@@ -21,7 +18,7 @@ use chillerlan\QRCode\Data\{Kanji, QRCodeDataException, QRDataInterface};
 final class KanjiTest extends DatainterfaceTestAbstract{
 
 	/** @internal */
-	protected string $testdata = '茗荷茗荷茗荷茗荷茗荷';
+	protected array $testdata = [Kanji::class, '茗荷茗荷茗荷茗荷茗荷'];
 
 	/** @internal */
 	protected array  $expected = [
@@ -40,14 +37,6 @@ final class KanjiTest extends DatainterfaceTestAbstract{
 		96, 113, 54, 191
 	];
 
-	/**
-	 * @inheritDoc
-	 * @internal
-	 */
-	protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{
-		return new Kanji($options);
-	}
-
 	/**
 	 * Tests if an exception is thrown when an invalid character is encountered
 	 */
@@ -55,7 +44,9 @@ final class KanjiTest extends DatainterfaceTestAbstract{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('illegal char at 1 [16191]');
 
-		$this->dataInterface->setData('ÃÃ');
+		$this->testdata = [Kanji::class, 'ÃÃ'];
+
+		$this->setTestData();
 	}
 
 	/**
@@ -65,7 +56,9 @@ final class KanjiTest extends DatainterfaceTestAbstract{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('illegal char at 1');
 
-		$this->dataInterface->setData('Ã');
+		$this->testdata = [Kanji::class, 'Ã'];
+
+		$this->setTestData();
 	}
 
 }

+ 5 - 14
tests/Data/NumberTest.php

@@ -2,9 +2,7 @@
 /**
  * Class NumberTest
  *
- * @filesource   NumberTest.php
  * @created      24.11.2017
- * @package      chillerlan\QRCodeTest\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -12,8 +10,7 @@
 
 namespace chillerlan\QRCodeTest\Data;
 
-use chillerlan\QRCode\QROptions;
-use chillerlan\QRCode\Data\{Number, QRCodeDataException, QRDataInterface};
+use chillerlan\QRCode\Data\{Number, QRCodeDataException};
 
 /**
  * Tests the Number class
@@ -21,7 +18,7 @@ use chillerlan\QRCode\Data\{Number, QRCodeDataException, QRDataInterface};
 final class NumberTest extends DatainterfaceTestAbstract{
 
 	/** @internal */
-	protected string $testdata  = '0123456789';
+	protected array $testdata  = [Number::class, '0123456789'];
 
 	/** @internal */
 	protected array $expected = [
@@ -40,14 +37,6 @@ final class NumberTest extends DatainterfaceTestAbstract{
 		89, 63, 168, 151
 	];
 
-	/**
-	 * @inheritDoc
-	 * @internal
-	 */
-	protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{
-		return new Number($options);
-	}
-
 	/**
 	 * Tests if an exception is thrown when an invalid character is encountered
 	 */
@@ -55,7 +44,9 @@ final class NumberTest extends DatainterfaceTestAbstract{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('illegal char: "#" [35]');
 
-		$this->dataInterface->setData('#');
+		$this->testdata = [Number::class, '#'];
+
+		$this->setTestData();
 	}
 
 }

+ 60 - 57
tests/Data/QRMatrixTest.php

@@ -2,9 +2,7 @@
 /**
  * Class QRMatrixTest
  *
- * @filesource   QRMatrixTest.php
  * @created      17.11.2017
- * @package      chillerlan\QRCodeTest\Data
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -12,11 +10,10 @@
 
 namespace chillerlan\QRCodeTest\Data;
 
-use chillerlan\QRCode\QRCode;
-use chillerlan\QRCode\QROptions;
+use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version};
+use chillerlan\QRCode\{QRCode, QROptions};
 use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix};
 use PHPUnit\Framework\TestCase;
-use ReflectionClass;
 
 /**
  * Tests the QRMatix class
@@ -43,7 +40,7 @@ final class QRMatrixTest extends TestCase{
 	 * @internal
 	 */
 	protected function getMatrix(int $version):QRMatrix{
-		return  new QRMatrix($version, QRCode::ECC_L);
+		return  new QRMatrix(new Version($version), new EccLevel(EccLevel::L));
 	}
 
 	/**
@@ -53,26 +50,6 @@ final class QRMatrixTest extends TestCase{
 		$this::assertInstanceOf(QRMatrix::class, $this->matrix);
 	}
 
-	/**
-	 * Tests if an exception is thrown when an invalid QR version was given
-	 */
-	public function testInvalidVersionException():void{
-		$this->expectException(QRCodeDataException::class);
-		$this->expectExceptionMessage('invalid QR Code version');
-
-		$this->matrix = new QRMatrix(42, 0);
-	}
-
-	/**
-	 * Tests if an exception is thrown when an invalid ECC level was given
-	 */
-	public function testInvalidEccException():void{
-		$this->expectException(QRCodeDataException::class);
-		$this->expectExceptionMessage('invalid ecc level');
-
-		$this->matrix = new QRMatrix(1, 42);
-	}
-
 	/**
 	 * Tests if size() returns the actual matrix size/count
 	 */
@@ -84,23 +61,26 @@ final class QRMatrixTest extends TestCase{
 	 * Tests if version() returns the current (given) version
 	 */
 	public function testVersion():void{
-		$this::assertSame($this::version, $this->matrix->version());
+		$this::assertSame($this::version, $this->matrix->version()->getVersionNumber());
 	}
 
 	/**
 	 * Tests if eccLevel() returns the current (given) ECC level
 	 */
 	public function testECC():void{
-		$this::assertSame(QRCode::ECC_L, $this->matrix->eccLevel());
+		$this::assertSame(EccLevel::MODES[EccLevel::L], $this->matrix->eccLevel()->getOrdinal());
 	}
 
 	/**
 	 * Tests if maskPattern() returns the current (or default) mask pattern
 	 */
 	public function testMaskPattern():void{
-		$this::assertSame(-1, $this->matrix->maskPattern()); // default
+		$this::assertSame(null, $this->matrix->maskPattern());
+
+		$matrix = (new QRCode)->addByteSegment('testdata')->getMatrix();
 
-		// @todo: actual mask pattern after mapData()
+		$this::assertInstanceOf(MaskPattern::class, $matrix->maskPattern());
+		$this::assertSame(MaskPattern::PATTERN_010, $matrix->maskPattern()->getPattern());
 	}
 
 	/**
@@ -108,11 +88,11 @@ final class QRMatrixTest extends TestCase{
 	 */
 	public function testGetSetCheck():void{
 		$this->matrix->set(10, 10, true, QRMatrix::M_TEST);
-		$this::assertSame(65280, $this->matrix->get(10, 10));
+		$this::assertSame(QRMatrix::M_TEST | QRMatrix::IS_DARK, $this->matrix->get(10, 10));
 		$this::assertTrue($this->matrix->check(10, 10));
 
 		$this->matrix->set(20, 20, false, QRMatrix::M_TEST);
-		$this::assertSame(255, $this->matrix->get(20, 20));
+		$this::assertSame(QRMatrix::M_TEST, $this->matrix->get(20, 20));
 		$this::assertFalse($this->matrix->check(20, 20));
 	}
 
@@ -140,7 +120,7 @@ final class QRMatrixTest extends TestCase{
 	public function testSetDarkModule(int $version):void{
 		$matrix = $this->getMatrix($version)->setDarkModule();
 
-		$this::assertSame(QRMatrix::M_DARKMODULE << 8, $matrix->get(8, $matrix->size() - 8));
+		$this::assertSame(QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK, $matrix->get(8, $matrix->size() - 8));
 	}
 
 	/**
@@ -151,9 +131,9 @@ final class QRMatrixTest extends TestCase{
 	public function testSetFinderPattern(int $version):void{
 		$matrix = $this->getMatrix($version)->setFinderPattern();
 
-		$this::assertSame(QRMatrix::M_FINDER << 8, $matrix->get(0, 0));
-		$this::assertSame(QRMatrix::M_FINDER << 8, $matrix->get(0, $matrix->size() - 1));
-		$this::assertSame(QRMatrix::M_FINDER << 8, $matrix->get($matrix->size() - 1, 0));
+		$this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $matrix->get(0, 0));
+		$this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $matrix->get(0, $matrix->size() - 1));
+		$this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $matrix->get($matrix->size() - 1, 0));
 	}
 
 	/**
@@ -190,17 +170,17 @@ final class QRMatrixTest extends TestCase{
 			->setAlignmentPattern()
 		;
 
-		$alignmentPattern = (new ReflectionClass(QRMatrix::class))->getConstant('alignmentPattern')[$version];
+		$alignmentPattern = (new Version($version))->getAlignmentPattern();
 
 		foreach($alignmentPattern as $py){
 			foreach($alignmentPattern as $px){
 
-				if($matrix->get($px, $py) === QRMatrix::M_FINDER << 8){
-					$this::assertSame(QRMatrix::M_FINDER << 8, $matrix->get($px, $py), 'skipped finder pattern');
+				if($matrix->get($px, $py) === (QRMatrix::M_FINDER | QRMatrix::IS_DARK)){
+					$this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $matrix->get($px, $py), 'skipped finder pattern');
 					continue;
 				}
 
-				$this::assertSame(QRMatrix::M_ALIGNMENT << 8, $matrix->get($px, $py));
+				$this::assertSame(QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK, $matrix->get($px, $py));
 			}
 		}
 
@@ -225,13 +205,13 @@ final class QRMatrixTest extends TestCase{
 			if($i % 2 === 0){
 				$p1 = $matrix->get(6, $i);
 
-				if($p1 === QRMatrix::M_ALIGNMENT << 8){
-					$this::assertSame(QRMatrix::M_ALIGNMENT << 8, $p1, 'skipped alignment pattern');
+				if($p1 === (QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK)){
+					$this::assertSame(QRMatrix::M_ALIGNMENT | QRMatrix::IS_DARK, $p1, 'skipped alignment pattern');
 					continue;
 				}
 
-				$this::assertSame(QRMatrix::M_TIMING << 8, $p1);
-				$this::assertSame(QRMatrix::M_TIMING << 8, $matrix->get($i, 6));
+				$this::assertSame(QRMatrix::M_TIMING | QRMatrix::IS_DARK, $p1);
+				$this::assertSame(QRMatrix::M_TIMING | QRMatrix::IS_DARK, $matrix->get($i, 6));
 			}
 		}
 	}
@@ -264,7 +244,7 @@ final class QRMatrixTest extends TestCase{
 	 * @dataProvider versionProvider
 	 */
 	public function testSetFormatInfo(int $version):void{
-		$matrix = $this->getMatrix($version)->setFormatInfo(0, true);
+		$matrix = $this->getMatrix($version)->setFormatInfo(new MaskPattern(MaskPattern::PATTERN_000), true);
 
 		$this::assertSame(QRMatrix::M_FORMAT, $matrix->get(8, 0));
 		$this::assertSame(QRMatrix::M_FORMAT, $matrix->get(0, 8));
@@ -295,8 +275,8 @@ final class QRMatrixTest extends TestCase{
 		$this::assertSame(QRMatrix::M_QUIETZONE, $matrix->get(0, 0));
 		$this::assertSame(QRMatrix::M_QUIETZONE, $matrix->get($size - 1, $size - 1));
 
-		$this::assertSame(QRMatrix::M_TEST << 8, $matrix->get($q, $q));
-		$this::assertSame(QRMatrix::M_TEST << 8, $matrix->get($size - 1 - $q, $size - 1 - $q));
+		$this::assertSame(QRMatrix::M_TEST | QRMatrix::IS_DARK, $matrix->get($q, $q));
+		$this::assertSame(QRMatrix::M_TEST | QRMatrix::IS_DARK, $matrix->get($size - 1 - $q, $size - 1 - $q));
 	}
 
 	/**
@@ -312,10 +292,10 @@ final class QRMatrixTest extends TestCase{
 	public function testSetLogoSpaceOrientation():void{
 		$o = new QROptions;
 		$o->version      = 10;
-		$o->eccLevel     = QRCode::ECC_H;
+		$o->eccLevel     = EccLevel::H;
 		$o->addQuietzone = false;
 
-		$matrix = (new QRCode($o))->getMatrix('testdata');
+		$matrix = (new QRCode($o))->addByteSegment('testdata')->getMatrix();
 		// also testing size adjustment to uneven numbers
 		$matrix->setLogoSpace(20, 14);
 
@@ -331,19 +311,19 @@ final class QRMatrixTest extends TestCase{
 	public function testSetLogoSpacePosition():void{
 		$o = new QROptions;
 		$o->version       = 10;
-		$o->eccLevel      = QRCode::ECC_H;
+		$o->eccLevel      = EccLevel::H;
 		$o->addQuietzone  = true;
 		$o->quietzoneSize = 10;
 
-		$m = (new QRCode($o))->getMatrix('testdata');
+		$m = (new QRCode($o))->addByteSegment('testdata')->getMatrix();
 
 		// logo space should not overwrite quiet zone & function patterns
 		$m->setLogoSpace(21, 21, -10, -10);
 		$this::assertSame(QRMatrix::M_QUIETZONE, $m->get(9, 9));
-		$this::assertSame(QRMatrix::M_FINDER << 8, $m->get(10, 10));
-		$this::assertSame(QRMatrix::M_FINDER << 8, $m->get(16, 16));
+		$this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $m->get(10, 10));
+		$this::assertSame(QRMatrix::M_FINDER | QRMatrix::IS_DARK, $m->get(16, 16));
 		$this::assertSame(QRMatrix::M_SEPARATOR, $m->get(17, 17));
-		$this::assertSame(QRMatrix::M_FORMAT << 8, $m->get(18, 18));
+		$this::assertSame(QRMatrix::M_FORMAT | QRMatrix::IS_DARK, $m->get(18, 18));
 		$this::assertSame(QRMatrix::M_LOGO, $m->get(19, 19));
 		$this::assertSame(QRMatrix::M_LOGO, $m->get(20, 20));
 		$this::assertNotSame(QRMatrix::M_LOGO, $m->get(21, 21));
@@ -360,7 +340,7 @@ final class QRMatrixTest extends TestCase{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('ECC level "H" required to add logo space');
 
-		(new QRCode)->getMatrix('testdata')->setLogoSpace(50, 50);
+		(new QRCode)->addByteSegment('testdata')->getMatrix()->setLogoSpace(50, 50);
 	}
 
 	public function testSetLogoSpaceMaxSizeException():void{
@@ -369,9 +349,32 @@ final class QRMatrixTest extends TestCase{
 
 		$o = new QROptions;
 		$o->version  = 5;
-		$o->eccLevel = QRCode::ECC_H;
+		$o->eccLevel = EccLevel::H;
 
-		(new QRCode($o))->getMatrix('testdata')->setLogoSpace(50, 50);
+		(new QRCode($o))->addByteSegment('testdata')->getMatrix()->setLogoSpace(50, 50);
+	}
+
+	/**
+	 * Tests flipping the value of a module
+	 */
+	public function testFlip():void{
+		// using the dark module here because i'm lazy
+		$matrix = $this->getMatrix(10)->setDarkModule();
+		$x = 8;
+		$y = $matrix->size() - 8;
+
+		// cover checkType()
+		$this::assertTrue($matrix->checkType($x, $y, QRMatrix::M_DARKMODULE));
+		// verify the current state (dark)
+		$this::assertSame(QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK, $matrix->get($x, $y));
+		// flip
+		$matrix->flip($x, $y);
+		// verify flip
+		$this::assertSame(QRMatrix::M_DARKMODULE, $matrix->get($x, $y));
+		// flip again
+		$matrix->flip($x, $y);
+		// verify flip
+		$this::assertSame(QRMatrix::M_DARKMODULE | QRMatrix::IS_DARK, $matrix->get($x, $y));
 	}
 
 }

+ 0 - 42
tests/Helpers/PolynomialTest.php

@@ -1,42 +0,0 @@
-<?php
-/**
- * Class PolynomialTest
- *
- * @filesource   PolynomialTest.php
- * @created      09.02.2016
- * @package      chillerlan\QRCodeTest\Helpers
- * @author       Smiley <smiley@chillerlan.net>
- * @copyright    2015 Smiley
- * @license      MIT
- */
-
-namespace chillerlan\QRCodeTest\Helpers;
-
-use chillerlan\QRCode\Helpers\Polynomial;
-use chillerlan\QRCode\QRCodeException;
-use PHPUnit\Framework\TestCase;
-
-/**
- * Polynomial coverage test
- */
-final class PolynomialTest extends TestCase{
-
-	protected Polynomial $polynomial;
-
-	protected function setUp():void{
-		$this->polynomial = new Polynomial;
-	}
-
-	public function testGexp():void{
-		$this::assertSame(142, $this->polynomial->gexp(-1));
-		$this::assertSame(133, $this->polynomial->gexp(128));
-		$this::assertSame(2,   $this->polynomial->gexp(256));
-	}
-
-	public function testGlogException():void{
-		$this->expectException(QRCodeException::class);
-		$this->expectExceptionMessage('log(0)');
-
-		$this->polynomial->glog(0);
-	}
-}

+ 0 - 2
tests/Output/QRFpdfTest.php

@@ -2,9 +2,7 @@
 /**
  * Class QRFpdfTest
  *
- * @filesource   QRFpdfTest.php
  * @created      03.06.2020
- * @package      chillerlan\QRCodeTest\Output
  * @author       smiley <smiley@chillerlan.net>
  * @copyright    2020 smiley
  * @license      MIT

+ 2 - 4
tests/Output/QRImageTest.php

@@ -2,9 +2,7 @@
 /**
  * Class QRImageTest
  *
- * @filesource   QRImageTest.php
  * @created      24.12.2017
- * @package      chillerlan\QRCodeTest\Output
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -72,7 +70,7 @@ class QRImageTest extends QROutputTestAbstract{
 	}
 
 	/**
-	 * @phan-suppress PhanUndeclaredClassReference
+	 *
 	 */
 	public function testOutputGetResource():void{
 		$this->options->returnResource = true;
@@ -80,7 +78,7 @@ class QRImageTest extends QROutputTestAbstract{
 
 		$actual = $this->outputInterface->dump();
 
-		/** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
+		/** @noinspection PhpFullyQualifiedNameUsageInspection */
 		\PHP_MAJOR_VERSION >= 8
 			? $this::assertInstanceOf(\GdImage::class, $actual)
 			: $this::assertIsResource($actual);

+ 0 - 2
tests/Output/QRImagickTest.php

@@ -2,9 +2,7 @@
 /**
  * Class QRImagickTest
  *
- * @filesource   QRImagickTest.php
  * @created      04.07.2018
- * @package      chillerlan\QRCodeTest\Output
  * @author       smiley <smiley@chillerlan.net>
  * @copyright    2018 smiley
  * @license      MIT

+ 3 - 5
tests/Output/QRMarkupTest.php

@@ -2,9 +2,7 @@
 /**
  * Class QRMarkupTest
  *
- * @filesource   QRMarkupTest.php
  * @created      24.12.2017
- * @package      chillerlan\QRCodeTest\Output
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -12,7 +10,7 @@
 
 namespace chillerlan\QRCodeTest\Output;
 
-use chillerlan\QRCode\{QRCode, QROptions};
+use chillerlan\QRCode\{Data\QRMatrix, QRCode, QROptions};
 use chillerlan\QRCode\Output\{QROutputInterface, QRMarkup};
 
 /**
@@ -46,8 +44,8 @@ class QRMarkupTest extends QROutputTestAbstract{
 		$this->options->imageBase64  = false;
 		$this->options->moduleValues = [
 			// data
-			1024 => '#4A6000',
-			4    => '#ECF9BE',
+			QRMatrix::M_DATA | QRMatrix::IS_DARK => '#4A6000',
+			QRMatrix::M_DATA                     => '#ECF9BE',
 		];
 
 		$this->outputInterface = $this->getOutputInterface($this->options);

+ 4 - 4
tests/Output/QROutputTestAbstract.php

@@ -2,9 +2,7 @@
 /**
  * Class QROutputTestAbstract
  *
- * @filesource   QROutputTestAbstract.php
  * @created      24.12.2017
- * @package      chillerlan\QRCodeTest\Output
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -13,7 +11,8 @@
 namespace chillerlan\QRCodeTest\Output;
 
 use chillerlan\QRCode\{QRCode, QROptions};
-use chillerlan\QRCode\Data\{Byte, QRMatrix};
+use chillerlan\QRCode\Common\MaskPattern;
+use chillerlan\QRCode\Data\{Byte, QRData, QRMatrix};
 use chillerlan\QRCode\Output\{QRCodeOutputException, QROutputInterface};
 use PHPUnit\Framework\TestCase;
 
@@ -48,7 +47,8 @@ abstract class QROutputTestAbstract extends TestCase{
 		}
 
 		$this->options         = new QROptions;
-		$this->matrix          = (new Byte($this->options, 'testdata'))->initMatrix(0);
+		$this->matrix          = (new QRData($this->options, [new Byte('testdata')]))
+			->writeMatrix(new MaskPattern(MaskPattern::PATTERN_010));
 		$this->outputInterface = $this->getOutputInterface($this->options);
 	}
 

+ 6 - 6
tests/Output/QRStringTest.php

@@ -2,9 +2,7 @@
 /**
  * Class QRStringTest
  *
- * @filesource   QRStringTest.php
  * @created      24.12.2017
- * @package      chillerlan\QRCodeTest\Output
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -12,9 +10,11 @@
 
 namespace chillerlan\QRCodeTest\Output;
 
-use chillerlan\QRCodeExamples\MyCustomOutput;
 use chillerlan\QRCode\{QRCode, QROptions};
+use chillerlan\QRCode\Common\EccLevel;
+use chillerlan\QRCode\Data\QRMatrix;
 use chillerlan\QRCode\Output\{QROutputInterface, QRString};
+use chillerlan\QRCodeExamples\MyCustomOutput;
 
 /**
  * Tests the QRString output module
@@ -47,8 +47,8 @@ class QRStringTest extends QROutputTestAbstract{
 
 		$this->options->moduleValues = [
 			// data
-			1024 => 'A',
-			4    => 'B',
+			QRMatrix::M_DATA | QRMatrix::IS_DARK => 'A',
+			QRMatrix::M_DATA                     => 'B',
 		];
 
 		$this->outputInterface = $this->getOutputInterface($this->options);
@@ -63,7 +63,7 @@ class QRStringTest extends QROutputTestAbstract{
 	 */
 	public function testCustomOutput():void{
 		$this->options->version         = 5;
-		$this->options->eccLevel        = QRCode::ECC_L;
+		$this->options->eccLevel        = EccLevel::L;
 		$this->options->outputType      = QRCode::OUTPUT_CUSTOM;
 		$this->options->outputInterface = MyCustomOutput::class;
 

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
tests/Output/samples/json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
tests/Output/samples/svg


+ 123 - 0
tests/QRCodeReaderTest.php

@@ -0,0 +1,123 @@
+<?php
+/**
+ * Class QRCodeReaderTest
+ *
+ * @created      17.01.2021
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2021 Smiley
+ * @license      MIT
+ *
+ * @noinspection PhpComposerExtensionStubsInspection
+ */
+
+namespace chillerlan\QRCodeTest;
+
+use Exception;
+use chillerlan\QRCode\Common\{EccLevel, Mode, Version};
+use chillerlan\QRCode\{QRCode, QROptions, QRCodeReader};
+use PHPUnit\Framework\TestCase;
+use function extension_loaded, range, str_repeat, substr;
+
+/**
+ * Tests the QR Code reader
+ */
+class QRCodeReaderTest extends TestCase{
+
+	// https://www.bobrosslipsum.com/
+	protected const loremipsum = 'Just let this happen. We just let this flow right out of our minds. '
+		.'Anyone can paint. We touch the canvas, the canvas takes what it wants. From all of us here, '
+		.'I want to wish you happy painting and God bless, my friends. A tree cannot be straight if it has a crooked trunk. '
+		.'You have to make almighty decisions when you\'re the creator. I guess that would be considered a UFO. '
+		.'A big cotton ball in the sky. I\'m gonna add just a tiny little amount of Prussian Blue. '
+		.'They say everything looks better with odd numbers of things. But sometimes I put even numbers—just '
+		.'to upset the critics. We\'ll lay all these little funky little things in there. ';
+
+	public function qrCodeProvider():array{
+		return [
+			'helloworld' => ['hello_world.png', 'Hello world!'],
+			// covers mirroring
+			'mirrored'   => ['hello_world_mirrored.png', 'Hello world!'],
+			// data modes
+			'byte'       => ['byte.png', 'https://smiley.codes/qrcode/'],
+			'numeric'    => ['numeric.png', '123456789012345678901234567890'],
+			'alphanum'   => ['alphanum.png', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:'],
+			'kanji'      => ['kanji.png', '茗荷茗荷茗荷茗荷'],
+			// covers most of ReedSolomonDecoder
+			'damaged'    => ['damaged.png', 'https://smiley.codes/qrcode/'],
+			// covers Binarizer::getHistogramBlackMatrix()
+			'smol'       => ['smol.png', 'https://smiley.codes/qrcode/'],
+			'tilted'     => ['tilted.png', 'Hello world!'], // tilted 22° CCW
+			'rotated'    => ['rotated.png', 'Hello world!'], // rotated 90° CW
+		];
+	}
+
+	/**
+	 * @dataProvider qrCodeProvider
+	 */
+	public function testReaderGD(string $img, string $expected):void{
+		$reader = new QRCodeReader(false);
+
+		$this::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img));
+	}
+
+	/**
+	 * @dataProvider qrCodeProvider
+	 */
+	public function testReaderImagick(string $img, string $expected):void{
+
+		if(!extension_loaded('imagick')){
+			$this::markTestSkipped('imagick not installed');
+		}
+
+		$reader = new QRCodeReader(true);
+
+		$this::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img));
+	}
+
+	public function dataTestProvider():array{
+		$data = [];
+		$str  = str_repeat($this::loremipsum, 5);
+
+		foreach(range(1, 40) as $v){
+			$version = new Version($v);
+
+			foreach(EccLevel::MODES as $ecc => $_){
+				$eccLevel = new EccLevel($ecc);
+
+				$data['version: '.$version.$eccLevel] = [
+					$version,
+					$eccLevel,
+					/** @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal */
+					substr($str, 0, $version->getMaxLengthForMode(Mode::DATA_BYTE, $eccLevel))
+				];
+			}
+		}
+
+		return $data;
+	}
+
+	/**
+	 * @dataProvider dataTestProvider
+	 */
+	public function testReadData(Version $version, EccLevel $ecc, string $expected):void{
+		$options = new QROptions;
+#		$options->imageTransparent = false;
+		$options->eccLevel         = $ecc->getLevel();
+		$options->version          = $version->getVersionNumber();
+		$options->imageBase64      = false;
+		$options->scale            = 1; // what's interesting is that a smaller scale seems to produce fewer reader errors???
+
+		try{
+			$imagedata = (new QRCode($options))->render($expected);
+			$result    = (new QRCodeReader(true))->readBlob($imagedata);
+		}
+		catch(Exception $e){
+			$this::markTestSkipped($version.$ecc.': '.$e->getMessage());
+		}
+
+		$this::assertSame($expected, $result->getText());
+		$this::assertSame($version->getVersionNumber(), $result->getVersion()->getVersionNumber());
+		$this::assertSame($ecc->getLevel(), $result->getEccLevel()->getLevel());
+	}
+
+}

+ 2 - 54
tests/QRCodeTest.php

@@ -2,9 +2,7 @@
 /**
  * Class QRCodeTest
  *
- * @filesource   QRCodeTest.php
  * @created      17.11.2017
- * @package      chillerlan\QRCodeTest
  * @author       Smiley <smiley@chillerlan.net>
  * @copyright    2017 Smiley
  * @license      MIT
@@ -13,12 +11,10 @@
 namespace chillerlan\QRCodeTest;
 
 use chillerlan\QRCode\{QROptions, QRCode};
-use chillerlan\QRCode\Data\{AlphaNum, Byte, Kanji, Number, QRCodeDataException};
+use chillerlan\QRCode\Data\QRCodeDataException;
 use chillerlan\QRCode\Output\QRCodeOutputException;
 use PHPUnit\Framework\TestCase;
 
-use function random_bytes;
-
 /**
  * Tests basic functions of the QRCode class
  */
@@ -97,55 +93,7 @@ class QRCodeTest extends TestCase{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('QRCode::getMatrix() No data given.');
 
-		$this->qrcode->getMatrix('');
-	}
-
-	/**
-	 * test whether stings are trimmed (they are not) - i'm still torn on that (see isByte)
-	 */
-	public function testAvoidTrimming():void{
-		$m1 = $this->qrcode->getMatrix('hello')->matrix();
-		$m2 = $this->qrcode->getMatrix('hello ')->matrix(); // added space
-
-		$this::assertNotSame($m1, $m2);
-	}
-
-	/**
-	 * tests if the data mode is overriden if QROptions::$dataModeOverride is set to a valid value
-	 *
-	 * @see https://github.com/chillerlan/php-qrcode/issues/39
-	 */
-	public function testDataModeOverride():void{
-
-		// no (or invalid) value set - auto detection
-		$this->options->dataModeOverride = 'foo';
-		$this->qrcode = new QRCode;
-
-		$this::assertInstanceOf(Number::class, $this->qrcode->initDataInterface('123'));
-		$this::assertInstanceOf(AlphaNum::class, $this->qrcode->initDataInterface('ABC123'));
-		$this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface(random_bytes(32)));
-		$this::assertInstanceOf(Kanji::class, $this->qrcode->initDataInterface('茗荷'));
-
-		// data mode set: force the given data mode
-		$this->options->dataModeOverride = 'Byte';
-		$this->qrcode = new QRCode($this->options);
-
-		$this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface('123'));
-		$this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface('ABC123'));
-		$this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface(random_bytes(32)));
-		$this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface('茗荷'));
-	}
-
-	/**
-	 * tests if an exception is thrown when an invalid character occurs when forcing a data mode other than Byte
-	 */
-	public function testDataModeOverrideError():void{
-		$this->expectException(QRCodeDataException::class);
-		$this->expectExceptionMessage('illegal char:');
-
-		$this->options->dataModeOverride = 'AlphaNum';
-
-		(new QRCode($this->options))->initDataInterface(random_bytes(32));
+		$this->qrcode->getMatrix();
 	}
 
 }

+ 0 - 2
tests/QROptionsTest.php

@@ -2,9 +2,7 @@
 /**
  * Class QROptionsTest
  *
- * @filesource   QROptionsTest.php
  * @created      08.11.2018
- * @package      chillerlan\QRCodeTest
  * @author       smiley <smiley@chillerlan.net>
  * @copyright    2018 smiley
  * @license      MIT

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác