Repository navigation
Releases
2.0.0-preview.8
Summary
FeatherQR now reads every code in an image, of any symbology, in one call. It writes and reads binary payloads with ECI, and the data types report their ECC level and mask. Micro QR and rMQR gain the multi-symbol reads Standard QR already had. The net8.0 target is removed (.NET 8 and 9 reaching end of support on 2026-11-10).
Key changes
-
Read every symbol of any symbology at once.
SymbolDecoderreads Standard QR, Micro QR and rMQR codes from one image, and each result carriesSymbology,TextandCorners. Micro QR and rMQR also gainDecodeSymbolsandDecodeImageSymbols.using FeatherQR; using FeatherQR.SkiaSharp; await using var image = File.OpenRead("label.png"); foreach (var symbol in await SymbolDecoder.DecodeSymbolsAsync(image)) { var corner = symbol.Corners.TopLeft; Console.WriteLine($"{symbol.Symbology}: {symbol.Text} at ({corner.X}, {corner.Y})"); }
-
Binary payloads and ECI. The generators take
ReadOnlySpan<byte>, andTryDecodeBytes/TryDecodeImageBytesreturn the bytes with no character set applied. Standard QR and rMQR take an ECI assignment, which comes back asEciAssignment. The data types reportEccLevel, and all but rMQR also reportMaskPattern.using FeatherQR; byte[] payload = ...; var qr = QRCodeGenerator.Create(payload, QREccLevel.M); // no ECI header var tagged = QRCodeGenerator.Create(payload, QREccLevel.M, eciAssignment: 899); // declared as binary data if (QRCodeDecoder.TryDecodeBytes(tagged, out var bytes, out var info)) { Console.WriteLine($"{bytes.Length} bytes, ECI {info.EciAssignment}"); // ECI 899 } Console.WriteLine($"level {qr.EccLevel}, mask {qr.MaskPattern}");
Breaking changes
See the migration guide for details.
- No net8.0 build. Apps on .NET 8 and 9 now use the netstandard2.1 build. It has the same API and output but no hardware intrinsics. Move to .NET 10 to get the SIMD paths back.
- The empty-matrix constructors are no longer public. This covers
new QRCodeData(int, int),new MicroQRCodeData(MicroQRVersion, int)andnew RmQRCodeData(RmQRVersion, int). The restore constructors now throwInvalidDataExceptionunless the format information is one a generator writes. QRCodeImageBuilderrejectsWithErrorCorrectionandWithEciModeon a builder made fromQRCodeData. They used to be ignored silently. Now they throwInvalidOperationException.- A width or height below one throws
ArgumentOutOfRangeExceptionnaming that parameter, inTryDecodeImageandDecodeImageSymbols. It used to be anArgumentExceptionnamingluminance. - Empty content written as
default,[]or anullliteral is ambiguous (CS0121) before C# 13. This is because of the new bytes overloads. On Micro QR it is ambiguous even with options. Pass""orReadOnlySpan<byte>.Emptyto choose one.
What's Changed
- feat: add microqr, rmqr symbols reader like StandardQR by @guitarrapc in #484
- feat: Improve rMQR & MicroQR decoder by @guitarrapc in #485
- feat: support Mixed QR Image decoder by @guitarrapc in #486
- ci: speed up ci test by @guitarrapc in #487
- fix: prevent invalid QR data and Playground stability by @guitarrapc in #488
- fix: remove code warnings by @guitarrapc in #489
- feat: public EccLevel, MaskPattern on QRCodeData, throw WithErrorCorrection/WithEciMode when data null by @guitarrapc in #490
- feat: add Binary Encode/Decode by @guitarrapc in #492
- chore(deps): bump the dependencies group with 3 updates by @dependabot[bot] in #491
- feat: add Binary Encode for Micro QR, rank bytes overloads below text by @guitarrapc in #493
- chore: add byte encode/decode to playground by @guitarrapc in #494
- chore: drop net8.0 as EOL on Nov, 2026 by @guitarrapc in #495
Full Changelog: 2.0.0-preview.7...2.0.0-preview.8
2.0.0-preview.7
Summary
This preview adds async stream I/O to FeatherQR.SkiaSharp, reads an encoded image straight from a Stream, and reads every QR code in an image in one call. Standard QR now throws instead of silently rewriting text that ISO-8859-1 cannot hold. Small-version encodes are faster on ARM64, and so is the scalar path on every platform.
Key changes
- Async writes. Every method that writes an image to a
Streamhas an...Asynctwin:SaveToAsyncandSaveToSvgAsyncon the builders, and the staticSavePngAsyncandSaveSvgAsyncon each of the three builders. Under a UISynchronizationContextthe render moves off the caller's thread; an ASP.NET Core request pays no extra hop.
using FeatherQR;
using FeatherQR.SkiaSharp;
// ASP.NET Core: write the PNG into the response body without blocking a thread.
app.MapGet("/qr", async (string text, HttpResponse response, CancellationToken ct) =>
{
response.ContentType = "image/png";
await QRCodeImageBuilder.SavePngAsync(text, response.Body, cancellationToken: ct);
});
// Builder form, same options as before.
await new QRCodeImageBuilder(text).WithSize(512).SaveToAsync(stream, ct);- Stream reads.
Decode(Stream)andDecodeAsync(Stream)on the three image decoders read an encoded PNG, JPEG or WebP and return a result instead ofoutparameters:QRCodeDecodeResult,MicroQRCodeDecodeResultandRmQRCodeDecodeResult, each withText,InfoandSuccess. With C# 14 they are reachable onQRCodeDecoder,MicroQRCodeDecoderandRmQRCodeDecoder; on older language versions call them onQRCodeImageDecoderand its siblings.
// Read an uploaded image (Request.Body, IFormFile.OpenReadStream(), a FileStream ...).
var result = await QRCodeDecoder.DecodeAsync(stream, ct);
if (result.Success)
Console.WriteLine($"{result.Text} (version {result.Info.Version})");- Every QR code in an image.
DecodeSymbolsreturns each Standard QR code found, once, with its text and corners. From anSKBitmapor a stream inFeatherQR.SkiaSharp, and from luminance in the core asDecodeImageSymbols, in an array form and a span form that allocates only the texts. On a photograph of a sheet of 60 codes it reads all 60 in one call.
// From a bitmap or a stream.
QRCodeDecodeResult[] codes = QRCodeDecoder.DecodeSymbols(bitmap);
codes = await QRCodeDecoder.DecodeSymbolsAsync(stream, ct);
foreach (var code in codes)
Console.WriteLine($"{code.Text} at {code.Info.Corners.TopLeft}");
// From grayscale pixels, into a buffer you own: returns how many were written.
Span<QRCodeDecodeResult> room = new QRCodeDecodeResult[8];
int count = QRCodeDecoder.DecodeImageSymbols(luminance, width, height, room);- Read limits. A stream read refuses an image over 64 MiB of encoded bytes or 16,777,216 pixels (4,096 × 4,096) by its header, before any pixel is allocated, with an
InvalidDataExceptionthat names the limit. PassImageStreamReadLimitsto raise or lower them.
var limits = new ImageStreamReadLimits(maxBytes: 128 * 1024 * 1024, maxPixels: 48_000_000);
var result = await QRCodeDecoder.DecodeAsync(stream, limits, ct);- Performance. On ARM64, mask selection for Standard QR versions 1-11 uses the 128-bit lane-per-pattern scorer, so small-version encodes on Apple silicon are much closer to x64. The scalar path (hardware intrinsics off, .NET Framework) encodes about 15 % faster, and UTF-8 handling allocates less on every target.
- Playground. The decode panel can read every QR code in an image and outline each one, and the generator has a Photograph effect to preview a printed-and-photographed symbol.
Breaking change
EciMode.Iso8859_1over text it cannot hold throws.QRCodeGenerator.Create,CreateStructuredAppendandTryGetRequiredBufferSizethrowArgumentException(namingeciMode) when the text has a character above U+00FF, asRmQRCodeGeneratoralready did. Earlier versions wrote a look-alike (žasz,“as") or?on .NET 8+, and the low byte on netstandard, so the symbol read back as different text. Text that ISO-8859-1 holds is written as before. UseEciMode.Utf8, orEciMode.Defaultto let the library choose. Details in the migration guide.
What's Changed
- chore: add wider encode corpus benchmarks by @guitarrapc in #479
- feat: Improve macOS ARM64 by discard NEON but use Vector128 optimized implementation by @guitarrapc in #480
- feat: Improve scalar mask scoring and UTF-8 handling, and reject incompatible ISO-8859-1 input. by @guitarrapc in #481
- feat: Async API for GUI/Web stream I/O by @guitarrapc in #483
Full Changelog: 2.0.0-preview.6...2.0.0-preview.7
2.0.0-preview.6
Summary
This preview makes Optimal segmentation the default, adds FNC1 to Standard QR and rMQR, and introduces a new package, FeatherQR.Gs1, for GS1 QR Code and GS1 Digital Link data. Standard QR also reads camera photographs far more reliably. The generator options changed shape, so update FeatherQR and FeatherQR.SkiaSharp together.
Key changes
Optimalsegmentation is the default. Mixed content such as URLs with numeric tails, order numbers and labels now gets the smallest symbol without opting in.Optimalencoding was made faster first.
using FeatherQR;
using FeatherQR.SkiaSharp;
var url = "https://example.com/item?id=123456789012345678901234567890";
// Default: Optimal splits the text into Byte + Numeric runs.
var optimal = QRCodeGenerator.Create(url, QREccLevel.M); // version 3
// Single: one segment in one mode, as in 1.x and earlier previews.
var single = QRCodeGenerator.Create(url, QREccLevel.M,
new QRCodeGeneratorOptions { Segmentation = QRSegmentation.Single }); // version 4
// Options are settable now, so you can also assign after construction.
var options = new QRCodeGeneratorOptions();
options.Segmentation = QRSegmentation.Single;
// Image builders take the same setting.
var png = new QRCodeImageBuilder(url)
.WithSegmentation(QRSegmentation.Single)
.ToByteArray();
// RmQRCodeGeneratorOptions and MicroQRCodeGeneratorOptions work the same way, with RmQRSegmentation and MicroQRSegmentation.- FNC1 encode and decode for Standard QR and rMQR, in first position (GS1) and second position (AIM application indicator). Set it with the new
Fnc1option (Fnc1Mode.FirstPositionorFnc1Mode.SecondPosition(...)). Decoders report it onQRCodeDecodeInfo.Fnc1andRmQRCodeDecodeInfo.Fnc1. Output without FNC1 is unchanged. - New package:
FeatherQR.Gs1.QRCodeGenerator.CreateGs1andCreateGs1DigitalLink, plus the rMQR equivalents, write a GS1 QR Code or a Digital Link URI from(AI)text or an element string. Invalid data throwsGs1FormatException, which reports where the problem is.Gs1ElementStringvalidates element strings and converts between element strings,(AI)text and Digital Link URIs.TryReadturns decoded text into(AI)text. The span forms allocate nothing.- The AI table is generated from the GS1 Barcode Syntax Dictionary, release 2026-01-27 (Apache-2.0).
- The generator methods are C# 14 extension members. On C# 12 and 13, call them on
Gs1QRCodeGeneratorandGs1RmQRCodeGenerator.
dotnet add package FeatherQR.Gs1 --prerelease
using FeatherQR;
using FeatherQR.Gs1;
using FeatherQR.SkiaSharp;
// GS1 QR Code: an element string behind FNC1 in first position.
var gs1 = QRCodeGenerator.CreateGs1("(01)09520123456788(17)251231(10)ABC123", QREccLevel.M);
File.WriteAllBytes("label.png", new QRCodeImageBuilder(gs1).ToByteArray());
// GS1 Digital Link: https://id.gs1.org/01/09520123456788/10/ABC123?17=251231
var link = QRCodeGenerator.CreateGs1DigitalLink("(01)09520123456788(17)251231(10)ABC123", QREccLevel.M);
// Your own domain instead of id.gs1.org: https://brand.example/01/09520123456788
var branded = QRCodeGenerator.CreateGs1DigitalLink("(01)09520123456788", QREccLevel.M, stem: "https://brand.example");
// Invalid data throws Gs1FormatException, which says what is wrong and where.
try
{
QRCodeGenerator.CreateGs1("(01)09520123456789", QREccLevel.M);
}
catch (Gs1FormatException ex)
{
Console.WriteLine(ex.Message); // InvalidCheckDigit at 17 (1 character): the check digit is wrong.
}- Much better photo decoding for Standard QR. On BoofCV's 536 test photographs the decoder now reads 457, up from 322. zxing-cpp reads 392 of them. Photographs also decode faster on x64.
- Shift_JIS text decodes. Byte segments under ECI 20, or with no ECI header and Shift_JIS bytes, now return Japanese text.
- Playground supports GS1 QR Code, GS1 Digital Link and AIM formats, and shows the FNC1 mode and GS1 data when decoding.
Breaking changes
See the migration guide for details and code samples.
Optimalis the default, and the enum values swapped. InQRSegmentation,MicroQRSegmentationandRmQRSegmentation,Optimalis now0andSingleis1. A call that passes no segmentation can produce a smaller symbol with a different bit stream, rMQR can pick different dimensions, and content that used to overflow can now fit. An integer stored from an earlier release reads as the other value. To keep the old output, setSegmentation = QRSegmentation.Singleor call.WithSegmentation(QRSegmentation.Single)on an image builder.- Generator options use
setinstead ofinit.QRCodeGeneratorOptions,MicroQRCodeGeneratorOptionsandRmQRCodeGeneratorOptionsare now mutablerecord structs. The constructors added in preview.3 are gone, so writenew QRCodeGeneratorOptions { QuietZoneSize = 2 }. This is binary breaking: an assembly compiled against an earlier preview throwsMissingMethodException. Recompile, and updateFeatherQRandFeatherQR.SkiaSharptogether. - Some symbols decode differently. FNC1 symbols now decode, where Standard QR returned
UnsupportedContentand rMQR returnedInvalidBitstream. Byte segments under ECI 20 decode instead of returningUnsupportedContent. Byte segments without an ECI header can now read as Shift_JIS, so a few ISO-8859-1 and Windows-1252 texts read differently. - Micro QR writes UTF-8 for a few ISO-8859-1 texts whose bytes would otherwise read back as different text, such as
é. These texts can need a larger version or no longer fit M4.
What's Changed
- feat(Breaking): change GeneratorOptions from init to set by @guitarrapc in #462
- feat: add FNC1 decode by @guitarrapc in #463
- feat: add FeatherQR.Gs1 to handle data layer fo GS1 Q!R Code/DigitalLink by @guitarrapc in #465
- feat: improve photo decode accuracy by @guitarrapc in #466
- chore: update comments by @guitarrapc in #467
- fix: dup calculation in PlaceFunctionModulesReference by @guitarrapc in #468
- feat: add FNC1 Encode by @guitarrapc in #469
- feat: fnc1 interop sweep by @guitarrapc in #470
- Takes back the two costs the photo-decode stages by @guitarrapc in #471
- feat: read every Standard QR symbol of a photograph, and decode photographs faster by @guitarrapc in #472
- feat: add GS1 encoding entrypoint by @guitarrapc in #473
- feat: add GS1 to the Playground, address adversary review by @guitarrapc in #474
- feat: Make GS1 ToHri/ToDigitalLink string forms one pass by @guitarrapc in #475
- feat: Improve Optimal encoding by @guitarrapc in #476
- feat: Improve Optimal segmentation encoding performance by @guitarrapc in #477
- feat: Make Optimal default by @guitarrapc in #478
Full Changelog: 2.0.0-preview.5...2.0.0-preview.6
2.0.0-preview.5
Summary
This preview makes Standard QR encoding about three times faster from version 19 up, and 4 to 7.5 times faster on .NET Framework. Every symbol is byte-identical to preview.4, and the public API is unchanged. Standard QR now checks its arguments up front, as Micro QR and rMQR already did.
Key changes
Faster Standard QR encoding. Mask selection, which took most of an encode, was rewritten. Payload writing, Reed-Solomon and the quiet zone also got faster. Against preview.4:
- .NET 10: 2.7 to 3.4 times faster from version 19 up, and 1.1 to 1.3 times at versions 1 to 10.
- x64 without AVX, the 128-bit path a default NativeAOT publish takes: 1.7 to 2.5 times from version 19 up.
- .NET Framework 4.8: 4 to 7.5 times from version 19 up.
- Micro QR and rMQR encoding: 1.1 to 1.25 times.
Other improvements
- Micro QR matrix decoding no longer branches on each module. M2 and M3 symbols that change from call to call decode 2.3 to 2.8 times faster, and a NativeAOT build now runs close to the JIT (Apple M2).
CreateStructuredAppendno longer slows down on text with long runs of U+FEFF. A 15,000-character set with runs of 200 takes 1.2 ms instead of 2.1 ms.- Image decoding no longer takes most cross-shaped marks for finder patterns. Read results on our test sets are unchanged.
- The README now covers NativeAOT on x64. A default publish runs the 128-bit paths and can take up to twice as long as the JIT. Targeting
x86-64-v3brings it close to JIT speed.
Breaking changes
- Standard QR checks its arguments up front.
QRCodeGenerator.Create,CreateStructuredAppendandTryGetRequiredBufferSizethrowArgumentOutOfRangeExceptionfor an undefinedQREccLevelorEciMode, or for a quiet zone above 10,000 modules. Before, these failed deep inside, wrote an unreadable symbol or overflowed. TheQRCodeDataconstructors apply the same bound, and also refuse a negative quiet zone or a version outside 1 to 40. See the migration guide.
What's Changed
- fix: validation and allocation before throw by @guitarrapc in #446
- feat: Improve Large QR Ecc table lookup by linear search to lookups and stack buffers by @guitarrapc in #447
- feat: Improve Mask evaluation penalty 3 by @guitarrapc in #448
- feat: Improve encoding speed when pinning mask by @guitarrapc in #450
- chore: cross benchmark and improve microqr codeword extraction by @guitarrapc in #449
- feat: Improve Encode pipeline by remove duplicated analysis by @guitarrapc in #451
- feat: Module Placer Mask Tranposed over v12 with vectorize row/col rules by @guitarrapc in #452
- chore: add benchmark ci by @guitarrapc in #454
- feat: Improve Payload writers by @guitarrapc in #455
- feat: Improve StandardQR Encode (no Span) with use Vector pack by @guitarrapc in #457
- chore(deps): bump golang.org/x/text from 0.3.7 to 0.3.8 in /tools/CrossLanguageBenchmark/go in the go_modules group across 1 directory by @dependabot[bot] in #456
- feat: Improve RS by handling multiple blocks at once, code by @guitarrapc in #459
- feaet: improve scalar mask selection for .NET Framework by @guitarrapc in #458
- feat: Improve Quietzone gaps clear line over 16 bytes by @guitarrapc in #460
- fix: the lane walk's exit moves with the waiting lanes by @guitarrapc in #461
Full Changelog: 2.0.0-preview.4...2.0.0-preview.5
2.0.0-preview.4
Summary
This preview adds Kanji-mode encoding to all three symbologies through a new AllowKanji option, off by default. It also makes encoding and decoding faster in builds that cannot use AVX2, such as a default NativeAOT publish and Blazor WebAssembly.
Coming from 1.x? The migration guide covers every 2.0.0 preview.
Key changes
Kanji mode for Japanese text
Standard QR, Micro QR and rMQR can now write Japanese text in Kanji mode: 13 bits a character and no ECI header, where UTF-8 takes 24 bits a character plus an ECI header. A symbol holds about 1.85 times as many characters, so the same text often fits a smaller version.
var kanji = new QRCodeGeneratorOptions { AllowKanji = true };
QRCodeGenerator.Create("日本語のテキスト", QREccLevel.M); // UTF-8, version 2
QRCodeGenerator.Create("日本語のテキスト", QREccLevel.M, kanji); // Kanji, version 1
// Text with ASCII needs Optimal, which mixes Kanji and ASCII runs when that gives a smaller symbol
QRCodeGenerator.Create("東京タワー333m", QREccLevel.M, kanji with { Segmentation = QRSegmentation.Optimal }); // version 1
MicroQRCodeGenerator.Create("こんにちは", MicroQREccLevel.L, new MicroQRCodeGeneratorOptions { AllowKanji = true }); // M3 instead of M4- Off by default, and without it the output is byte-identical to preview.3. Not every reader handles Kanji mode: in our tests the iPhone Camera read it, but the built-in QR scanner and Google Lens on Android 17 did not. Turn it on when you know your readers or need the capacity.
- Text is written in Kanji mode only when every character is in JIS X 0208. One character outside it, such as an emoji, ① or halfwidth katakana, keeps the whole text in UTF-8. So does an explicit
EciModeorUtf8Bom. TryGetRequiredBufferSizeandCreateStructuredAppendfollow the option.
Faster on NativeAOT and WebAssembly
A default NativeAOT publish on x64 cannot use AVX2, and WebAssembly had little SIMD code, so much of the library ran scalar there. Those builds, and x64 CPUs without AVX, now have 128-bit SIMD paths for encoding and decoding. Decoding gains the most. No code change is needed.
Image decoding
- Standard QR, Micro QR and rMQR now share one image decode pipeline. Read results across our synthetic and real-image test sets are unchanged from preview.3.
- Retrying with a larger buffer is cheap for Micro QR and rMQR images. When the destination is too short, the decoder now stops at the symbol it read and reports
DestinationTooSmallwith that symbol's version, where it used to keep searching the image.
Span<char> buffer = stackalloc char[64];
if (!RmQRCodeDecoder.TryDecodeImage(luminance, width, height, buffer, out var written, out var info)
&& info.Status == DecodeStatus.DestinationTooSmall)
{
buffer = new char[RmQRCodeDecoder.GetMaxDecodedLength(info.Version)];
RmQRCodeDecoder.TryDecodeImage(luminance, width, height, buffer, out written, out info);
}Breaking changes
- A failed matrix decode reports no characters written. The module-matrix
TryDecodespan overloads of all three decoders now setcharsWrittento 0 whenever they returnfalse, as the image overloads already did. Before, a symbol that failed partway could report the characters read before the failure. See the migration guide.
What's Changed
- chore(deps): bump the dependencies group with 2 updates by @dependabot[bot] in #441
- feat: Share the image decode pipeline across Standard QR, Micro QR and rMQR by @guitarrapc in #442
- feat: add Kanji encoding support by @guitarrapc in #443
- feat: Shared decode pipeline for MicroQR and rMQR by @guitarrapc in #444
- feat: add 128bit SIMD fallback for NativeAOT default, WASM and non-Avx CPU by @guitarrapc in #445
Full Changelog: 2.0.0-preview.3...2.0.0-preview.4
2.0.0-preview.3
Summary
This preview finishes the 2.0.0 API cleanup. Every public name follows one naming rule, the members deprecated in 1.2.0 are gone, and results and options are immutable. It also adds Structured Append encoding and decoding for Standard QR, reports where a decoded symbol sits in the image, and reads many more real-world captures. The next preview adds Kanji-mode encoding.
Coming from 1.x? The package split and namespace changes shipped in 2.0.0-preview.2. The migration guide covers both previews.
Key changes
Image decoding reads far more real-world captures
All three symbologies now read images that failed before: crisp symbols down to one pixel per module, anti-aliased or blurred edges, uneven lighting, and strong perspective or two-axis tilt. Standard QR also reads bowed prints and finders printed too thin or too thick, and Micro QR reports fewer false reads. Finder search and thresholding are faster on both x64 and ARM64.
Structured Append (Standard QR)
Standard QR now both creates and reads Structured Append sets, where one message is split across up to 16 symbols.
QRCodeGenerator.CreateStructuredAppend splits text that does not fit in one symbol into symbols of the same version. The version range you allow decides how many symbols it uses. On decode, each symbol reports its index, count and parity through QRCodeDecodeInfo.StructuredAppend instead of failing with UnsupportedContent.
QRCodeData[] set = QRCodeGenerator.CreateStructuredAppend(longText, QREccLevel.M,
new QRCodeGeneratorOptions { Version = QRVersionRange.AtMost(10) });Symbol corners in the decode result
For a successful image decode, the decode info of all three symbologies reports Corners: the four outer corners of the symbol in image pixel coordinates. They are named in the symbol's own frame, so they also tell you its rotation and whether the capture is mirrored.
Breaking changes
- API renaming. A prefix names the symbology (
QR,MicroQR,RmQR), and no prefix means all three. The migration guide has a replacement script.
| Before | After |
|---|---|
ECCLevel |
QREccLevel |
QRCodeSegmentation |
QRSegmentation |
QRCodeVersionRange |
QRVersionRange |
QRCodeDecodeStatus |
DecodeStatus |
QRCodeGenerator.CreateQrCode |
QRCodeGenerator.Create |
MicroQRCodeGenerator.CreateMicroQRCode |
MicroQRCodeGenerator.Create |
RmQRCodeGenerator.CreateRmQRCode |
RmQRCodeGenerator.Create |
QRCodeCalculatedSize.QrSize, MicroQRCodeCalculatedSize.QrSize |
.Size |
QRCodeGeneratorOptions.Utf8BOM |
.Utf8Bom |
QRCodeRenderer |
SymbolRenderer |
QRCodeImageBuilderBase<TSelf> |
SymbolImageBuilderBase<TSelf> |
QRCodeExtensions |
SKCanvasExtensions |
- Recompile if you call an options constructor. The constructors of
QRCodeGeneratorOptions,MicroQRCodeGeneratorOptionsandRmQRCodeGeneratorOptionsgain an optionalallowKanjiparameter. Source stays compatible, but binaries built against preview.3 that call them need a rebuild. Object initializers, the form the docs use, are not affected. - Removals. Generators take an options struct only,
GetRequiredBufferSizeis replaced byTryGetRequiredBufferSize, and thestringoverloads are gone. Astringstill converts toReadOnlySpan<char>, except on netstandard2.0 below C# 14. - Immutable, sealed types.
QRCodeCalculatedSizeis built by the library,IconDatais init-only, andGradientOptionscompares by value. The data types and image builders are sealed. - Rendering can change existing images. Module styling no longer reaches the finder patterns, a non-square canvas or render area fits the symbol instead of stretching it, padding takes the background colour instead of transparency, and custom
FinderPatternShapes draw only the dark modules. - Colours are values.
WithColorstakes two required colours, with newWithCodeColor,WithBackgroundColorandWithClearColorsetters.SymbolRenderer.RendertakesSKColorinstead ofSKColor?. - Decode behavior. Structured Append symbols decode instead of returning
UnsupportedContent.
What's Changed
- chore: public api check for future library split by @guitarrapc in #389
- chore: Split Core packages and rename library by @guitarrapc in #394
- chore: replace Old SkiaSharp.QrCode to FeatherQR by @guitarrapc in #395
- feat(breaking): remove old API and rename Public API by @guitarrapc in #396
- Terminal feat: report symbol corners from image decode by @guitarrapc in #397
- fix: Keep finder patterns readable when modules are styled by @guitarrapc in #399
- feat(breaking): fit the symbol to the canvas instead of stretching it by @guitarrapc in #400
- fix: stop stretch with SymbolRenderer.Render, but change to keep square and fit it. by @guitarrapc in #401
- feat: WithColor/WithModuleShape/WithFinderPatternShape to be not optional, added WithCodeColorWithBackgroundColor, WithClearColor by @guitarrapc in #402
- fix: svg rendering lost finder pattern when alpha has been set by @guitarrapc in #403
- feat: Stop passing null for Render method colour value by @guitarrapc in #404
- chore: enable deterministics build by @guitarrapc in #405
- chore: add missing overload tests by @guitarrapc in #406
- fix: refuse a non-finite corner radius in the three rounded shapes by @guitarrapc in #408
- fix: builder setter cross effect on tests by @guitarrapc in #407
- feat: add StructuredAppend Decoder/Encoder by @guitarrapc in #409
- Fixes three image-decoder defects by @guitarrapc in #410
- feat: Improve Structured Append NEON handling by @guitarrapc in #411
- feat: increase image decode accuracy for clean images by @guitarrapc in #412
- fix: Cannot decode StandardQR v19-H195px / v27L 255px pattern by @guitarrapc in #413
- feat: increase image decode accuracy for anti-alias & 1px/module image, crisp renders at fractional low density by @guitarrapc in #414
- feat: Improve decode performance for Structured Append and also MicroQR/rMQR by @guitarrapc in #415
- feat: Improve Otsu image decode histogram with 256bit register mask count & improve scalar path by @guitarrapc in #416
- feat: improve inversed-polarityretry speed by reuse otsu histogram by @guitarrapc in #418
- chore: add sweap fixture images of Realm zxing cpp samples by @guitarrapc in #417
- feat: improve decoding of large Standard QR symbols by vectorizing the piecewise mesh sampler by @guitarrapc in #420
- chore: add sweap fixture images of Realm zxing cpp samples by @guitarrapc in #421
- feat: Improve Decode finder for AMD64/ARM64 & Otsu hisogram for ARM64 by @guitarrapc in #422
- feat: improve accuracy for the threshold under uneven lighting by @guitarrapc in #423
- feat: Improve Micro QR and rMQR failure path by @guitarrapc in #424
- feat: Improve Micro QR false results by @guitarrapc in #425
- feat: Improve decode accuracy for grid position difference by @guitarrapc in #426
- feat: Improve decode accuracy for distorted finder frame by @guitarrapc in #427
- feat: improve over 30% distorted images failure. Expand column approval to perspective view by @guitarrapc in #428
- feat: Improve decoding two-axis perspective images, a tilt image mis-reading by @guitarrapc in #429
- refactor: remove dead code and wrong comments by @guitarrapc in #430
- chore: rMQR keep the anisotropic grid, record the perspective gate measurement, correct the non-square edge by @guitarrapc in #431
- feat: change StandardQR decoder order from always mesh to use mesh only when right-bottom alignment couldt't anchored by @guitarrapc in #432
- chore: add tests by @guitarrapc in #433
- feat: Improve decoding Thin or thick rings StandardQR by @guitarrapc in #434
- feat: Improve strongly keystoned rMQR by tracing its timing-pattern perimeter by @guitarrapc in #436
- chore: analyse AOT SIMD capability by @guitarrapc in #435
- feat: Improve decoding for the four interior nodes are searched wh...
2.0.0-preview.2
Summary
The repository is now guitarrapc/FeatherQR, matching the package name. The library code is unchanged from 2.0.0-preview.1.
Key changes
- Package metadata (project and repository URLs) points to the new repository.
- The Playground moved to https://guitarrapc.github.io/FeatherQR/.
Breaking changes
None since 2.0.0-preview.1. Coming from 1.x, see the 2.0.0-preview.1 notes and the migration guide.
What's Changed
- chore: public api check for future library split by @guitarrapc in #389
- chore: Split Core packages and rename library by @guitarrapc in #394
- chore: replace Old SkiaSharp.QrCode to FeatherQR by @guitarrapc in #395
Full Changelog: 1.2.0...2.0.0-preview.2
2.0.0-preview.1
Summary
The first 2.0.0 preview splits the library into a dependency-free core and a SkiaSharp rendering package, and renames it to FeatherQR. Encoding and decoding behave exactly as in 1.2.0, and type names are unchanged; the work is in using lines and, if you want it, the package reference. The API cleanup announced in 1.2.0 comes in a later preview.
See the migration guide for details.
Key changes
Three packages, with a core that needs no SkiaSharp
| Package | Contents |
|---|---|
FeatherQR |
Generators, decoders, data types, options. No dependencies on .NET 8+ |
FeatherQR.SkiaSharp |
Image builders, renderer, SKCanvas extensions, icons and shapes, SKBitmap decoding |
SkiaSharp.QrCode |
No code. A metapackage that resolves FeatherQR.SkiaSharp |
A project that only needs module matrices, or decodes from another image library through the luminance overloads, can reference FeatherQR alone and no longer carries the SkiaSharp native library.
The SkiaSharp.QrCode install line keeps working
An existing <PackageReference Include="SkiaSharp.QrCode" /> restores FeatherQR.SkiaSharp and FeatherQR transitively. Switch the reference to FeatherQR.SkiaSharp when convenient. All three packages ship at the same version.
Breaking changes
- Namespaces.
SkiaSharp.QrCode→FeatherQR, andSkiaSharp.QrCode.Image→FeatherQR.SkiaSharp. TryDecode(SKBitmap)moved. The bitmap overloads are now extension members inFeatherQR.SkiaSharp.QRCodeImageDecoder.TryDecode(bitmap, …)works on every language version (likewiseMicroQRCodeImageDecoderandRmQRCodeImageDecoder). The 1.x spellingQRCodeDecoder.TryDecode(bitmap, …)still compiles on C# 14.
What's Changed
- chore: public api check for future library split by @guitarrapc in #389
- chore: Split Core packages and rename library by @guitarrapc in #394
Full Changelog: 1.2.0...2.0.0-preview.1
1.2.0
Overview
This release adds rMQR (ISO/IEC 23941) as a third symbology, teaches every decoder to read Kanji mode segments, and introduces an options struct on every generator so new settings no longer need new parameter lists. Encoding and decoding are faster across all symbologies on both x64 (AVX2 / GFNI) and ARM64 (NEON), and the package now ships XML documentation.
Everything is source compatible with v1.1.1. Two APIs are marked [Obsolete] and will be removed in 2.0.0.
Highlights
rMQR support
rMQR is a rectangular symbol that fits banners, cable labels, and other narrow spaces where a square QR code wastes room. All 32 versions (R7x43 - R17x139) are supported for generation and decoding, including image decoding.
using SkiaSharp.QrCode;
using SkiaSharp.QrCode.Image;
// One-liner PNG
var pngBytes = RmQRCodeImageBuilder.GetPngBytes("https://example.com/r/12345", RmQREccLevel.M, size: 512);
// Fix the height and let the width be selected
var data = RmQRCodeGenerator.CreateRmQRCode("https://example.com/r/12345", RmQREccLevel.M, new RmQRCodeGeneratorOptions
{
Height = RmQRHeight.H9,
FitStrategy = RmQRFitStrategy.MinimizeArea,
});
// Decode back
if (RmQRCodeDecoder.TryDecode(data, out var text, out var info))
Console.WriteLine($"{text} ({info.Version})");Mixed-mode segmentation
By default the whole content is encoded in a single mode, so one lowercase letter pushes an otherwise numeric payload into Byte mode. Opt into segmentation and the content is split into the Numeric / Alphanumeric / Byte runs that cost the fewest bits, which often drops the symbol by a version or more. The result is never larger than the single-mode symbol, and identical when splitting would not help.
var standard = QRCodeGenerator.CreateQrCode(
"https://example.com/item?id=123456789012345678901234567890",
ECCLevel.M,
new QRCodeGeneratorOptions { Segmentation = QRCodeSegmentation.Optimal }); // version 3 instead of 4
var rmqr = RmQRCodeGenerator.CreateRmQRCode(
"https://example.com/p/1234567890123456",
RmQREccLevel.M,
new RmQRCodeGeneratorOptions { Segmentation = RmQRSegmentation.Optimal }); // R15x43 instead of R11x77Micro QR has MicroQRSegmentation.Optimal as well, respecting each version's available modes.
Generator options
Every generator entry point gained an overload taking an options struct. The parameter list overloads keep their signatures, exceptions, and output; new options are added to the struct from here on. default means "all defaults".
var data = QRCodeGenerator.CreateQrCode("https://example.com", ECCLevel.M, new QRCodeGeneratorOptions
{
Version = new QRCodeVersionRange(10, 20), // inclusive range; an int pins a single version
BoostEccLevel = true, // treat ECC as a floor, raise it within the same symbol size
MaskPattern = 3, // pin a data mask instead of automatic selection
QuietZoneSize = 0,
});- Version ranges (
QRCodeVersionRange,MicroQRVersionRange) express a pinned version, a range, or a lower bound in one setting. A pinned version that cannot hold the content now throws a message naming the version, ECC level, and mode. - ECC boost picks the version for the level you request, then raises the level as far as that version's spare capacity allows without growing the symbol. Recommended when robustness matters more than an exact level, especially with icons. The builder spells it
WithErrorCorrectionBoost(). - Mask pattern pinning reproduces a symbol produced elsewhere byte for byte, and lets you exercise scanners against every pattern.
Kanji mode decoding
All three decoders now read ISO/IEC 18004 Kanji mode segments. Symbols that previously returned UnsupportedContent now decode to text. Encoding is unchanged: the generators still write Japanese text as UTF-8 in Byte mode.
if (QRCodeDecoder.TryDecode(bitmap, out var text, out var info))
Console.WriteLine(text);
else if (info.Status == QRCodeDecodeStatus.UnmappedCharacter)
// A Kanji segment used a CP932-only character (NEC row 13); route it to a CP932-capable reader.
Console.WriteLine("CP932-only content");The mapping is JIS X 0208, not CP932. The new UnmappedCharacter status is deliberately distinct from UnsupportedContent, so those symbols can be routed elsewhere instead of being silently rewritten.
Module rectangles for vector output
GetModuleRectangles returns the dark modules as merged, disjoint rectangles in module coordinates, typically halving the element count compared to one rectangle per module. Useful for SVG paths and draw-call based graphics APIs.
foreach (var r in qrData.GetModuleRectangles())
sb.Append(CultureInfo.InvariantCulture, $"M{r.X},{r.Y}h{r.Width}v{r.Height}h{-r.Width}z");For allocation-free use, size a pooled buffer with GetModuleRectanglesMaxCount() and call TryGetModuleRectangles(Span<ModuleRect>, out int). Available on QRCodeData, MicroQRCodeData, and RmQRCodeData.
Buffer sizing is Try-only
"The content does not fit" is a data-dependent answer, not a defect, so sizing reports it with a bool rather than an exception. TryGetRequiredBufferSize is available on all three generators, and its options parameter is optional.
if (!MicroQRCodeGenerator.TryGetRequiredBufferSize(userInput, MicroQREccLevel.L, out var size))
return "Content does not fit a Micro QR symbol.";Invalid arguments still throw, so a configuration mistake is never reported as "content too long".
Migration
Nothing breaks on upgrade. See the Migration Guide for the deprecations and the Kanji behaviour change.
What's Changed
- [dotnet format] Automated changes by @github-actions[bot] in #356
- chore(deps): bump the dependencies group with 3 updates by @dependabot[bot] in #357
- chore(deps): bump actions/stale from 10.4.0 to 11.0.0 in the dependencies group by @dependabot[bot] in #358
- feat: add rMQR Implementation by @guitarrapc in #359
- feat: Optimizes RmQR performance by @guitarrapc in #360
- feat: Improve StandardQR performance by @guitarrapc in #361
- feat: improve result-object packing and quiet-zone placement by @guitarrapc in #362
- feat: Improve rMQR Auto fit to table scan by @guitarrapc in #363
- [dotnet format] Automated changes by @github-actions[bot] in #364
- feat: improve rMQR image-decode by @guitarrapc in #365
- feat: rMQR Encode ECI support by @guitarrapc in #366
- feat: Improve ARM NEON rMQR and related handling by @guitarrapc in #367
- docs: update benchmarks to latest code base by @guitarrapc in #368
- fix: rmqr decoder correction cap from Full RS strength to Per-block cap read from a 64-row capacity table, by @guitarrapc in #369
- feat: Support rMQR segmentation by @guitarrapc in #371
- feat: add Kanji decode support by @guitarrapc in #372
- chore(deps): bump actions/attest-build-provenance from 4.1.0 to 4.2.2 in the dependencies group by @dependabot[bot] in #374
- feat: add TryGetRequiredBufferSize api by @guitarrapc in #373
- feat: Introduce QRCodeGeneratorOptions by @guitarrapc in #375
- feat: make buffer sizing Try-only, obsolete parameter-list GetRequiredBufferSize by @guitarrapc in #376
- feat: add EccBoost is same version but ecc can be upgraded. by @guitarrapc in #377
- ci: aot analysis by @guitarrapc in #378
- feat: cut off MaskPlacer when it is possible by @guitarrapc in #379
- feat: allow specify MaskPattern to use. (StandardQR / MicroQR) by @guitarrapc in #380
- chore: add negative test for intended 1 module broke, picture approved test by @guitarrapc in #381
- feat: Add GetModuleRectanglesMaxCount to retrieve merged geometries by @guitarrapc in #382
- feat: Mixed-mode segmentation to StandardQR and MicroQR by @guitarrapc in #384
- chore: compression no referenced and should be obsolete by @guitarrapc in #385
- chore: add xml document to package by @guitarrapc in #386
- chore: add docs page in playground by @guitarrapc in #387
- chore: trim xml comments by @guitarrapc in #388
Full Changelog: https://github.com/guitarrapc/SkiaSharp.QrCode/compare/1.1.1.....
1.1.1
What's Changed
- fix: FinderPattern filled by outer ring dark colour when using transparent bg by @guitarrapc in #355
Full Changelog: 1.1.0...1.1.1