Skip to content

Extending ​

Almost every part of a code is a class behind a contract, built by the service container and found by name in a registry. Your own parts sit next to the built-in ones and are used the same way: by name, in config, in stored styles, in Blade.

Registries ​

RegistryHoldsContract
Qr::modules()module shapesShapes\Contracts\ModuleShape
Qr::outerEyes()outer eyesShapes\Contracts\OuterEyeShape
Qr::innerEyes()inner eyesShapes\Contracts\InnerEyeShape
Qr::eyes()eye presetsShapes\Contracts\ProvidesQrEye
Qr::filters()filtersFilters\Contracts\NamedFilter
Qr::gradients()gradientsGradients\Contracts\SerializableGradient
Qr::payloads()kinds of contentPayloads\Contracts\Payload
Qr::writers()output formatsWriters\Contracts\Writer
Qr::decoders()readersDecoding\Contracts\Decoder
Qr::styles()named stylesRendering\Style

Each one takes the same forms:

php
Qr::modules()->register('pill', PillModule::class);                   // a class, built by the container
Qr::modules()->register('pill-thin', [PillModule::class, 'width' => 0.4]);  // a class with options
Qr::modules()->register('pill', new PillModule(width: 0.6));          // an instance
Qr::modules()->register('pill', fn () => new PillModule());           // a closure
Qr::modules()->register(MyShapes::class);                             // an enum: every case under its value

Qr::modules()->has('pill');
Qr::modules()->get('pill', ['width' => 0.5]);   // options over the registered ones
Qr::modules()->names();
Qr::modules()->forget('hearts');                 // built-in names too

Register in a service provider's boot(). Classes, classes with options and enums can go in config/qr.php instead (see Configuration); closures and instances belong in code. A name registered in code wins over one in the config, which wins over a built-in one. php artisan qr:check builds every entry and names the broken ones.

Classes are built through the container, so their constructors can ask for services, and options are passed by name. An option a class does not take throws an InvalidOptionException that lists the ones it does.

Module shapes ​

A module shape draws one dark module about its middle, (0, 0), in modules. It should stay within -0.5 to 0.5 and cover the middle well: scanners read each module at its center.

php
use ExeQue\Qr\Geometry\Path;
use ExeQue\Qr\Geometry\RoundedRect;
use ExeQue\Qr\Rendering\ModuleContext;
use ExeQue\Qr\Shapes\Contracts\Configurable;
use ExeQue\Qr\Shapes\Contracts\ModuleShape;

final readonly class PillModule implements Configurable, ModuleShape
{
    public function __construct(
        public float $width = 0.5,
    ) {
    }

    public function draw(ModuleContext $cell): Path
    {
        // Lying down where the module has a neighbor to the left or right, standing otherwise.
        $across = $cell->isDark(-1, 0) || $cell->isDark(1, 0);
        [$w, $h] = $across ? [1.0, $this->width] : [$this->width, 1.0];

        return RoundedRect::path(-$w / 2, -$h / 2, $w, $h, $this->width / 2);
    }

    public function options(): array
    {
        return ['width' => $this->width];
    }
}

ModuleContext describes the module:

column, rowits place in the symbol
x, yits middle, in modules
sizethe symbol's width in modules
roleRole::Data, Timing, Alignment, ...
isDark($dx, $dy)whether a neighbor (-1, 0 or 1 each way) is drawn dark; finder modules and modules a logo clears count as light
random($min, $max, $purpose)a number that is the same for this module every time
logothe region the code's logo clears, if it has one

Shapes are drawn clockwise (y grows downwards); a hole is drawn the other way round. Path and RoundedRect build outlines:

php
Path::make()->moveTo(0, -0.5)->lineTo(0.5, 0)->lineTo(0, 0.5)->lineTo(-0.5, 0)->close();
Path::polygon([[0, -0.5], [0.5, 0], [0, 0.5], [-0.5, 0]]);
$path->curveTo($x1, $y1, $x2, $y2, $x, $y);
$path->append($hole->reversed());
$path->transform(Transform::identity()->rotate(45)->scale(0.8));

RoundedRect::path($x, $y, $width, $height, $radius);   // one radius, or [top-right, bottom-right, bottom-left, top-left]
RoundedRect::circle($x, $y, $radius);

ExeQue\Qr\Geometry\Figures has the figures the built-in shapes use: squircle(), heart(), star(), diamond(), turnedSquare(), flower(), snowflake(), octagon() and hexagon().

Configurable ​

A shape with settings implements Configurable, whose options() gives them as named constructor arguments. That is how a code using it is cached, and how a stored style builds it again. A shape with settings that does not is drawn fine, but its codes are not cached.

Versions ​

A shape that implements Versioned gives a version number. Raise it when the shape draws differently, and cached codes that use it are drawn again; other cached codes stay.

Providers and enums ​

Anything implementing ProvidesQrModule (qrModule(): ModuleShape) can be used as a shape: an enum of your shapes, a model holding a user's choice. An enum registered by class puts every case under its value.

Eye shapes ​

An outer eye draws the ring of a finder pattern; an inner eye its middle. Scanners find a code by these, measuring dark and light runs of 1:1:3:1:1 through the middle, so their geometry has rules:

Outer eye (OuterEyeShape::path()):

  • drawn in a box from -3.5 to 3.5, centered on (0, 0): seven modules across, both ways;
  • the ring about one module wide, its hole about five modules across, drawn the opposite way round to the outline;
  • straight-ish sides; a point may reach half a module past the box, into the light border round the eye.

Inner eye (InnerEyeShape::path()):

  • drawn in a box from -1.5 to 1.5: about three modules across, both ways;
  • its weight on the middle; narrow shapes may reach a little past the box so a line across the middle still measures three modules.
php
final readonly class BeveledOuterEye implements OuterEyeShape
{
    public function path(): Path
    {
        return Figures::octagon(0, 0, 7, 7, 1.0)->append(Figures::octagon(0, 0, 5, 5, 0.6)->reversed());
    }
}

An eye preset implements ProvidesQrEye, giving an outer and an inner eye: qrOuterEye() and qrInnerEye().

Check new eyes with a real scanner, or Qr::read(), as SVG and PNG. The scan guards cannot see inside a shape.

Filters ​

A filter changes modules (ModuleFilter), eyes (EyeFilter) or both, and implements NamedFilter so it can be stored, cached and registered:

php
use ExeQue\Qr\Filters\Contracts\ModuleFilter;
use ExeQue\Qr\Filters\Contracts\NamedFilter;

final class Checkerboard implements ModuleFilter, NamedFilter
{
    public function __construct(
        private float $opacity = 0.7,
    ) {
    }

    public function module(ModuleContext $cell, ModuleStyle $style): ModuleStyle
    {
        return ($cell->column + $cell->row) % 2 === 0 ? $style->fadeBy($this->opacity) : $style;
    }

    public function name(): string
    {
        return 'checkerboard';
    }

    public function options(): array
    {
        return ['opacity' => $this->opacity];
    }
}

Qr::filters()->register('checkerboard', Checkerboard::class);
Qr::url($url)->filter('checkerboard', opacity: 0.5);

ModuleStyle has withShape(), withPaint(), withRotate(), withSize(), withOffset(), withSkew(), withOpacity() and withSkip(), and relative rotateBy(), scaleBy(), offsetBy() and fadeBy(). An eye filter gets an EyeContext (the corner, the symbol's size, a stable random()) and an EyeStyle, whose outer and inner parts change with withOuter() and withInner().

name() and options() must rebuild the filter: new Checkerboard(...$filter->options()). Use $cell->random() for anything random, never rand(), so a code looks the same every time.

Gradients ​

A gradient implements Gradient: colorAt($x, $y) for a point of its box (0 to 1 both ways) and stops(). To be stored and cached, it implements SerializableGradient (toArray() and fromArray()) and is registered by name:

php
Qr::gradients()->register('stripes', StripesGradient::class);

A gradient that is not registered and serializable still paints; codes using it are not cached.

Content ​

See Content.

Writers ​

A writer turns a drawn Scene into an Output:

php
use ExeQue\Qr\Rendering\Scene;
use ExeQue\Qr\Writers\BufferedOutput;
use ExeQue\Qr\Writers\Contracts\Output;
use ExeQue\Qr\Writers\Contracts\Writer;
use ExeQue\Qr\Writers\StreamFactory;

final readonly class AsciiWriter implements Writer
{
    public function __construct(
        private StreamFactory $streams,
    ) {
    }

    public function write(Scene $scene): Output
    {
        $buffer = $this->streams->buffer();

        foreach ($scene->items as $item) {
            // $item->part, $item->cell, $item->shape (a Path), $item->transform, $item->paint, $item->opacity ...
        }

        return new BufferedOutput($buffer, 'text/plain', 'txt');
    }
}

Qr::extend('ascii', fn (Container $app, array $options): Writer => $app->make(AsciiWriter::class, $options));
Qr::url($url)->format('ascii');

A Scene has the symbol's size and margin in modules, the background paint, the drawn items in order, the scan guards' adjustments and the logo. Each item is a module or an eye part, already shaped, transformed and painted.

Outputs keep their bytes in a StreamBuffer from the StreamFactory: in memory while small, in a temporary file beyond that.

A writer that implements Fakeable gives fake(Scene $scene): Output too, a cheap stand-in used under Qr::fake().

Decoders ​

A decoder implements Decoding\Contracts\Decoder: decode(string $image): Decoded, throwing an UnreadableCodeException when there is no code to read. Throwing a DecoderUnavailableException when it cannot run lets the stack move on:

php
Qr::decoders()->register('cloud', CloudDecoder::class);
config(['qr.reading.stack' => ['cloud', 'zxing', 'native']]);

Styles ​

A class implementing Rendering\Contracts\ProvidesQrStyle (qrStyle(): Style) can be registered or passed to ->style(); an enum of such styles registered by class puts each case under its value.

Swapping the package's own services ​

Bindings to replace in a service provider:

BindTo change
Logos\Contracts\SvgSanitizerhow SVG logos are sanitized (or qr.logos.sanitizer)
Colors\ColorNameswhere color names come from
Writers\StreamFactorywhere outputs keep their bytes
any shape, filter, writer or decoder classwhat the container builds for it

Macros ​

The manager and the code builder are macroable:

php
use ExeQue\Qr\Builders\PendingQr;
use ExeQue\Qr\Facades\Qr;

PendingQr::macro('branded', fn () => $this->style('brand')->logo(resource_path('logo.svg'), size: 0.2));
Qr::macro('ticket', fn (Ticket $ticket) => $this->url($ticket->url)->branded());

Qr::ticket($ticket)->png(600);