qr.pl - Relational QR codes in Prolog

                                     
                                     
    █▀▀▀▀▀█  ▀▀█▄█   █▀ ▄ █▀▀▀▀▀█    
    █ ███ █ ▄▀██ ▄ ▀ ▄▀ ▄ █ ███ █    
    █ ▀▀▀ █ ▀▄█ ▀█▄ █ ▀▀█ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ ▀▄▀ █ █▄█ ▀▄▀ ▀▀▀▀▀▀▀    
    ▀▀▄ ▄▀▀█ ▀█ ▀██▄▄██▄   ▄█▀▄▄     
     ▀ ▀█▄▀█▄▀▄█▀ ▀█▄▀ ▄▄ ▀▄ ▄       
    ▄▀▀▄▀▄▀▀▄▀█ ▄▄ ▄█▄ ▄▀ █▀▄  ▄█    
    ▄   ▄▀▀▀  ▄ ▄▀▀▀▀▀▄  ██ ▄▀█ ▄    
    ▄▀█▀ ▀▀ ▀▀▀ ▀▀▀█▄▀▄█▄ ▄▄▄▀█▄▄    
    █▀▀ ▀▀▀▀  ▄▄▀ ▄█▀ █▀ ▄ ▄█▀  ▀    
    ▀ ▀   ▀▀█  ▀██▄█▀█  █▀▀▀█▄▀▀▀    
    █▀▀▀▀▀█ █▀▄▄ ▀█▀█ ▀██ ▀ █▀▀▄     
    █ ███ █   █▄█ ▄█▄ ▄▀██▀▀▀█▄ ▄    
    █ ▀▀▀ █ ▄▀█▄█  ██ ▀▀▀▀▀█▀██▀▄    
    ▀▀▀▀▀▀▀ ▀▀▀  ▀▀  ▀ ▀   ▀▀▀▀      
                                     
                                     

https://web.liminal.cafe/~byakuren/prolog/qr/

Download qr.pl

qr.pl is a pure Prolog QR Code Model 2 implementation that is much more interesting than a normal one-shot QR generator.

It can still do the ordinary thing: turn "byakuren" into a scannable QR code, pick a sensible version / error-correction level / mask, print it to the terminal, and decode text-art QR codes copied from other tools.

But the useful part is that it stays relational.

The same module can:

It works in both Scryer and Trealla.

Downloads


Quick start

Load the module:

?- use_module(qr).
true.

Generate the best QR for byakuren and print it:

?- qr("byakuren", Spec).
                             
                             
    █▀▀▀▀▀█ ▀▀█▄█ █▀▀▀▀▀█    
    █ ███ █ █▀▄ ▄ █ ███ █    
    █ ▀▀▀ █ ▄▄█▄▀ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ █▄█ █ ▀▀▀▀▀▀▀    
     ▀▄▀ ▀▀▀▀█▀█▀▀██▄██ █    
    ▄▄▄█ ▄▀▄ █▄  █▄▀ █▀▄▀    
    ▀   ▀▀▀ ▄ ▄██▄██▀▄  █    
    █▀▀▀▀▀█ █▄▄▄█ ███ ▀█     
    █ ███ █ ▄█ ▀ ██ ▄ ▄█▄    
    █ ▀▀▀ █ ▄█▄█▄▀▀▀█▀  ▀    
    ▀▀▀▀▀▀▀  ▀   ▀ ▀▀  ▀     
                             
                             

Spec = [version(1), level(q), mode(byte), mask(7)].

Get the pure binary grid instead of rendering:

?- qr_code("byakuren", Grid, Spec).
Grid = [[1,1,1,...], ...],
Spec = [version(1), level(q), mode(byte), mask(0)] ;
... .

Run the relation backwards from a grid:

?- qr_code("byakuren", Grid, Spec),
   qr_code(Text, Grid, Spec).
Grid = [[1,1,1,...], ...],
Spec = [version(1), level(q), mode(byte), mask(0)],
Text = "byakuren" ;
... .

That encode/decode symmetry is the main difference from an ordinary QR tool. qr/1..3 are the deterministic convenience wrappers. qr_code/2,3,4 are the relational core.


Rendered examples

The simplest practical use is still just “make me a QR and print it”.

Best QR for byakuren:

?- qr("byakuren").
Spec: [version(1), level(q), mode(byte), mask(7)]

                             
                             
    █▀▀▀▀▀█ ▀▀█▄█ █▀▀▀▀▀█    
    █ ███ █ █▀▄ ▄ █ ███ █    
    █ ▀▀▀ █ ▄▄█▄▀ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ █▄█ █ ▀▀▀▀▀▀▀    
     ▀▄▀ ▀▀▀▀█▀█▀▀██▄██ █    
    ▄▄▄█ ▄▀▄ █▄  █▄▀ █▀▄▀    
    ▀   ▀▀▀ ▄ ▄██▄██▀▄  █    
    █▀▀▀▀▀█ █▄▄▄█ ███ ▀█     
    █ ███ █ ▄█ ▀ ██ ▄ ▄█▄    
    █ ▀▀▀ █ ▄█▄█▄▀▀▀█▀  ▀    
    ▀▀▀▀▀▀▀  ▀   ▀ ▀▀  ▀     
                             
                             

If you want a specific valid spec instead of the best wrapper, you can ask for one directly. Here the mask is fixed to 3 even though the best wrapper picked 7:

?- qr_code("byakuren",
           Grid,
           [version(1), level(q), mode(byte), mask(3)]),
   display_qr(Grid).

                             
                             
    █▀▀▀▀▀█ ▄▄ ▄█ █▀▀▀▀▀█    
    █ ███ █ ▀▀▄█▀ █ ███ █    
    █ ▀▀▀ █   ▀█  █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ ▀ ▀ █ ▀▀▀▀▀▀▀    
    ▄██▀ █▀▄ █▀ ▄▄▄▄▄ ▀█▄    
    ██ ▀▄█▀█▄▀ ▀▀▄ █▄▄ ██    
    ▀ ▀▀ ▀▀ ██▀██▄  ▄▄       
    █▀▀▀▀▀█ ▄▄▄▀ █████▄      
    █ ███ █ ▄▀▄ ▀▄▀▄ ▀█▄     
    █ ▀▀▀ █ █ ▀█▄▀▄▄ ▀  ▄    
    ▀▀▀▀▀▀▀  ▀ ▀▀  ▀▀▀▀      
                             
                             
Grid = [[1,1,1,...], ...].

And for the page URL itself:

?- qr("https://web.liminal.cafe/~byakuren/prolog/qr/", Spec).
Spec: [version(3), level(l), mode(byte), mask(5)]

                                     
                                     
    █▀▀▀▀▀█  ▀▀█▄█   █▀ ▄ █▀▀▀▀▀█    
    █ ███ █ ▄▀██ ▄ ▀ ▄▀ ▄ █ ███ █    
    █ ▀▀▀ █ ▀▄█ ▀█▄ █ ▀▀█ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ ▀▄▀ █ █▄█ ▀▄▀ ▀▀▀▀▀▀▀    
    ▀▀▄ ▄▀▀█ ▀█ ▀██▄▄██▄   ▄█▀▄▄     
     ▀ ▀█▄▀█▄▀▄█▀ ▀█▄▀ ▄▄ ▀▄ ▄       
    ▄▀▀▄▀▄▀▀▄▀█ ▄▄ ▄█▄ ▄▀ █▀▄  ▄█    
    ▄   ▄▀▀▀  ▄ ▄▀▀▀▀▀▄  ██ ▄▀█ ▄    
    ▄▀█▀ ▀▀ ▀▀▀ ▀▀▀█▄▀▄█▄ ▄▄▄▀█▄▄    
    █▀▀ ▀▀▀▀  ▄▄▀ ▄█▀ █▀ ▄ ▄█▀  ▀    
    ▀ ▀   ▀▀█  ▀██▄█▀█  █▀▀▀█▄▀▀▀    
    █▀▀▀▀▀█ █▀▄▄ ▀█▀█ ▀██ ▀ █▀▀▄     
    █ ███ █   █▄█ ▄█▄ ▄▀██▀▀▀█▄ ▄    
    █ ▀▀▀ █ ▄▀█▄█  ██ ▀▀▀▀▀█▀██▀▄    
    ▀▀▀▀▀▀▀ ▀▀▀  ▀▀  ▀ ▀   ▀▀▀▀      
                                     
                                     

Example run

?- qr("byakuren", Spec).
                             
                             
    █▀▀▀▀▀█ ▀▀█▄█ █▀▀▀▀▀█    
    █ ███ █ █▀▄ ▄ █ ███ █    
    █ ▀▀▀ █ ▄▄█▄▀ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ █▄█ █ ▀▀▀▀▀▀▀    
     ▀▄▀ ▀▀▀▀█▀█▀▀██▄██ █    
    ▄▄▄█ ▄▀▄ █▄  █▄▀ █▀▄▀    
    ▀   ▀▀▀ ▄ ▄██▄██▀▄  █    
    █▀▀▀▀▀█ █▄▄▄█ ███ ▀█     
    █ ███ █ ▄█ ▀ ██ ▄ ▄█▄    
    █ ▀▀▀ █ ▄█▄█▄▀▀▀█▀  ▀    
    ▀▀▀▀▀▀▀  ▀   ▀ ▀▀  ▀     
                             
                             

Spec = [version(1), level(q), mode(byte), mask(7)].

?- qr_code("byakuren", Grid, Spec),
   qr_penalty_breakdown(Grid, Breakdown).
Grid = [[1,1,1,...], ...],
Spec = [version(1), level(q), mode(byte), mask(0)],
Breakdown = [runs(230), blocks(207), finders(800), balance(0), total(1237)] ;
... .

?- qr_mask_penalties("byakuren",
                     [version(1), level(q), mode(byte), mask(_)],
                     Penalties).
Penalties =
  [ mask_penalty(0,1237),
    mask_penalty(1,1385),
    mask_penalty(2,1308),
    mask_penalty(3,1248),
    mask_penalty(4,1410),
    mask_penalty(5,1309),
    mask_penalty(6,1428),
    mask_penalty(7,1196)
  ].

So qr/1..3 are not just picking a random valid code. They can justify the chosen mask.


Why this is different

Most QR generators give you one answer and hide the rest.

qr.pl lets you treat QR codes as a search space:

For byakuren, there are many valid version-1 QR codes:

?- findall(Spec,
           ( spec_version(Spec, 1),
             qr_code("byakuren", _, Spec)
           ),
           Specs),
   length(Specs, Count).
Specs = [[version(1), level(q), mode(byte), mask(0)], ...],
Count = 48.

That is the sort of query that normal generators usually do not expose at all.

Backtracking through valid QR codes

The easiest way to see the relational side is to leave part of the spec open and let Prolog backtrack:

?- qr_code("byakuren",
           _,
           [version(1), level(q), mode(byte), mask(Mask)]).
Mask = 0 ;
Mask = 1 ;
Mask = 2 ;
Mask = 3 ;
Mask = 4 ;
Mask = 5 ;
Mask = 6 ;
Mask = 7 ;
false.

Or leave the whole spec open:

?- qr_code("byakuren", _, Spec).
Spec = [version(1), level(q), mode(byte), mask(0)] ;
Spec = [version(1), level(q), mode(byte), mask(1)] ;
Spec = [version(1), level(q), mode(byte), mask(2)] ;
... .

That is the key difference between qr_code/* and qr/*: qr/* chooses one best symbol, while qr_code/* lets Prolog enumerate the valid search space.


Reasoning over specs

Specs are plain property lists:

[version(Version), level(Level), mode(Mode), mask(Mask)]

That makes them pleasant to reason over with ordinary Prolog.

Constrain the search:

?- spec_version(Spec, 1),
   spec_level(Spec, q),
   spec_mode(Spec, byte),
   spec_mask(Spec, 3),
   qr_code("byakuren", _, Spec).
Spec = [version(1), level(q), mode(byte), mask(3)].

Ask for one exact family of answers:

?- qr_code("byakuren",
           _,
           [version(1), level(q), mode(byte), mask(Mask)]).
Mask = 0 ;
Mask = 1 ;
Mask = 2 ;
... .

Or use the bounded skeleton directly:

?- qr_spec(Spec),
   member(version(1), Spec),
   member(level(q), Spec),
   member(mode(byte), Spec),
   member(mask(0), Spec),
   qr_code("byakuren", _, Spec).
Spec = [version(1), level(q), mode(byte), mask(0)].

The spec_* helpers are useful because they keep the search finite while still feeling like ordinary Prolog reasoning.


Best-code generation

The qr/1..3 predicates are the deterministic front-end.

They choose:

  1. the smallest version that fits
  2. the strongest error-correction level that still fits inside that version
  3. the lowest-penalty mask for that symbol

That means:

?- qr("https://web.liminal.cafe/~byakuren/prolog/qr/", Spec).
                                     
                                     
    █▀▀▀▀▀█  ▀▀█▄█   █▀ ▄ █▀▀▀▀▀█    
    █ ███ █ ▄▀██ ▄ ▀ ▄▀ ▄ █ ███ █    
    █ ▀▀▀ █ ▀▄█ ▀█▄ █ ▀▀█ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ ▀▄▀ █ █▄█ ▀▄▀ ▀▀▀▀▀▀▀    
    ▀▀▄ ▄▀▀█ ▀█ ▀██▄▄██▄   ▄█▀▄▄     
     ▀ ▀█▄▀█▄▀▄█▀ ▀█▄▀ ▄▄ ▀▄ ▄       
    ▄▀▀▄▀▄▀▀▄▀█ ▄▄ ▄█▄ ▄▀ █▀▄  ▄█    
    ▄   ▄▀▀▀  ▄ ▄▀▀▀▀▀▄  ██ ▄▀█ ▄    
    ▄▀█▀ ▀▀ ▀▀▀ ▀▀▀█▄▀▄█▄ ▄▄▄▀█▄▄    
    █▀▀ ▀▀▀▀  ▄▄▀ ▄█▀ █▀ ▄ ▄█▀  ▀    
    ▀ ▀   ▀▀█  ▀██▄█▀█  █▀▀▀█▄▀▀▀    
    █▀▀▀▀▀█ █▀▄▄ ▀█▀█ ▀██ ▀ █▀▀▄     
    █ ███ █   █▄█ ▄█▄ ▄▀██▀▀▀█▄ ▄    
    █ ▀▀▀ █ ▄▀█▄█  ██ ▀▀▀▀▀█▀██▀▄    
    ▀▀▀▀▀▀▀ ▀▀▀  ▀▀  ▀ ▀   ▀▀▀▀      
                                     
                                     

Spec = [version(3), level(l), mode(byte), mask(5)].

That is the QR shown at the top of this page.

If you want every valid answer instead of the best one, use qr_code/2,3,4.


Analysis predicates

qr.pl exposes a useful analysis layer:

For example:

?- qr("byakuren", Spec, Grid),
   qr_capacity(Spec, Capacity).
                             
                             
    █▀▀▀▀▀█ ▀▀█▄█ █▀▀▀▀▀█    
    █ ███ █ █▀▄ ▄ █ ███ █    
    █ ▀▀▀ █ ▄▄█▄▀ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ █▄█ █ ▀▀▀▀▀▀▀    
     ▀▄▀ ▀▀▀▀█▀█▀▀██▄██ █    
    ▄▄▄█ ▄▀▄ █▄  █▄▀ █▀▄▀    
    ▀   ▀▀▀ ▄ ▄██▄██▀▄  █    
    █▀▀▀▀▀█ █▄▄▄█ ███ ▀█     
    █ ███ █ ▄█ ▀ ██ ▄ ▄█▄    
    █ ▀▀▀ █ ▄█▄█▄▀▀▀█▀  ▀    
    ▀▀▀▀▀▀▀  ▀   ▀ ▀▀  ▀     
                             
                             

Spec = [version(1), level(q), mode(byte), mask(7)].
Grid = [[1,1,1,...], ...].

Capacity =
  [ size(21),
    total_codewords(26),
    data_codewords(13),
    data_bits(104),
    ecc_codewords(13)
  ].

And the bundled view:

?- qr_analysis("byakuren", Analysis).
Analysis =
  [ data("byakuren"),
    spec([version(1), level(q), mode(byte), mask(0)]),
    grid([[1,1,1,...], ...]),
    size(21),
    capacity([size(21), total_codewords(26), data_codewords(13), ...]),
    penalty(1237),
    penalty_breakdown([runs(230), blocks(207), finders(800), balance(0), total(1237)]),
    received_codewords([...]),
    codewords([...]),
    mask_penalties([mask_penalty(0,1237), ..., mask_penalty(7,1196)])
  ] ;
... .

The same predicate also works directly from a grid:

?- qr_code("byakuren", Grid, [version(1), level(q), mode(byte), mask(7)]),
   qr_analysis(Grid, Analysis).
Grid = [[1,1,1,...], ...],
Analysis =
  [ data("byakuren"),
    spec([version(1), level(q), mode(byte), mask(7)]),
    grid([[1,1,1,...], ...]),
    size(21),
    capacity([size(21), total_codewords(26), data_codewords(13), ...]),
    penalty(1196),
    penalty_breakdown([runs(204), blocks(192), finders(800), balance(0), total(1196)]),
    received_codewords([...]),
    codewords([...]),
    mask_penalties([mask_penalty(0,1237), ..., mask_penalty(7,1196)])
  ].

returns a property list that includes the chosen spec, grid, capacity, penalty, breakdown, received codewords, corrected codewords, and the penalty table for every mask in that family.

That is very useful when you want to inspect why a particular QR was chosen.


Decode copied QR text

qr.pl can also decode text-art QR codes copied from terminals or websites.

It accepts:

From standard input:

qrencode -t ANSIUTF8 "byakuren" | \
  tpl -g "use_module(qr), qr_decode_stdin(Text, Spec)."

   Text = "byakuren", Spec = [version(1),level(l),mode(byte),mask(5)].

From a file:

tpl -g \
  "use_module(qr), qr_decode_file('byakuren-qr.txt', Text, Spec)."

   Text = "byakuren", Spec = [version(1),level(l),mode(byte),mask(5)].

From Prolog directly:

?- qr_code("byakuren", Grid, [version(1), level(q), mode(byte), mask(7)]),
   qr:qr_render_lines(Grid, TextArt),
   qr_decode(TextArt, Data, Spec).
Grid = [[1,1,1,...], ...],
TextArt = [[32,32,32,...], ...],
Data = "byakuren",
Spec = [version(1), level(q), mode(byte), mask(7)].

That makes it easy to copy/paste a QR from elsewhere and still bring it back into a relational Prolog workflow.


Beyond normal generators

The module also exposes things that ordinary QR front-ends usually keep hidden.

Codewords and repair

You can inspect the extracted versus corrected codewords:

?- qr_code("byakuren", Grid, [version(1), level(q), mode(byte), mask(7)], _),
   qr_codewords(Grid,
                [version(1), level(q), mode(byte), mask(7)],
                ReceivedCodewords,
                CorrectedCodewords).
Grid = [[1,1,1,...], ...],
ReceivedCodewords =
  [64,134,39,150,22,183,87,38,86,224,236,17,236,230,72,52,248,95,161,26,105,51,173,138,79,180],
CorrectedCodewords =
  [64,134,39,150,22,183,87,38,86,224,236,17,236,230,72,52,248,95,161,26,105,51,173,138,79,180].

When the grid contains recoverable damage, those two lists differ.

For example, if one data module is flipped:

?- qr_code("byakuren", Grid, [version(1), level(q), mode(byte), mask(7)], _),
   qr:flip_first_data_module(Grid,
                             [version(1), level(q), mode(byte), mask(7)],
                             DamagedGrid),
   qr_codewords(DamagedGrid,
                [version(1), level(q), mode(byte), mask(7)],
                ReceivedCodewords,
                CorrectedCodewords).
Grid = [[1,1,1,...], ...],
DamagedGrid = [[1,1,1,...], ...],
ReceivedCodewords =
  [192,134,39,150,22,183,87,38,86,224,236,17,236,230,72,52,248,95,161,26,105,51,173,138,79,180],
CorrectedCodewords =
  [64,134,39,150,22,183,87,38,86,224,236,17,236,230,72,52,248,95,161,26,105,51,173,138,79,180].

And decoding the damaged grid still recovers the payload:

?- qr_code("byakuren", Grid, [version(1), level(q), mode(byte), mask(7)], _),
   qr:flip_first_data_module(Grid,
                             [version(1), level(q), mode(byte), mask(7)],
                             DamagedGrid),
   qr_code(Text, DamagedGrid, Spec, Stages),
   member(stage(rs_corrections, Corrections), Stages).
Grid = [[1,1,1,...], ...],
DamagedGrid = [[1,1,1,...], ...],
Text = "byakuren",
Spec = [version(1), level(q), mode(byte), mask(7)],
Stages = [stage(spec, [version(1), level(q), mode(byte), mask(7)]), ...],
Corrections = [rs_corrected([correction(0,128)])].

Unknown cells and erasures

Decode can work with unknown modules marked as u or left as variables. When a partial grid is still within the Reed-Solomon repair limit, the relation can recover the original text and fill the unknown cells from the corrected symbol.

Structured append orchestration

For multi-symbol payloads:

?- qr_sequence("byakuren", _, Specs).
Specs = [[version(1), level(q), mode(byte), mask(7)]].

If the payload fits in one symbol, you get one symbol. If not, qr.pl can split it into a structured-append sequence and decode the sequence back again.

Explicit segment streams

If you do not want automatic text handling, give the exact segment structure:

?- qr_code(qr_data([fnc1(first), byte("bya"), byte("kuren")]),
           Grid,
           Spec).
Grid = [[1,1,1,...], ...],
Spec = [version(1), level(q), mode(segments), mask(0)] ;
... .

That is useful for learning how QR payload streams are really assembled.


Prolog all the way down

What makes this module interesting is not just that it can generate QR codes in Prolog.

It is that Prolog stays visible at the boundary:

So qr.pl works both as a practical terminal QR generator and as a small reference implementation for learning how QR codes work.