Skip to content

Conversation

@wgevaert
Copy link

Proposed changes

See #322

Types of changes

What types of changes does your code introduce?

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation fix or enhancement (no code was touched)
  • Other (CI, dependencies, etc., please describe)

Checklist:

  • I have checked to ensure there aren't other open Issues or Pull Requests for the same update/change
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)
  • Any dependent changes have been merged and published in downstream modules There are none
  • Static analysis and unit tests pass locally with my changes

@codemasher
Copy link
Member

Hey, I haven't had a close look at the specification yet, but I see that there are multiple formats, so I'm thinking what the best way to abstract is (QRNetpbmAbstract -> QRPbm, ...). I'll add my thoughts to the code.


class QRPbm extends QROutputAbstract {

private const LIGHT_DARK = [ '0', '1' ];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary when we handle integers instead.

private const LIGHT_DARK = [ '0', '1' ];

protected function prepareModuleValue( mixed $value ): mixed {
return $value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should cast/clamp the allowed values, and the return type can be narrowed to int then.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will use bool instead

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking beyond the implementation of the b/w PBM format - int is certainly the more flexible and consistent choice.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The b/w PBM format is fundamentally different from the other formats: In PBM, 0/false is white, in PGM/PPM, 0 is black. To prevent confusion, I think I will handle PBM as a special case and use booleans for those.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case you can just completely ignore the module values setting. If you're going to abstract the classes as I recommended, you can override the setModuleValues() method from the parent to do nothing and be done with it for this particular format.

}

protected function getDefaultModuleValue( bool $isDark ): mixed {
return $isDark ? self::LIGHT_DARK[1] : self::LIGHT_DARK[0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can return (int)$isDark for the b/w PBM, 0 and 255 (or 65535?) for grayscale PGM, and for the RGB formats PPM/PAM it could return an array similar to the GD output class.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not entirely true; For P1 image format, 0 is white so we'd have to return (int)!$isDark instead. Confusingly, for P2 image format, 0 is black so we'd have to return $isDark ? 0 : $this->getMaxValue().

}

public static function moduleValueIsValid( mixed $value): bool {
return is_string( $value ) && in_array( $value, self::LIGHT_DARK, true );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simple is_int() here would suffice for grayscale, for RGB formats see

public static function moduleValueIsValid(mixed $value):bool{
if(!is_array($value) || count($value) < 3){
return false;
}
// check the first 3 values of the array
foreach(array_values($value) as $i => $val){
if($i > 2){
break;
}
if(!is_numeric($val)){
return false;
}
}
return true;
}

}

public function dump( string|null $file = null ): mixed {
$size = $this->matrix->getSize();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call is unnecessary, the size is already available in the parent class as $this->size.

public function dump( string|null $file = null ): mixed {
$size = $this->matrix->getSize();
$qrString = "P1\n"
.$size.' '.$size."\n";
Copy link
Member

@codemasher codemasher Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation does not yet take into account the module scaling. From what I understand, the Netpbm formats are raster images, so the output size would be 1 pixel per QR module, which is very small. The parent class also holds the calculated matrix size as $this->length as well as the current scale as $this->scale, which could be used to repeat the rows/columns accordingly. The return type can be narrowed to string.

.$size.' '.$size."\n";
foreach($this->matrix->getBooleanMatrix() as $row) {
foreach ($row as $isDark) {
$qrString .= $this->getDefaultModuleValue( $isDark );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method getDefaultModuleValue() is supposed to only be called when you really need the default value - while it doesn't matter in this case, it can lead to unexpected results in extended classes. In the case of the b/w PBM format, a simple (int)$isDark will suffice.

I'm also thinking of adding an option like netpbmBinary or so, to allow saving in the binary format as the raw plaintext format can grow excessively large.

Comment on lines 42 to 47
if ( is_string($file) ) {
$writeResult = file_put_contents($file,$qrString);
if ( $writeResult === false ) {
throw new QRCodeOutputException('Cannot write data to cache file: '.$file);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire block can be replaced by $this->saveToFile($qrString, $file); from the parent - it does all necessary error handling.

Comment on lines 57 to 58
$this->assertStringContainsString('1', $data);
$this->assertStringContainsString('0', $data);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You had this right before, we do call static methods statically :)


namespace chillerlan\QRCode\Output;

use chillerlan\QRCode\Output\QRNetpbmBitmapAbstract;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need separate classes for ASCII and binary here - it's quite excessive for a file format that's barely used. My initial recommendation was an abstract class for all netpbm based formats with child classes for each PBM, PGM, PPM and PAM. The conversion to binary can be done in the loop that creates the body (avoiding to loop over the same data 3 times), with a switch in the options, same goes for the header.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants