qr

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package qr encodes a string as a QR code and draws it as SVG or PNG (M41, M49).

**It was SVG only, and that was decision D11**: the output is vector text, so no image encoder joined the dependency set and nothing rasterised on a request. The package comment said *"a PNG download, if it is ever wanted, is an additive change here and nowhere else"*, and M49 is that change. D11's premise was that the rasteriser is never called; a person who asked for a file calls it, so the reversal honours the reasoning rather than overriding it. The encoder is `image/png` from the standard library, so the dependency set is still what it was, and the rasteriser is bounded by MaxSize.

**One arithmetic, two encoders.** Code.SVGClass and Code.PNG both draw from [Code.runs] at the geometry [Code.geometry] computes, so the two outputs cannot round differently — which is the claim TestTheSVGAndThePNGAreTheSamePicture holds them to.

**The encoder is github.com/boombuler/barcode, MIT, with no module dependencies of its own** — see decisions.md, D72, for what it was weighed against. This package uses it for one thing: turning a string into a matrix of dark and light modules. Everything a reader can see — the quiet zone, the colours, the size — is drawn here, because `qr_codes.style` has to drive it and a library's own renderer would have its own opinions instead.

**Nothing an attacker controls reaches the output.** The SVG is built from integers and from colours that have already been parsed as `#rrggbb`, and it carries no title, no aria-label naming the destination, and no metadata. That is what makes it safe to inline into a dashboard page as `template.HTML`: the bytes cannot contain a `<` that did not come from this file. A QR code that announced its own URL to a screen reader would read better and would put a workspace-controlled string inside markup the template engine no longer escapes, so the surrounding page carries the label instead.

Index

Constants

View Source
const (
	LogoBoxNumerator   = 3
	LogoBoxDenominator = 10
)

LogoBoxNumerator and LogoBoxDenominator are the cap: the logo's box spans at most this fraction of the symbol's width, so nine hundredths — 9% — of its area. The package comment above is where the fraction came from and what it was measured against.

View Source
const (
	MaxLogoUploadBytes = 1 << 20 // 1,048,576
	MaxLogoDimension   = 1024
	MaxLogoPixels      = 262_144
	MaxLogoStoredBytes = 1_060_000

	// MaxDecodedLogoPixels is the decode bound the F214 reopening had to state,
	// and it is *derived* rather than chosen: the side cap admits nothing larger
	// than a square at that side, so this is what MaxLogoDimension already
	// implies. Written down because the allocation above is written down.
	MaxDecodedLogoPixels = MaxLogoDimension * MaxLogoDimension // 1,048,576

	// MaxDecodedLogoBytesPerPixel is how wide a pixel the decoders hand back,
	// which is not how wide one this package writes is. `image/png` returns
	// *image.NRGBA64 or *image.RGBA64 for a bit-depth-16 file and *image.Gray16
	// for a 16-bit greyscale one; `image/jpeg` returns *image.YCbCr or
	// *image.CMYK, both narrower. Eight is the widest either produces.
	//
	// It is a constant rather than a `4` inlined into the arithmetic because a
	// `4` inlined into the arithmetic is exactly how the shipped figure came to
	// be half of the real one.
	MaxDecodedLogoBytesPerPixel = 8

	// MaxDecodedLogoBytes is the pixel buffer those two bound together, and it
	// is the number the allocation story above turns on.
	//
	// **It is measured, not asserted.** TestTheDecodeBoundIsMeasuredNotAssumed
	// builds the widest file the caps admit, puts it through the real decoder,
	// and compares this constant against the buffer that came back;
	// TestTheCapsAgreeWithEachOther then sums the peak table out of buffers the
	// standard library allocated rather than out of this file's multiplications.
	// A test that re-derives the code's own arithmetic checks nothing, which is
	// the failure that let the four-byte figure ship.
	MaxDecodedLogoBytes = MaxDecodedLogoPixels * MaxDecodedLogoBytesPerPixel // 8,388,608
)

The caps, as numbers, with the allocation each one bounds.

This is the standard MaxSize set for M49's rasteriser — *a cap is a number with the maximum allocation it implies* — and D134 is why it is not optional here. The owner chose a `bytea` column for the stored logo against an assumption nobody could check at the time: that the caps this milestone sets keep a stored image small enough for a column to be uncontroversial. Those numbers are below, and so is the worst-case row they imply. Without the arithmetic written down, D134 would be true by assertion.

**1. The request body.** MaxLogoUploadBytes is 1 MiB and bounds the whole multipart body, envelope included, so it is also the largest buffer the handler can hold: the part is read under the same `MaxBytesReader`.

**2. The declared image, and this is the only header bound left.** MaxLogoDimension is 1024 pixels a side, checked against the *header* before any pixel buffer exists. One bound rather than two, since the F214 reopening: a side cap of 1024 already implies an area of at most 1024 × 1024, so MaxDecodedLogoPixels is **1,048,576** and is derived from the side rather than declared beside it. Nothing can pass the side cap and fail an area cap, which is the shape that made the shipped refusal unanswerable.

**3. The decoded image, and a pixel is eight bytes wide rather than four.** This package normalizes to image.NRGBA, which is four — but that is what it *produces*, not what the decoder *hands it*. `image/png` decodes a bit-depth-16 file to image.NRGBA64 or image.RGBA64, **eight bytes a pixel**, and such a file is not exotic or large: a 1024×1024 16-bit RGBA PNG of one flat colour is about ten kilobytes on the wire, so it passes the body cap by two orders of magnitude and the side cap exactly. Every earlier statement of this figure — this file's, SECURITY.md's, D135's — computed at four and was therefore half of what an upload can actually cost.

The alternative was to refuse bit depth 16 here and keep four true. **Rejected**: it adds a refusal to the one milestone whose purpose is to stop refusing what it can adapt, and it would refuse a valid PNG for a property its author cannot see in any viewer. So the arithmetic moved instead — see MaxDecodedLogoBytesPerPixel, and the D180 entry in decisions.md for the trade.

The largest decode this package can be made to perform is therefore 1,048,576 × 8 = **8,388,608 bytes** (MaxDecodedLogoBytes) — four times what the shipped area cap admitted, because the reopening's whole point is that an image over the *storage* target is decoded and shrunk rather than refused unread. [resampleNRGBA] converts that source once more into its own NRGBA buffer, and the destination is bounded by step 4, so the **image buffers** this package holds at once are

the upload, live across the decode  MaxLogoUploadBytes = 1,048,576
decoded source        1,048,576 × 8                    = 8,388,608
resampler's own copy  1,048,576 × 4                    = 4,194,304
resampled destination   262,144 × 4                    = 1,048,576
                                                        ----------
                                                        14,680,064

**14 MiB exactly, against 4 MiB before** — the shipped pipeline refused anything past 262,144 pixels from the header, so its terms were the same upload buffer, a 2,097,152-byte decode and a 1,048,576-byte NRGBA copy with no resample at all, summing to 4,194,304. The upload is a term because NormalizeLogo holds it for the whole call: `png.Decode` reads a reader over that same slice, so it is live alongside everything the decode allocates. A figure called *the peak* that leaves out the upload is not one.

**What the table excludes is the encoder, and the standard library bounds it rather than this file.** Two terms, neither a function of the upload's size:

  • the `bytes.Buffer` the PNG is written into grows by doubling, so at its last growth the old array and the new one are both live — under 3,145,728 bytes for a worst-case output of about a megabyte;
  • flate's window and hash tables and the encoder's per-scanline buffers are a fixed cost of one `png.Encode`, measured at about 850,000 bytes on go1.26 and the same for a 1×1 image as for the largest one.

Under 4 MiB together, so **under 18 MiB for one upload in flight**. Stated beside the table rather than summed into it because both terms are properties of a Go release, which is the same reason MaxLogoStoredBytes carries slack over its derivation; TestTheCapsAgreeWithEachOther pins the 14,680,064 this file's own caps do bound. The handler's read buffer doubles the same way and does not add to the peak — it reaches its own largest size before anything is decoded, and what it hands over is the first row of the table.

That is the price of downscaling instead of refusing, and it is why uploads have a rate limit bucket of their own rather than sharing the write bucket.

**4. The stored artefact, which is the number D134 is owed.** MaxLogoPixels is 262,144 and is a *target*, not a refusal: anything above it is resampled down to fit, keeping its aspect ratio, and the caller is told what it was and what it became. The output is a PNG this product encoded from that NRGBA, and the worst case is an image whose pixels do not compress at all, so deflate falls back to stored blocks. It is also the *tallest* such image the caps allow rather than the squarest, because PNG spends a filter byte per scanline: at the area cap, 1024×256 costs 1024 filter bytes where 512×512 costs 512.

filtered scanlines    1024 × (1 + 256×4)               = 1,049,600
deflate stored blocks ceil(1,049,600 / 65,535) × 5     =        85
zlib header and Adler-32                               =         6
IDAT chunk framing    ceil(1,049,691 / 32,768) × 12    =       396
PNG signature, IHDR and IEND                           =        45
                                                         ---------
                                                         1,050,132

MaxLogoStoredBytes is **1,060,000** — that bound with room over it, because two of its terms (the stored-block size deflate falls back to, and the 32 KiB buffer Go's encoder flushes IDAT chunks at) are properties of the standard library rather than of the PNG format. And it is **enforced rather than argued**: NormalizeLogo refuses an encoding above it instead of trusting the arithmetic, so a Go release that frames its output differently produces a failed upload rather than a row past the bound. TestTheWorstCaseLogoFitsTheStatedBound builds that exact image out of incompressible pixels and pins both halves — that the real figure is close to the derivation, and that it is under the constant.

**So the worst case a `qr_codes` row can carry is 1,060,000 bytes**, and a link at domain.MaxQRCodesPerLink — twenty — is bounded at 21,200,000 bytes, about 20 MiB. That is the sizing question D134 accepted, stated as a number: it is in the row, in every backup and in every `pg_dump`. Typical logos are two orders of magnitude below it; the ceiling is what an adversary can reach.

View Source
const (
	// DefaultLevel is where the level *starts*, and since D184 it is no longer
	// what a code draws at. M is the level nearly every printed QR code in the
	// world uses, so it is the baseline the free-level rule is measured against:
	// [Encode] draws at the strongest level whose symbol is no bigger than this
	// one's, which for the URL shapes this product produces is usually Q. See
	// [LevelFor].
	//
	// **It is a floor under every code and not a default for codes that named
	// nothing** (D187). Nothing this product draws is ever below the strongest
	// free level, whatever a row says, so `L` is a value the API accepts and can
	// never draw — a level weaker than one that costs nothing is a request for
	// less correction at no saving, which is the thing the owner refused.
	DefaultLevel  = LevelM
	DefaultMargin = 4
	DefaultScale  = 8

	// MinScale and MaxScale bound what a stored style may carry. The ceiling is
	// derived from [MaxSize]: the smallest code is 21 modules, the narrowest
	// quiet zone adds 2×[MinMarginModules], and floor(2048/27) = 75 is the
	// largest pixels-per-module [FitSize] can ever emit — so every fit is a
	// style Normalize accepts. It was 32, capped by nothing but the `width`
	// attribute a downloaded file carries, and that cap is what made the old
	// FitSize fill large requests with quiet zone instead of scale (F213); it
	// was 68 while the quiet zone was pinned at four modules and [MaxSize] was
	// 2000, and both of those moved at the second M49 reopening (F221).
	MinScale = 2
	MaxScale = 75
	// MaxMargin bounds the quiet zone a *stored* style may carry in modules,
	// and since the M49 reopening that is all it bounds: [FitSize] writes the
	// quiet zone in pixels now, and 16 stays only because rows written by the
	// old search carry up to it and must keep rendering as written.
	MaxMargin = 16
	// MinMarginModules is the narrowest quiet zone [FitSize] will produce, and
	// it is **below** the four modules ISO/IEC 18004 specifies (D182).
	//
	// Four ±25% is the owner's band, and three is its low end. Being under the
	// specification, it is measured rather than argued: `make verify-scan`
	// renders the corpus at this quiet zone across the whole version range and
	// decodes it through two pinned decoders at five simulated distances, the
	// same instrument M50.6's logo fraction rests on. The result is in
	// decisions.md; a change to this number is a change that has to be
	// re-measured, not re-reasoned.
	MinMarginModules = 3

	// DefaultForeground and DefaultBackground are dark-on-light, and the
	// background is always painted rather than left transparent. A QR code
	// inverted onto a dark page is refused by a large share of scanners, and a
	// transparent one becomes inverted the moment somebody views the dashboard
	// in dark mode. So the code carries its own background and does not follow
	// the theme; the frame around it does. See decisions.md, D74.
	DefaultForeground = "#000000"
	DefaultBackground = "#ffffff"
)

Defaults. Four modules of quiet zone is the minimum ISO/IEC 18004 specifies — below it scanners start failing against busy backgrounds. Eight pixels per module puts a short URL at roughly 300px, which is a size a phone camera reads from a screen without zooming.

View Source
const (
	MinSize = 64
	MaxSize = 2048
)

MinSize and MaxSize bound the output size in pixels a caller may ask for, and MaxSize is also the bound on what this package will rasterise (M49).

**The ceiling is what D11 was protecting.** D11 refused an image encoder partly so that nothing would allocate a bitmap on a request; M49 makes that happen, so the allocation gets a number rather than a hope. The PNG is image.Paletted over a two-colour palette — one byte per pixel — so 2048×2048 is **4,194,304 bytes**, 4 MiB exactly, and that is the largest buffer a request can cause here. The SVG path allocates nothing of the sort and is bounded by the module count instead.

**2048 rather than 2000 is owner-instructed** (D182), so the top of the size control is a round power of two; it is a QR code 6.8 inches across at 300 DPI, which covers the poster the milestone is written for.

The floor is the smallest picture the bounds can produce at all: the shortest code is 21 modules, the narrowest quiet zone is MinMarginModules a side, and MinScale pixels per module makes 54 — so 64 is a request the shortest code can always draw. **It is not a floor every code can draw**, because the number of pixels a symbol needs is a property of the symbol: see MinSizeFor, which is what a request below a particular code's own floor is refused against.

A request outside the range is **refused rather than clamped**, on the rule TestOutOfRangeSizesAreRefusedRatherThanClamped already states for margin and scale: clamping reports success for a setting nobody asked for.

View Source
const (
	ContentType    = "image/svg+xml"
	PNGContentType = "image/png"
)

ContentType is what a QR response is served as, and PNGContentType is the second one since M49.

View Source
const FluidClass = "max-w-full h-auto"

FluidClass is the class an inlined code carries so that the box it is drawn into can shrink it (F184).

**`width` and `height` are pixels and the page is not.** The drawing sits inside a viewBox of the same extent, so the two attributes decide an intrinsic size and nothing else — a consumer that sizes the element with CSS gets the same code at any size. What they *also* decide, for a consumer that sizes nothing, is how far the element reaches: a 488px enrolment code inside a 160px frame took `/account/mfa` 174px past a 360px viewport and made it the one page in the dashboard that scrolled sideways. `max-w-full` is the constraint and `h-auto` is what keeps the picture square while it applies.

Tailwind's names, in a package that knows nothing else about the dashboard, because the alternative is an inline `style` attribute and the dashboard's CSP is `style-src 'self'` with no `unsafe-inline`. Both utilities are already in the generated stylesheet, so no build step depends on this constant. Anything that wants the bare document — a file somebody downloads — asks for it, by naming the empty class through RenderClass; see link.Service.RenderQRBySlug.

View Source
const MaxContent = 1024

MaxContent is the longest string this package will encode. Version 40 at level L holds 2953 bytes, and a short URL is two orders of magnitude below that; the bound exists so an oversized input is a sentence rather than a library error nobody can act on.

View Source
const MaxLogoRasterSide = 512

MaxLogoRasterSide bounds the composited raster, in pixels a side.

**512 is not a new number.** It is ⌊√[MaxLogoPixels]⌋, so the largest raster this file can produce is exactly the largest image M50.5 *stores*. Since D180 that figure bounds neither the decode nor what is accepted — a header is refused only past MaxLogoDimension, and everything inside it is decoded and resampled down to fit — so what this raster matches is the stored artefact it is drawn from. The worst case its PNG encodes to is the 1,050,132-byte figure logo.go already derives, bounded by MaxLogoStoredBytes and enforced there. Reusing that arithmetic is the point: a second bound would be a second place to keep the same number.

**It binds on both paths since the 2026-08-12 reopening**, and it did not before. A rasterised code stops at MaxSize pixels, so its box stops at MaxSize·LogoBoxNumerator/LogoBoxDenominator; at the old one fifth that is 409 and never reached this, and at three tenths it is 614 and does. What happens then is already written: [logoDrawing.drawPNG] resamples the clamped raster up to the rectangle the box needs, on the same arithmetic the SVG path has always used, so the two outputs still draw the same rectangle and the only cost is that a logo filling a 2048px code is drawn from 512 pixels of detail rather than 614.

View Source
const MinProductContent = 22

MinProductContent is the shortest payload, in bytes, that this product asks for a QR code of.

**It is the floor of the range the cap was checked over**, and it is here rather than derived because this package knows nothing about aliases or hostnames: `https://` (8) + the shortest registrable hostname `a.b` (3) + `/` + an alias at `alias.MinLength` (3) + `?src=qr` (7). A named code adds `&qrc=` and eight characters and is therefore longer.

Versions 1 and 2 hold 7 and 14 bytes at level H, so nothing this product encodes reaches them — which matters, because neither can carry a logo inside the share of H's correction budget a box may spend, and LogoBoxModules shrinks the box to almost nothing for both. TestTheShortestContentIsWhatInternalQRAssumes, in internal/link, is what holds this number to the two bounds it came from.

Variables

View Source
var (
	// ErrLogoEmpty is an upload with no bytes in it.
	ErrLogoEmpty = errors.New("logo: no bytes")
	// ErrLogoFormat is an upload that is neither PNG nor JPEG.
	ErrLogoFormat = errors.New("logo: not a PNG or a JPEG")
	// ErrLogoSVG is the one refused format worth naming, because it is the one
	// somebody will try.
	ErrLogoSVG = errors.New("logo: SVG is a document, not an image")
	// ErrLogoTooLarge is an image this package will not decode. Raised from the
	// header, before a pixel buffer exists. Every one of them carries a
	// [LogoBoundError] with the measurement and the single bound it crossed —
	// see that type for why "a sentinel plus a sentence" was not enough.
	ErrLogoTooLarge = errors.New("logo: too large to decode")
	// ErrLogoUndecodable is a file whose header sniffed as PNG or JPEG and whose
	// body then did not decode.
	ErrLogoUndecodable = errors.New("logo: does not decode")
	// ErrLogoStoreTooLarge is a re-encoding above MaxLogoStoredBytes. Unreachable
	// through the caps above by the arithmetic in this file's comment, and
	// checked anyway — an arithmetic bound nothing enforces is the shape D134
	// asked this milestone not to leave behind.
	ErrLogoStoreTooLarge = errors.New("logo: re-encodes above the stored bound")
)

The refusals, as sentinels rather than as messages.

internal/qr holds no opinion about HTTP status codes and does not import internal/domain, so the wording a caller sees is written in internal/link beside the other validation errors — the shape ErrTooLarge already established for M49's rasteriser.

View Source
var ErrSizeOutOfRange = errors.New("output size out of range")

ErrSizeOutOfRange is a requested output size outside [MinSize, MaxSize].

View Source
var ErrTooLarge = errors.New("too large to rasterise")

ErrTooLarge is a drawing whose pixel size exceeds MaxSize. It is reachable only from the PNG path and only for a style stored before M49, whose margin and scale are read forward exactly as written and can therefore describe a picture larger than anything the size control will now produce.

View Source
var ErrTooLong = errors.New("too long to encode as a QR code")

ErrTooLong is returned for content past MaxContent.

Levels is every level a style may name, in the order a form should offer them. A style may name LevelL and no picture is ever drawn at it: D187 makes the level a floor and the strongest free level is never below DefaultLevel, so naming L asks for less correction than costs nothing and gets the free level.

Functions

func FitStoredLogo(w, h int) (int, int)

FitStoredLogo is the size an image of w×h is stored at.

**Both bounds, one ratio, and never larger than what came in.** The side bound is MaxLogoDimension and the area bound is MaxLogoPixels; an image inside both is stored untouched, because resampling a picture that already fits would throw away detail to no purpose. Anything outside either is scaled by the smallest factor that satisfies both, which keeps the aspect ratio: a wordmark stays a wordmark, which is [fitInside]'s reasoning applied to a rectangle rather than to a square.

Rounding is to nearest rather than down, and then corrected. Flooring costs the common case a pixel for nothing — 813×813 lands on 511.99 and would store 511×511 where 512×512 fits exactly — and rounding up can cross the area bound by one row, so the loop below walks the longer side back until it does not. It runs at most a handful of times: one step of the longer side removes a whole row or column.

func LogoBoxModules added in v0.3.0

func LogoBoxModules(modules int) int

LogoBoxModules is the side, in modules, of the centred square a logo occupies in a symbol of `modules` modules.

Zero for a symbol too small to carry one at all, which no version this product encodes to reaches.

func MinSizeFor added in v0.3.0

func MinSizeFor(modules int) int

MinSizeFor is the smallest picture a code of `modules` modules can be drawn at: the symbol at MinScale, plus MinMarginModules of quiet zone a side.

**A floor per code rather than one constant, because the pixels a symbol needs are a property of the symbol.** MinSize is 64 and a 29-module code cannot be drawn at 64 with a quiet zone that scans — it needs 70. The alternative was raising MinSize until it covered every code, which at version 40 is 366 and would refuse five sixths of the range the product accepts today for the sake of payloads no link in it produces. So the global floor stays the control's and this is the code's, and a request between the two is refused with this number in the sentence.

func MinSizeForStyle added in v0.3.0

func MinSizeForStyle(modules int, st Style) int

MinSizeForStyle is MinSizeFor for a style that has already fixed the module width: the smallest picture a code of `modules` modules can be drawn at with this style's Style.Scale, and so the floor Style.Size must clear for the drawn size to be the requested one.

**Two floors, both real, and which one binds depends on who is asking.** MinSizeFor is the floor over every scale, and it is the size control's, because the control chooses the scale itself and will take the smallest one before it gives up. This is the floor for a caller who set the scale as well, which is what the API accepts: a style is `size` *and* `scale`, and [fitGeometry] draws the requested size only while the symbol and its quiet zone fit inside it — below that it falls back to margin-and-scale and the picture measures something else. MinSizeFor is this function at MinScale.

func OutputSize added in v0.3.0

func OutputSize(modules int, st Style) int

OutputSize is the width in pixels a normalized style draws a code of `modules` modules at. The picture is square, so one number (M49).

This is the read direction of the size control, and it answers for both forms of a style: one written since D182 carries the size and this returns it, and one written before carries a margin and a scale and the size it means is whatever those two already produce.

func Render

func Render(content string, style Style) ([]byte, error)

Render encodes content and draws it, in one call, for the common case.

**The common case is a code inlined into a page**, so it carries FluidClass and fits the box it is put in. A caller that has stated the element's size itself passes its own class to RenderClass — see ui.QRThumbClass, which fixes the link page's thumbnail at 6rem — and a caller that wants no class at all passes the empty string, which writes no attribute.

func RenderClass added in v0.3.0

func RenderClass(content string, style Style, class string) ([]byte, error)

RenderClass is Render with a CSS class on the root `<svg>` (M48).

**Why a class is worth an entry point of its own.** `Scale` sizes the drawing in pixels, and the pixel count is a function of the *encoded version* — a longer URL is a bigger matrix, so the same style produces a taller picture for a longer link. That is fine for a code somebody scans and wrong for one drawn into a fixed row of a page, where the height has to be a property of the page rather than of the data. A class is how a caller states it.

The class is **validated, not trusted**, which is what keeps the package comment's promise true — the bytes of an SVG this package emits cannot contain a `<` that did not come from this file. `validClass` accepts the characters a class list is made of and nothing that could close an attribute or a tag, so a caller cannot smuggle markup through it whatever it passes.

An empty class writes no attribute at all, which is what Render relies on.

func RenderClassWithLogo(content string, style Style, class string, logo []byte) ([]byte, error)

RenderClassWithLogo is RenderClass with an image composited into the middle (M50.6).

**The level is forced to H whenever there is a logo**, here rather than only at the service that stores the style: a logo occludes modules, and the cap composite.go derives is a cap against H's correction budget. A row that says otherwise draws at H anyway, so the picture and the claim cannot come apart.

func RenderPNG added in v0.3.0

func RenderPNG(content string, style Style) ([]byte, error)

RenderPNG encodes content and rasterises it, the way Render draws it (M49).

func RenderPNGWithLogo(content string, style Style, logo []byte) ([]byte, error)

RenderPNGWithLogo is RenderPNG with an image composited into the middle (M50.6). A nil logo is RenderPNG, down to the paletted output.

func RenderWithLogo(content string, style Style, logo []byte) ([]byte, error)

RenderWithLogo draws a code with an image composited into the middle of it (M50.6). A nil logo is Render, FluidClass and all.

Types

type Code

type Code struct {
	// Size is the width of the matrix in modules, quiet zone excluded.
	Size int
	// contains filtered or unexported fields
}

Code is an encoded matrix, before anything has been drawn.

func Encode

func Encode(content string, level Level) (*Code, error)

Encode turns content into a matrix at the level **the rule** picks, which is the stronger of the level asked for and the strongest one that is free (D184, D187).

**Correction is taken wherever it costs nothing.** A QR symbol steps between versions, and correction below the next step is free: `https://lnk.io/ab3x9?src=qr` is 29 modules across at M and 29 at Q, so a code drawn at the old default of M was giving up a level of damage tolerance that cost it nothing. The free level is the strongest one whose symbol is no bigger than DefaultLevel's — *never at the cost of a version.*

**The level asked for is a floor, not an instruction** (D187). The rule binds everything below it and never lowers: `L` on a payload where `Q` is free draws `Q`, because nobody may ask for less correction than costs nothing; `H` draws `H` at whatever version it costs, which is how Style.ForLogo forces the level a logo needs (D141). The build recommended honouring a named level exactly, on D185's ground that a `PUT` which reads back changed is a surprise; the owner took the other side, and the cost accepted with it is that a row naming a weak level disagrees with the picture drawn from it.

**At or below the free level the module count is DefaultLevel's, always.** That is not a nice property, it is the load-bearing one: a stored Style.Size is fitted against a module count, so a rule that moved one would falsify M49's *the requested size is the size stored and drawn, exactly*. A floor *above* the free level does move it — that is what H under a logo has always cost — and every site in internal/link that fits a size encodes through this function, so the fit is made against the symbol that will be drawn rather than against the one the row names. TestTheRuleNeverChangesTheModuleCount and TestTheRuleBindsALevelSomebodyNamed are where both halves are asserted.

LevelFor is this same arithmetic when the caller wants the level rather than the matrix.

The style's colours and sizes do not reach here: they change the drawing, not the encoding, which is why a workspace re-styling its code cannot change what the code says.

func (*Code) Dark

func (c *Code) Dark(x, y int) bool

Dark reports whether the module at (x, y) is dark. Out of range is light, which is what the quiet zone is.

func (*Code) PNG added in v0.3.0

func (c *Code) PNG(st Style) ([]byte, error)

PNG rasterises the matrix at the same geometry SVGClass draws it.

**A paletted image, two colours, and that is not a size optimisation.** It is what makes the allocation MaxSize's comment states — one byte per pixel rather than four — and it is also what makes the output honest: a QR code has exactly two colours, so an RGBA buffer would be three quarters padding and would let an antialiasing bug produce a third colour without anything noticing. Go's PNG encoder writes a two-entry palette at one bit per pixel, so the file is small as a side effect rather than as an aim.

The style must already be normalized — RenderPNG is the entry point that guarantees it, on the same terms SVGClass is written for.

func (*Code) SVG

func (c *Code) SVG(st Style) []byte

SVG draws the matrix, with no class on the root element.

func (*Code) SVGClass added in v0.3.0

func (c *Code) SVGClass(st Style, class string) []byte

SVGClass draws the matrix. The style must already be normalized and the class must already have been checked — Render and RenderClass are the entry points that guarantee both, and passing an unchecked one is a programming error rather than a runtime one.

**Dark modules are drawn as horizontal runs, one rect per run.** A rect per module is the obvious shape and produces roughly ten times the bytes for a version-10 code; a single path with move-and-draw commands is smaller still and cannot be read back by anything simpler than a path parser. Runs are the middle: about a quarter of the size of per-module rects, and a shape whose test can reconstruct the matrix and compare it to the encoder's.

type FieldError

type FieldError struct {
	Field   string
	Code    string
	Message string
}

FieldError is one thing wrong with a style. Deliberately its own type rather than domain.FieldError: this package draws pictures and knows nothing about HTTP, and the service that calls it converts.

type Level

type Level string

Level is the error-correction level, as ISO/IEC 18004 names them. A higher level survives more damage and costs modules, which makes the code denser at the same printed size.

const (
	LevelL Level = "L" // ~7% recoverable
	LevelM Level = "M" // ~15%
	LevelQ Level = "Q" // ~25%
	LevelH Level = "H" // ~30%
)

func LevelFor added in v0.3.0

func LevelFor(content string, named Level) Level

LevelFor is the level content is drawn at for a style whose level is `named`: the stronger of that floor and the strongest free level. It is the level a `GET` has to report and a panel would have to print, because it is the level the picture actually carries (D184, D187).

It answers the floor for content that cannot be encoded at all, because the callers are reporting a level beside a picture whose failure they are already reporting by other means — the same reasoning link.QROutputSize's zero rests on.

type Logo struct {
	// PNG is what goes in the column. Never the received bytes.
	PNG []byte
	// Width and Height are the stored dimensions, carried so a caller can
	// report them without decoding the output again.
	Width, Height int
	// SourceWidth and SourceHeight are what was uploaded. They differ from the
	// pair above exactly when the image was resampled to fit MaxLogoPixels, and
	// they exist so the caller can say so — a product that silently shrinks
	// somebody's artwork and reports success has told them nothing.
	SourceWidth, SourceHeight int
}

Logo is a stored logo: bytes this product produced, and the size they draw at.

func NormalizeLogo(upload []byte) (Logo, error)

NormalizeLogo turns an upload into the bytes this product will store.

**What comes out is never what went in**, and that is three defences in one step rather than a tidiness preference. A polyglot — a file that is a valid PNG *and* a valid something-else — stops being one, because only the pixels survive. Metadata goes with it: EXIF, colour profiles, XMP, a JPEG comment holding whatever somebody put there. And what this product later serves is bytes it encoded, so a decoder bug in a reader downstream is not reachable through a file this instance merely relayed.

The intermediate is image.NRGBA rather than whatever the decoder returned, which is what makes MaxLogoStoredBytes computable at all: a JPEG decodes to YCbCr and a paletted PNG to image.Paletted, and the encoder's output size depends on which. One buffer shape, one arithmetic.

**And it is shrunk to fit rather than refused** (F214). Everything past MaxLogoPixels is resampled down to it with its aspect ratio kept, through the same [resampleNRGBA] the composited drawing uses — the scaler M50.6 wrote by hand rather than adding a module, reused here rather than copied. The returned Logo carries both sizes so the caller can say what happened.

func (Logo) Resampled added in v0.3.0

func (l Logo) Resampled() bool

Resampled reports whether the stored image is a shrunk copy of the upload.

type LogoBoundError added in v0.3.0

type LogoBoundError struct {
	// Width and Height are what the file declared, in pixels.
	Width, Height int
	// Bound names which limit was crossed: "side" or "pixels". Two values
	// because two callers enforce a bound — this file refuses an oversized
	// *upload* by its side, and composite.go refuses an oversized *stored* image
	// by its area (the storage target, which nothing but a hand-written row can
	// exceed).
	Bound string
	// Limit is that bound's value.
	Limit int
}

LogoBoundError is a refusal that names what was measured and the one bound it crossed.

**A sentinel and a sentence were not enough, and F214 is the proof.** The shipped refusal read *"a logo is at most 1024 pixels on a side and 262,144 pixels in total"* for an 813×813 upload — two bounds, neither of them obviously the one that bit, and no mention of what the file actually measured. The caller could not tell which number to fix. Carrying the measurement and the limit as *fields* is what lets internal/link write a sentence with a verdict in it instead of a restatement of the rules.

It answers errors.Is for ErrLogoTooLarge, so every existing caller that matched the sentinel goes on matching it.

func (*LogoBoundError) Error added in v0.3.0

func (e *LogoBoundError) Error() string

func (*LogoBoundError) Is added in v0.3.0

func (e *LogoBoundError) Is(target error) bool

Is reports that this is an ErrLogoTooLarge, so the sentinel goes on being the thing callers switch on.

type LogoConfig added in v0.3.0

type LogoConfig struct {
	Width, Height int
	// Format is "png" or "jpeg", as sniffed. Reported so a caller can say which
	// decoder ran without re-sniffing.
	Format string
}

LogoConfig is what step 2 learns without decoding anything.

func DecodeLogoConfig added in v0.3.0

func DecodeLogoConfig(upload []byte) (LogoConfig, error)

DecodeLogoConfig reads the header only.

**This is the step that bounds the allocation, and it is separated from NormalizeLogo so it can be called on its own by a test that proves the bound is enforced before any pixel buffer exists.** `DecodeConfig` on both standard-library decoders parses the header and returns; neither allocates an image, which is what makes checking the dimensions here different in kind from checking them after `Decode`.

type SizeFit added in v0.3.0

type SizeFit struct {
	// Size is the size in pixels, and since D182 it is the size that was asked
	// for — this field exists because a caller needs the number, not because it
	// can differ from the request.
	Size int
	// Scale is the pixels per module, and Margin the quiet zone **in pixels on
	// the near side** — the left and the top.
	//
	// Near side, because the far side carries the odd pixel when the remainder
	// is odd: 71 pixels over a 25-module symbol at 2 pixels a module leaves 21,
	// which is 10 on the left and 11 on the right. Centring it perfectly would
	// mean giving up a pixel of the requested size, and the requested size is
	// the thing this whole arithmetic exists to keep. One pixel of asymmetry in
	// a quiet zone is invisible and costs a scanner nothing; a size that came
	// back one short of what was typed is the defect being fixed.
	Margin int
	Scale  int
}

SizeFit is a requested output size resolved onto a style that draws it.

func FitSize added in v0.3.0

func FitSize(modules, want int) (SizeFit, error)

FitSize resolves a requested output size in pixels to a style that draws exactly it (M49; arithmetic replaced twice, at the 2026-08-12 reopenings F213 and F221).

**The requested size is the size drawn, at every value in the range**, and this is the second answer to the question — D179 pinned the quiet zone at four modules and put the rounding remainder into the drawn size, which is the behaviour the owner used and rejected: *"the number set is where it should stay, the quiet zone should be reduced to fit"*. D182 is what makes that possible, and it is one observation: **only the symbol needs whole modules.** The quiet zone is white space, so it can be any pixel count at all —

size = modules·scale + 2·margin_px

is satisfiable at every size, with `scale` the only integer to choose.

**The scale chosen is the one whose remainder puts the quiet zone nearest DefaultMargin modules**, subject to never leaving less than MinMarginModules. That floor is the binding constraint and the objective is not: at a coarse scale — a large symbol in a small picture — the two candidates either side of four modules can be three tenths of a module and twenty-six of them, and this takes the wide one. The consequence is stated rather than hidden: the quiet zone lands inside the owner's 3-to-5 band wherever the grid admits one, and where it does not it errs **wide**, which costs white space and never scannability. TestTheQuietZoneLandsInTheBand is where the condition is written down and measured.

A tie goes to the larger scale, so a picture is never looser than it needs to be.

type Style

type Style struct {
	Foreground string `json:"foreground,omitempty"`
	Background string `json:"background,omitempty"`
	// Level is the error-correction level, and since D184 it is a **floor rather
	// than an instruction**: the level drawn is whatever [LevelFor] answers,
	// which is the stronger of this field and the strongest level that does not
	// make the symbol any bigger than [DefaultLevel] does.
	//
	// **Honoured upward, ignored downward** (D187). Naming `H` gets `H`, at
	// whatever version that costs — that is how a logo forces one. Naming `L`
	// gets the free level, because nobody may ask for less correction than costs
	// nothing. The row keeps what was written and the picture carries what was
	// drawn, so the two disagree for a caller who named a weak level; that
	// disagreement is the cost the owner accepted rather than rewrite a field
	// behind a caller's back.
	//
	// This is why [Style.Normalize] leaves it empty rather than filling in
	// [DefaultLevel] the way it fills in every other field: the other defaults
	// are numbers, and a floor written in by a constant is a floor nobody asked
	// for. It is inert — [DefaultLevel] is never above the free level, by
	// construction — but a row is a record of what a caller said, and every row
	// this product ever wrote said `M` because Normalize said it.
	Level Level `json:"level,omitempty"`
	// Margin is the quiet zone, in modules. It is what the picture is built
	// from only when Size is unset — see Size.
	Margin int `json:"margin,omitempty"`
	// Scale is pixels per module. It is the one number both forms of this
	// struct share, because a module is a whole number of pixels in either.
	Scale int `json:"scale,omitempty"`
	// Size is the output size in pixels, and when it is set it is what the
	// picture measures — exactly, at every value (D182).
	//
	// **Two forms of the same geometry, and this one is the newer.** Before the
	// second M49 reopening a style carried Margin and Scale and the picture was
	// whatever those multiplied out to, which is why a requested size could only
	// be honoured by snapping it to the nearest one the module grid admitted.
	// The way out is that **only the symbol needs whole modules**: the quiet
	// zone is white space and can be any pixel count at all. So the picture is
	// Size pixels across, the symbol is `modules × Scale` of them centred inside
	// it, and the remainder is the quiet zone — carried in pixels, which is what
	// makes every requested size reachable.
	//
	// Zero is unset and means the older form, which is what every row written
	// before this milestone carries and what [Code.geometry] falls back to. It
	// is also the fallback for a stored Size the symbol has since outgrown — a
	// link whose alias grew encodes to a larger matrix, and a picture that can
	// no longer hold its own symbol is not a picture.
	Size int `json:"size,omitempty"`
}

Style is how a code is drawn. It is what `qr_codes.style` holds, field for field, and the zero value is the default style rather than a blank one — a style row that has never been written renders exactly as a link with no row at all.

func Drawn added in v0.3.0

func Drawn(content string, st Style) (Style, int)

Drawn is the style a code is actually drawn at and the size in pixels it comes out at, in one pass over the encoder (D184).

**The style differs from the stored one in exactly one field**, and only since D184: Style.Level comes back as the level the picture is drawn at, which is the rule's answer rather than the floor the row holds. That is the level a `GET` has to report. Everything else is the caller's own.

One call rather than LevelFor and OutputSize beside each other, because the two answers come off the same matrix and asking twice encodes the content twice. A size of 0 is content that cannot be encoded, on link.QROutputSize's reasoning.

func (s Style) ForLogo() Style

ForLogo is the style a code carrying a logo is drawn at: this one, at level H.

**Forced, not defaulted, and forced in two places on purpose.** The service writes H into the row when a logo is set so that a `GET` reports what will be drawn (D141), and the renderer forces it again so the geometric claim above holds for *any* row — including one written before this milestone, or by hand. The two cannot disagree, because the second is what draws the picture.

The style must already be normalized; this changes one field of a style that has been through Style.Normalize and adds nothing that needs checking.

func (Style) Normalize

func (s Style) Normalize() (Style, []FieldError)

Normalize fills in the defaults and returns the field errors for anything it cannot. It is the only way a Style reaches the renderer, so every colour in an SVG this package emits has been through the parser below.

**Style.Level is the one field it does not fill in**, since D184: the level is a rule rather than a constant, and the rule needs the payload this function has never seen. Encode resolves it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL