-
-
Notifications
You must be signed in to change notification settings - Fork 328
Add QRPbm output format for Portable BitMap outputs #323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 14 commits
0a069c3
e70a744
f5a2d2b
aa748d2
9b94cfe
aff03cc
78ae3a3
03802e8
ddef97d
e5f60fd
a520c0f
b2f1286
a9eda61
ce85a50
5182a50
2bb89f8
e6f3319
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| <?php | ||
| /** | ||
| * Class QRNetbmAbstract | ||
| * | ||
| * @created 19.12.2025 | ||
| * @author wgevaert & codemasher | ||
| * @copyright 2025 wgevaert & codemasher | ||
| * @license MIT | ||
| */ | ||
| declare(strict_types=1); | ||
|
|
||
| namespace chillerlan\QRCode\Output; | ||
|
|
||
| use chillerlan\QRCode\Output\QROutputAbstract; | ||
| use UnexpectedValueException; | ||
| use function sprintf; | ||
|
|
||
| abstract class QRNetpbmAbstract extends QROutputAbstract{ | ||
|
|
||
| protected const HEADER_ASCII = ''; | ||
| protected const HEADER_BINARY = ''; | ||
|
|
||
| protected function getMagicNumber():string{ | ||
| return $this->options->netpbmPlain ? static::HEADER_ASCII : static::HEADER_BINARY; | ||
| } | ||
|
|
||
| protected function getHeader():string{ | ||
| $comment = 'created by https://github.com/chillerlan/php-qrcode'; | ||
|
|
||
| return sprintf( | ||
| "%s\n# %s\n%s %s\n%s", | ||
| $this->getMagicNumber(), | ||
| $comment, | ||
| $this->length, | ||
| $this->length, | ||
| $this->getMaxValueHeaderString(), | ||
| ); | ||
| } | ||
|
|
||
| protected function getMaxValueHeaderString():string { | ||
| if ( $this->options->netpbmMaxValue >= 65536 || $this->options->netpbmMaxValue <= 0 ) { | ||
| throw new UnexpectedValueException( 'NetpbmMaxValue should be between 0 and 65536' ); | ||
| } | ||
wgevaert marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return $this->options->netpbmMaxValue."\n"; | ||
| } | ||
|
|
||
| abstract protected function getBodyASCII():string; | ||
| abstract protected function getBodyBinary():string; | ||
|
|
||
| public function dump(string|null $file = null):string{ | ||
| $qrString = $this->getHeader(); | ||
|
|
||
| $qrString .= $this->options->netpbmPlain | ||
| ? $this->getBodyASCII() | ||
| : $this->getBodyBinary(); | ||
|
|
||
| $this->saveToFile($qrString, $file); | ||
|
|
||
| if($this->options->outputBase64){ | ||
| $qrString = $this->toBase64DataURI($qrString); | ||
| } | ||
|
|
||
| return $qrString; | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| <?php | ||
| /** | ||
| * Class QRNetbmBitmap | ||
| * | ||
| * @created 19.12.2025 | ||
| * @author wgevaert & codemasher | ||
| * @copyright 2025 wgevaert & codemasher | ||
| * @license MIT | ||
| */ | ||
| declare(strict_types=1); | ||
|
|
||
| namespace chillerlan\QRCode\Output; | ||
|
|
||
| use UnexpectedValueException; | ||
| use function is_bool; | ||
| use function str_split; | ||
| use function pack; | ||
| use function str_repeat; | ||
|
|
||
| class QRNetpbmBitmap extends QRNetpbmAbstract{ | ||
|
|
||
| public const MIME_TYPE = 'image/x-portable-bitmap'; | ||
|
|
||
| protected const HEADER_ASCII = 'P1'; | ||
| protected const HEADER_BINARY = 'P4'; | ||
|
|
||
| protected function prepareModuleValue(mixed $value):mixed{ | ||
| if ( !is_bool( $value ) ) { | ||
| throw new UnexpectedValueException( 'Bitmap expected bool modules' ); | ||
| } | ||
| return $value; | ||
| } | ||
|
|
||
| protected function getDefaultModuleValue(bool $isDark):mixed{ | ||
| return $isDark; | ||
| } | ||
|
|
||
| public static function moduleValueIsValid(mixed $value):bool{ | ||
| return is_bool($value); | ||
| } | ||
|
|
||
| protected function setModuleValues():void{ | ||
| // noop | ||
| } | ||
|
|
||
| protected function getMaxValueHeaderString(): string { | ||
| return ''; | ||
| } | ||
|
|
||
| protected function getBodyASCII():string{ | ||
| $body = ''; | ||
|
|
||
| foreach($this->matrix->getBooleanMatrix() as $row){ | ||
| $line = ''; | ||
|
|
||
| foreach($row as $isDark){ | ||
| $line .= str_repeat($isDark ? '1' : '0', $this->scale); | ||
| } | ||
| // Lines should not be longer than 70 chars | ||
| $line = implode("\n", str_split($line,70))."\n"; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why don't you just use the example I provided? This here would split a single line to multiples of 70 and leave multiple shorter lines then, which is not intended.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer if using plain format, when you open the image with a text reader like less or notepad, the image reads as if reading on a 70-tokens-wide terminal. Such that if your image is less than 70 tokens wide, you can kind-of see it when reading it, and if it is less than 140 tokens wide, you can still make it out a bit.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's still 2 unnecessary function calls within a loop that can be avoided, while at the same time producing a cleaner output similar to GIMP:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see why GIMP's output is considered "cleaner" than this: The image data above looks like a QR code if you look from far away and squint your eyes a bit. The GIMP image is difficult to see what it is supposed to be.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Try your code with a QR version >= 14 and see. |
||
|
|
||
| $body .= str_repeat($line, $this->scale); | ||
| } | ||
|
|
||
| return $body; | ||
| } | ||
|
|
||
| protected function getBodyBinary():string{ | ||
| $body = ''; | ||
|
|
||
| foreach($this->matrix->getBooleanMatrix() as $row){ | ||
| $rowdata = array_fill(0, (int)ceil($this->length / 8), 0); | ||
| $byte = 0; | ||
| $bit = 0b10000000; | ||
|
|
||
| foreach($row as $isDark){ | ||
| for($i = 0; $i < $this->scale; $i++){ | ||
| if($bit <= 0){ | ||
| $bit = 0b10000000; | ||
| $byte++; | ||
| } | ||
| if($isDark){ | ||
| $rowdata[$byte] |= $bit; | ||
| } | ||
| $bit >>= 1; | ||
| } | ||
| } | ||
|
|
||
| $rowdataString = pack('C*', ...$rowdata); | ||
|
|
||
| $body .= str_repeat($rowdataString, $this->scale); | ||
| } | ||
| return $body; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| <?php | ||
| /** | ||
| * Class QRPbm | ||
| * | ||
| * @created 19.12.2025 | ||
| * @author wgevaert | ||
| * @copyright 2025 wgevaert | ||
| * @license MIT | ||
| */ | ||
| declare(strict_types=1); | ||
|
|
||
| namespace chillerlan\QRCode\Output; | ||
|
|
||
| use function pack; | ||
| use function str_repeat; | ||
|
|
||
| class QRNetpbmGraymap extends QRNetpbmAbstract{ | ||
|
|
||
| public const MIME_TYPE = 'image/x-portable-graymap'; | ||
|
|
||
| protected const HEADER_ASCII = 'P2'; | ||
| protected const HEADER_BINARY = 'P5'; | ||
|
|
||
| protected function prepareModuleValue(mixed $value):mixed{ | ||
| $value = intval( $value ); | ||
| if ( $value < 0 ) { | ||
| return 0; | ||
| } | ||
| if ( $value > $this->options->netpbmMaxValue ) { | ||
| return $this->options->netpbmMaxValue; | ||
| } | ||
| return $value; | ||
| } | ||
|
|
||
| protected function getDefaultModuleValue(bool $isDark):mixed{ | ||
| return $isDark ? 0 : $this->options->netpbmMaxValue; | ||
| } | ||
|
|
||
| public static function moduleValueIsValid(mixed $value):bool{ | ||
| // Since this is called statically, we cannot know what $this->options->netpbmMaxValue will be. | ||
wgevaert marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return is_int($value) && 0 < $value && $value < 65536; | ||
wgevaert marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| protected function getBodyASCII():string{ | ||
| $body = ''; | ||
| $maxLength = (70 - strlen(' '.(string)$this->options->netpbmMaxValue) + 1); | ||
|
|
||
| foreach($this->matrix->getMatrix() as $row){ | ||
| $line = ''; | ||
| $rowString = ''; | ||
| foreach ($row as $module) { | ||
| for ($i = 0; $i < $this->scale; $i++) { | ||
| $line .= $this->getModuleValue($module); | ||
| if (strlen($line) >= $maxLength) { | ||
| $rowString .= $line."\n"; | ||
| $line = ''; | ||
| } else { | ||
| $line .= ' '; | ||
| } | ||
| } | ||
| } | ||
| $body .= str_repeat(trim($rowString.$line)."\n", $this->scale); | ||
| } | ||
|
|
||
| return $body; | ||
| } | ||
|
|
||
| protected function getBodyBinary():string{ | ||
| $body = ''; | ||
| foreach ($this->matrix->getMatrix() as $row) { | ||
| $line = ''; | ||
| foreach($row as $module) { | ||
| $line .= str_repeat($this->pack($this->getModuleValue($module)), $this->scale); | ||
| } | ||
| $body .= str_repeat($line, $this->scale); | ||
| } | ||
| return $body; | ||
| } | ||
| protected function pack( int $moduleValue ) { | ||
wgevaert marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if ( $this->options->netpbmMaxValue > 255 ) { | ||
| return pack('n', $moduleValue); | ||
| } | ||
| return pack('C', $moduleValue); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| <?php | ||
| /** | ||
| * Class QRNetpbmPixmap | ||
| * | ||
| * @created 19.12.2025 | ||
| * @author wgevaert | ||
| * @copyright 2025 wgevaert | ||
| * @license MIT | ||
| */ | ||
| declare(strict_types=1); | ||
|
|
||
| namespace chillerlan\QRCode\Output; | ||
|
|
||
| use function pack; | ||
| use function str_repeat; | ||
|
|
||
| class QRNetpbmPixmap extends QRNetpbmAbstract{ | ||
|
|
||
| public const MIME_TYPE = 'image/x-portable-pixmap'; | ||
|
|
||
| protected const HEADER_ASCII = 'P3'; | ||
| protected const HEADER_BINARY = 'P6'; | ||
|
|
||
| protected function prepareModuleValue(mixed $value):mixed{ | ||
| if (!is_array($value) || count($value) !== 3){ | ||
| throw new UnexpectedValueException( 'Module value should be array of length 3 for NetpbmPixmap' ); | ||
| } | ||
| foreach($value as &$rgbValue) { | ||
| $rgbValue = intval( $rgbValue ); | ||
| if ( $rgbValue < 0 ) { | ||
| $rgbValue = 0; | ||
| } | ||
| if ( $rgbValue > $this->options->netpbmMaxValue ) { | ||
| $rgbValue = $this->options->netpbmMaxValue; | ||
| } | ||
| } | ||
| return $value; | ||
| } | ||
|
|
||
| protected function getDefaultModuleValue(bool $isDark):mixed{ | ||
| return array_fill(0, 3, $isDark ? 0 : $this->options->netpbmMaxValue); | ||
| } | ||
|
|
||
| public static function moduleValueIsValid(mixed $value):bool{ | ||
| if ( !is_array($value) || count($value) !== 3 ) { | ||
| return false; | ||
| } | ||
| foreach ($value as $rgbVal) { | ||
| // Since this is called statically, we cannot know what $this->options->netpbmMaxValue will be. | ||
| if (!is_int($value) || 0 >= $value || $value >= 65536) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| protected function getBodyASCII():string{ | ||
| $body = ''; | ||
| $maxLength = (70 - strlen(' '.(string)$this->options->netpbmMaxValue) + 1); | ||
|
|
||
| foreach($this->matrix->getMatrix() as $row){ | ||
| $line = ''; | ||
| $rowString = ''; | ||
| foreach ($row as $module) { | ||
| for ($i = 0; $i < $this->scale; $i++) { | ||
| foreach($this->getModuleValue($module) as $rgbValue) { | ||
| $line .= $rgbValue; | ||
| if (strlen($line) >= $maxLength) { | ||
| $rowString .= $line."\n"; | ||
| $line = ''; | ||
| } else { | ||
| $line .= ' '; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| $body .= str_repeat(trim($rowString.$line)."\n", $this->scale); | ||
| } | ||
|
|
||
| return $body; | ||
| } | ||
|
|
||
| protected function getBodyBinary():string{ | ||
| $body = ''; | ||
| foreach ($this->matrix->getMatrix() as $row) { | ||
| $line = ''; | ||
| foreach($row as $module) { | ||
| $line .= str_repeat($this->pack($this->getModuleValue($module)), $this->scale); | ||
| } | ||
| $body .= str_repeat($line, $this->scale); | ||
| } | ||
| return $body; | ||
| } | ||
| protected function pack( array $moduleValue ) { | ||
| if ( $this->options->netpbmMaxValue > 255 ) { | ||
| return pack('n*', ...$moduleValue); | ||
| } | ||
| return pack('C*', ...$moduleValue); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.