From 6ca745c3565938d6113625401b47e394ffed06a7 Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Tue, 11 Nov 2025 06:13:47 +0100 Subject: [PATCH 1/4] Fix Kanji mode QR code generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes unreadable QR codes in Kanji mode with Shift-JIS encoding (see #172). PR #173 worked around the issue by forcing Byte mode, but that lost Kanji mode’s efficiency. The actual cause was using strlen() to count characters. Replacing it with iconv_strlen($content, 'utf-8') ensures correct character count and restores proper Kanji encoding. --- src/Encoder/Encoder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Encoder/Encoder.php b/src/Encoder/Encoder.php index c363953..f5adab8 100644 --- a/src/Encoder/Encoder.php +++ b/src/Encoder/Encoder.php @@ -115,7 +115,7 @@ public static function encode( $headerAndDataBits->appendBitArray($headerBits); // Find "length" of main segment and write it. - $numLetters = (Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : strlen($content)); + $numLetters = (Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : iconv_strlen($content, 'utf-8')); self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits); // Put data together into the overall payload. From a7e2d7bd747996d66ce037b0263601d1b93a2a07 Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Tue, 11 Nov 2025 13:54:04 +0100 Subject: [PATCH 2/4] Test: Cover exclusive vs. mixed Shift-JIS data for Kanji mode --- test/Encoder/EncoderTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/Encoder/EncoderTest.php b/test/Encoder/EncoderTest.php index 66459e9..26d1ab1 100644 --- a/test/Encoder/EncoderTest.php +++ b/test/Encoder/EncoderTest.php @@ -87,6 +87,12 @@ public function testChooseMode() : void // Sou-Utso-Byou in Kanji in SHIFT-JIS $this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, "\xe\x4\x9\x5\x9\x61")); + + // SHIFT-JIS encoding, content only consists of double-byte kanji characters + $this->assertSame(Mode::KANJI(), $this->methods['chooseMode']->invoke(null, 'あいうえお', 'SHIFT-JIS')); + + // SHIFT-JIS encoding, but content doesn't exclusively consist of kanji characters + $this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, 'あいうえお123', 'SHIFT-JIS')); } public function testEncode() : void From 97111fc039ff964c33c85cce37bb7cd7461a7548 Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:04:26 +0100 Subject: [PATCH 3/4] Use strlen() for numeric and alphanumeric modes to avoid iconv overhead For NUMERIC and ALPHANUMERIC modes, strlen() is sufficient since these modes only operate on single-byte characters. strlen() runs in O(1) time and avoids the overhead of iconv_strlen(). --- src/Encoder/Encoder.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Encoder/Encoder.php b/src/Encoder/Encoder.php index f5adab8..1707be7 100644 --- a/src/Encoder/Encoder.php +++ b/src/Encoder/Encoder.php @@ -115,7 +115,11 @@ public static function encode( $headerAndDataBits->appendBitArray($headerBits); // Find "length" of main segment and write it. - $numLetters = (Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : iconv_strlen($content, 'utf-8')); + $numLetters = match ($mode) { + Mode::BYTE() => $dataBits->getSizeInBytes(), + Mode::NUMERIC(), Mode::ALPHANUMERIC() => strlen($content), + Mode::KANJI() => iconv_strlen($content, 'utf-8'), + }; self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits); // Put data together into the overall payload. From 394a4fbb32109ad6eb67d602ce39d5e9d0697ea4 Mon Sep 17 00:00:00 2001 From: vlakoff <544424+vlakoff@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:11:51 +0100 Subject: [PATCH 4/4] Rewrite appendBytes() using a PHP match Nicer, and more consistent with some other code in the encode() method. Invalid modes are really not expected here, as this private method is called only by encode(), and the mode is determined right at the beginning of encode() by calling chooseMode(). Though, if an invalid mode ever comes here, the match would throw an UnhandledMatchError. --- src/Encoder/Encoder.php | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/Encoder/Encoder.php b/src/Encoder/Encoder.php index 1707be7..2e2ac79 100644 --- a/src/Encoder/Encoder.php +++ b/src/Encoder/Encoder.php @@ -518,31 +518,15 @@ private static function appendLengthInfo(int $numLetters, Version $version, Mode /** * Appends bytes to a bit array in a specific mode. - * - * @throws WriterException if an invalid mode was supplied */ private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void { - switch ($mode) { - case Mode::NUMERIC(): - self::appendNumericBytes($content, $bits); - break; - - case Mode::ALPHANUMERIC(): - self::appendAlphanumericBytes($content, $bits); - break; - - case Mode::BYTE(): - self::append8BitBytes($content, $bits, $encoding); - break; - - case Mode::KANJI(): - self::appendKanjiBytes($content, $bits); - break; - - default: - throw new WriterException('Invalid mode: ' . $mode); - } + match ($mode) { + Mode::NUMERIC() => self::appendNumericBytes($content, $bits), + Mode::ALPHANUMERIC() => self::appendAlphanumericBytes($content, $bits), + Mode::BYTE() => self::append8BitBytes($content, $bits, $encoding), + Mode::KANJI() => self::appendKanjiBytes($content, $bits), + }; } /**