# How to Use IronQR with Google Antigravity
A Blazor upload page may build without warnings, yet still fail to process user files correctly. The build process does not indicate if the page lacks interactivity, if large images exceed upload limits, or if images containing multiple passes are only partially processed. These issues only become apparent during real-world testing.
Google Antigravity addresses this challenge. Its `/browser` command selects files in the upload control. IronQR decodes uploaded images server-side using `QrReader.Read`, so the page needs no JavaScript scanner. This guide demonstrates how to build a visitor check-in page using both tools and validates the results by comparing each uploaded file with its expected values and the values displayed on the page.
One note on terminology. IronQR includes a machine learning model for detecting QR codes in challenging images, as detailed in the [scan modes guide](https://ironsoftware.com/qr/csharp/how-to/read-qr-codes-with-scan-modes/). In this guide, 'AI' refers to the coding agent responsible for writing and testing the page.
*as-heading:2(Quickstart)*
!!!--LIBRARY_NUGET_INSTALL_BLOCK--!!!
Open the Blazor project in Antigravity and specify the desired page configuration with `/plan`.
```text
/plan This repo is a .NET 9 Blazor Web App with Interactive Server render
mode. Add a visitor check-in page at /scanner. Staff pick a photo of a visitor
pass with InputFile, the server decodes it with IronQR (NuGet package IronQR,
namespace IronQr) by loading the upload into an AnyBitmap and calling
QrReader.Read, and the page lists every value found. Set
IronQr.License.LicenseKey once in Program.cs, read from IRONQR_LICENSE_KEY in
the environment, and never write the key to a file. Add a NavMenu link.
In the Implementation Plan, state the render mode the page will use and the
largest upload it will accept. Then build until dotnet build is clean.
```
The plan has to state two settings. A Blazor Web App renders a page statically unless the page opts into interactivity, as the [render modes guide](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes) explains. `OpenReadStream` rejects any file over 512,000 bytes unless the call raises that limit, per Microsoft's [Blazor file upload guide](https://learn.microsoft.com/en-us/aspnet/core/blazor/file-uploads), and phone photos run far larger. When you run `agy -p` with `--dangerously-skip-permissions`, every tool call is approved, so review the plan and the diff together.
Give the agent the variable name `IRONQR_LICENSE_KEY`, never the value. An agent that writes files and runs commands can copy a pasted key into a commit or a log. IronQR also reads an `IronQr.LicenseKey` entry from `appsettings.json`, which is easy to commit by accident. The [license key guide](https://ironsoftware.com/qr/csharp/get-started/license-keys/) covers every option.
<div class="hsg-featured-snippet">
<h3>Minimal Workflow (5 steps)</h3>
<ol>
<li><a class="js-modal-open" data-modal-id="trial-license-after-download" href="https://www.nuget.org/packages/IronQR/">Download the C# QR code library from NuGet</a></li>
<li>Start from a Blazor Web App that builds and runs on localhost</li>
<li>Make the scanner page interactive and cap uploads above your largest photo</li>
<li>Have <code>/browser</code> upload every test image and screenshot what the page shows</li>
<li>Compare each displayed value with the expected list</li>
</ol>
</div>
<br class="clear" />
---
## What is Google Antigravity, and how is it different from a chat assistant?
Antigravity runs agents that plan a change, edit the code, use a terminal, and drive a browser, then report back in Artifacts you can review instead of watching each step. Google describes [Antigravity 2.0](https://antigravity.google/docs/overview) as a standalone desktop app for managing agents, and the [CLI](https://antigravity.google/docs/getting-started/) installs as `agy`.
An agent left to pick a package may guess its name, and attackers register plausible names ahead of time and fill them with malware, a practice called *slopsquatting*. Name the package `IronQR` and the namespace `IronQr`.
IronQR's documentation links for agents and a one-prompt acceptance test are in the [AI coding assistants guide](https://ironsoftware.com/qr/csharp/ai-agents/ai/).
## What should you check in the plan, the diff and the Walkthrough?
The Implementation Plan should also say what the page shows for an image with no code and for an image with several.
In the code diff, check how the upload becomes an `AnyBitmap`, and look up every IronQR member the diff uses in the [API reference](https://ironsoftware.com/qr/csharp/object-reference/api/).
For browser tasks, Google says the [Walkthrough](https://antigravity.google/docs/walkthrough) often holds screenshots and recordings of what the agent did. In our CLI run, every task in the conversation rewrote the same `walkthrough.md`, so it held only the latest task's report. Copy any table you want to keep before you send the next prompt.
## How do you test the IronQR upload page in a real browser?
The `/browser` command starts a sandboxed Chrome subagent that opens pages and captures screenshots, per the [slash commands reference](https://antigravity.google/docs/slash-commands). Ask for a table it can fill only by uploading files.
```text
/browser Run the app with dotnet run (the http profile listens on
http://localhost:5181) and open /scanner. Upload each image in TestData/scans
through the page's file input, one at a time, wait until the page shows a
result or a message, and screenshot it. TestData/scans/expected.csv lists the
values each image carries, separated by | when there are several, and an empty
cell means no code. Return a Walkthrough table of file, bytes, expected values,
values the page displayed, and pass or fail. When a row fails, quote the server
log or browser console line that explains it, or say that neither recorded
anything, before you edit anything. Then fix the page, restart the app, and
upload all seven images again. Stop the app at the end.
```
A fix to the result list changes the markup for every image, so only a complete rerun shows that nothing regressed.
[[i:(Open the screenshot for the largest photo first. An oversized upload can fail without a line in the server log, so a report built from logs alone would call that row a pass.)]]
Every [screenshot is saved as an image artifact](https://antigravity.google/docs/screenshots). When one shows a problem, such as a pass number cut short, comment on it rather than describing it in a new prompt.
## What causes missing or wrong QR values?
Most failed rows trace back to a few causes, and naming the likely one in a follow-up task saves the agent a round of trial and error. We put the page from IronQR's [Blazor scanner guide](https://ironsoftware.com/qr/csharp/get-started/blazor-qr-code-scanner/) into a fresh .NET 9 template and uploaded the seven test images through headless Chrome with IronQR 2026.10.1. Every defect below compiled cleanly. With the render mode added, five of seven images showed the right value.
- A Scan button that stays disabled for every image needs the render mode the Quickstart plan states.
- The 48 MP photo (17,955,400 bytes) exceeds the guide's 10,000,000-byte cap and leaves the button disabled with no message. Set a cap above your largest photo and show a message for larger files.
- When an image of two passes shows one value, list every result.
## How do you set up the project and its rules for Antigravity?
Give the agent a project that already builds and runs. Create it with `dotnet new blazor --interactivity Server --framework net9.0`, run it once, and note the URL in `Properties/launchSettings.json`. The template picks a different port for each project, and the `/browser` prompt needs it.
Then delegate the package and key check.
```text
Tell me which IronQR version the project references and paste its
PackageReference line. Then show me where the license key is applied. It has
to be set once at startup from IRONQR_LICENSE_KEY, appsettings.json and
appsettings.Development.json must carry no IronQr.LicenseKey entry, and when
the variable is missing the app should log one startup warning that names the
variable but prints no value. Change no other file, and touch Program.cs
only if that warning is missing.
```
Use the reported version in every later task. Without a key, decoding fails with LicensingException and "Production License Required", so a missing variable fails the first scan.
Once the upload table passes, write the rules down. Antigravity loads `AGENTS.md` and `GEMINI.md` at every folder level up to the workspace root, per its [rules documentation](https://antigravity.google/docs/rules), and `/learn` turns a session's corrections into persistent rules.
```text
/learn Write the rules this session established into a new AGENTS.md at the
repository root (the always-on rules file, not .antigravity/rules.md) under a
"QR check-in page" heading. Cover the render mode the scanner page needs, the
upload cap and the reason for its value, showing every decoded value instead
of the first, keeping IRONQR_LICENSE_KEY out of files, and that no scanner
change is done until the /browser upload table passes for every image in
TestData/scans.
```
Every later task loads that file, so prompts stop repeating the render mode. A task that changes a rule has to change the file too. In our run, `AGENTS.md` kept the 30 MB cap from the Quickstart plan after the upload refactor moved the service to 25 MB.
## How do you decode an upload without a temporary file?
IronQR reads images as `AnyBitmap`, so the page has to turn the browser upload into one. The Blazor scanner guide copies the upload to a temporary file named after the client's file name and opens it from disk. Two desks that upload files with the same name at once can collide, and Microsoft says never to save an upload under a client-supplied name.
`AnyBitmap.FromStream` avoids the file, but [the browser stream behind the upload](https://github.com/dotnet/aspnetcore/blob/release/9.0/src/Components/Web/src/Forms/InputFile/BrowserFileStream.cs) cannot seek and rejects synchronous reads. In IronQR 2026.10.1, a stream with those limits passed straight in throws NotSupportedException. Copying the upload into a `MemoryStream` with `CopyToAsync` before the read works. Microsoft advises against buffering whole uploads in server memory, so the upload cap also caps the buffer.
```text
Open an Implementation Plan Artifact for this refactor before editing. Move
the decoding out of the Razor page into Services/PassScanService.cs,
registered as a singleton. The service takes the IBrowserFile, rejects files
over 25 MB with a message the page shows, copies the upload into a
MemoryStream with CopyToAsync, builds the AnyBitmap from that buffer, reads it
with QrReader.ReadAsync and QrScanMode.Auto, and returns value, QrType and Url
for every result. No temporary files and no client file names on disk. Show
the scan time in milliseconds under the results. Then repeat the /browser
upload table for all seven images.
```
Look for a service along these lines in the diff. We compiled this version against IronQR 2026.10.1, and a page that called it showed the right values for all seven test images.
```csharp
using IronQr;
using IronQr.Enum;
using IronSoftware.Drawing;
using Microsoft.AspNetCore.Components.Forms;
public sealed class PassScanService
{
public const long MaxUploadBytes = 25 * 1024 * 1024;
public async Task<List<QrResult>> ScanAsync(IBrowserFile file)
{
using var buffer = new MemoryStream();
await using (var upload = file.OpenReadStream(MaxUploadBytes))
{
await upload.CopyToAsync(buffer);
}
buffer.Position = 0;
using var image = AnyBitmap.FromStream(buffer);
var results = await new QrReader().ReadAsync(new QrImageInput(image, QrScanMode.Auto));
return results.ToList();
}
}
```
`QrResult.Url` is set only when the value is a valid URI, which suits a desk where some passes carry a link and others a plain pass number.
## Why is the first scan after a restart slow?
The `IronQR` package includes `IronQrDetection.dll`, a 44 MB assembly in 2026.10.1 that is almost entirely one embedded resource, and depends on `Microsoft.ML.OnnxRuntime` 1.20.1 to run the detection model. `QrScanMode.Auto` tries the detector before a basic scan, which runs only when the detector finds nothing.
In three fresh console processes reading a clean image, the first Auto read took 3.1 to 4.7 seconds and later reads took 0.5 to 1.0 seconds. A new reader per read did not bring the delay back, so the cost is per process. The first OnlyBasicScan read, loading the file included, took 0.33 to 0.56 seconds. On Blazor Server, the first visitor after a restart or scale-out waits unless the app loads the detector at startup.
```text
/browser Measure the cold-start cost of the detection model. Restart the app,
upload TestData/scans/pass-clean.png three times on /scanner, and note the
scan time the page shows each time. Then add an IHostedService that reads one
small blank AnyBitmap with QrReader at startup, off the request path, restart,
repeat the three uploads, and put both sets of timings side by side in the
Walkthrough with a screenshot of each first upload.
```
An `IHostedService` warm-up that blocks startup trades a slow first upload for a slow first page load, and the agent's timings show which one it built.
## When should you dispatch more than one agent?
A test agent and a browser agent check different things, the service's results in code and what staff see on the page.
```text
Agent A: create an xUnit project QrScanner.Tests that calls PassScanService
for every row of TestData/scans/expected.csv through a test double of
IBrowserFile, and asserts the exact set of values in any order. Add one test
that a file over the cap is rejected before IronQR reads it. Never compare
QR images byte for byte.
Agent B: start only after Agent A's tests pass against the current code. Use
/browser to upload the same seven images on /scanner, attach a screenshot of
each result to the Walkthrough, and state there that the tests passed first.
```
In our tests, IronQR returned the two passes in a different order under Auto than under OnlyBasicScan, which is why the tests compare sets.
[[i:(In the desktop app, give each agent its own task and compare the two Walkthroughs.)]]
## How do you deploy the scanner to a Linux container?
IronQR 2026.10.1 also depends on `IronQR.Slim` and `IronSoftware.ReaderInternals.Windows`, and `IronQR.Linux` swaps the Windows internals for `IronSoftware.ReaderInternals.Linux`.
```text
This app will ship in a linux-x64 container built from
mcr.microsoft.com/dotnet/aspnet:9.0. Do not change any C#. Make the project
reference IronQR for Windows builds and IronQR.Linux when RuntimeIdentifier is
linux-x64, add a Dockerfile that publishes for linux-x64 and passes
IRONQR_LICENSE_KEY in at run time, then run dotnet publish -r linux-x64
--self-contained false -o publish-linux and list every .so file in that
folder with the NuGet package it came from. Docker is not installed on this
machine, so stop before docker build.
```
The publish folder is the evidence, and you can inspect it on Windows. With the Windows `IronQR` package, a `linux-x64` publish carried the ONNX Runtime libraries but not `ReaderInterop.so`, the Linux native reader. A `PackageReference` conditioned on `RuntimeIdentifier` picked the right package for each build.
Both native libraries link against glibc, and neither package ships a musl build, so an Alpine base image has nothing to load. `IronSoftware.ReaderInternals.Linux` 2026.3.47413 ships only a `linux-x64` build, so ARM64 Linux is outside what `IronQR.Linux` 2026.10.1 covers. If the detector's DLLs miss the output folder, IronQR throws IronQrRuntimesCopyException, and the [runtimes copy exception page](https://ironsoftware.com/qr/csharp/troubleshooting/runtimes-copy-exception/) gives the fix. The [NuGet packages guide](https://ironsoftware.com/qr/csharp/get-started/nuget-packages/) lists every platform package, and `IronQR.Slim` alone runs OnlyBasicScan without the detection model.
## What did the prompts produce in a real run?
The eight prompts above ran in order in one Antigravity CLI conversation, unattended with `--dangerously-skip-permissions`, against a fresh .NET 9 Blazor Web App with Interactive Server render mode. The repository held seven test images with known values, listed in `TestData/scans/expected.csv`. They were a clean screenshot, a 12 MP and a 48 MP phone photo, an image of two passes, a tilted photo in poor light, a pass that carries a URL, and a desk photo with no pass. The key reached the agent only through the environment. After the run, we rebuilt the app in Release, ran the tests, and uploaded every image through headless Chrome ourselves.
Because the Quickstart plan already capped uploads above the 48 MP photo and listed every result, the guide page's two upload defects never appeared.
- The first upload after a restart took 3,315 ms before the warm-up and 901 ms after it.
- In the agent's own upload table, 6 of 7 rows passed, then 7 of 7 once it sorted the two-pass values.
- Our upload check of the final app passed 7 of 7, and the xUnit tests passed 8 of 8.
- The Release build finished with 0 warnings and 0 errors.
- `ReaderInterop.so` in the `linux-x64` publish came to 21,475,192 bytes.
- Both plan-first prompts wrote the plan and changed files in the same turn, and one agent did the work of the two-agent prompt.
- Browser screenshots and the `linux-x64` publish folder were left in the repository.
The hub's [run record](/qr/csharp/ai-agents/ai/#the-run-record) lists the Antigravity version, model and run time, and the machine the run used.
---
## Troubleshooting
- [Blazor QR Code Scanner](https://ironsoftware.com/qr/csharp/get-started/blazor-qr-code-scanner/) for the base page this guide extends
- [Apply a License Key in IronQR](https://ironsoftware.com/qr/csharp/troubleshooting/apply-a-license-key-in-ironqr/) to check that the key is applied before the first scan
- [Read QR Codes with Scan Modes](https://ironsoftware.com/qr/csharp/how-to/read-qr-codes-with-scan-modes/) to choose between Auto and OnlyBasicScan
---
## Questions?
If you have any questions, reach out to [support@ironsoftware.com](mailto:support@ironsoftware.com)
A Blazor upload page may build without warnings, yet still fail to process user files correctly. The build process does not indicate if the page lacks interactivity, if large images exceed upload limits, or if images containing multiple passes are only partially processed. These issues only become apparent during real-world testing.
Google Antigravity addresses this challenge. Its /browser command selects files in the upload control. IronQR decodes uploaded images server-side using QrReader.Read, so the page needs no JavaScript scanner. This guide demonstrates how to build a visitor check-in page using both tools and validates the results by comparing each uploaded file with its expected values and the values displayed on the page.
One note on terminology. IronQR includes a machine learning model for detecting QR codes in challenging images, as detailed in the scan modes guide. In this guide, 'AI' refers to the coding agent responsible for writing and testing the page.
Quickstart
Install with NuGet
PM > Install-Package IronQR
Install-Package IronQR
Install IronQR by running the command above in the NuGet Package Manager Console, or search for the package in the NuGet Package Manager.
Open the Blazor project in Antigravity and specify the desired page configuration with /plan.
/plan This repo is a .NET 9 Blazor Web App with Interactive Server rendermode. Add a visitor check-in page at /scanner. Staff pick a photo of a visitorpass with InputFile, the server decodes it with IronQR (NuGet package IronQR,namespace IronQr) by loading the upload into an AnyBitmap and callingQrReader.Read, and the page lists every value found. SetIronQr.License.LicenseKey once in Program.cs, read from IRONQR_LICENSE_KEY inthe environment, and never write the key to a file. Add a NavMenu link.In the Implementation Plan, state the render mode the page will use and thelargest upload it will accept. Then build until dotnet build is clean.
/plan This repo is a .NET 9 Blazor Web App with Interactive Server render
mode. Add a visitor check-in page at /scanner. Staff pick a photo of a visitor
pass with InputFile, the server decodes it with IronQR (NuGet package IronQR,
namespace IronQr) by loading the upload into an AnyBitmap and calling
QrReader.Read, and the page lists every value found. Set
IronQr.License.LicenseKey once in Program.cs, read from IRONQR_LICENSE_KEY in
the environment, and never write the key to a file. Add a NavMenu link.
In the Implementation Plan, state the render mode the page will use and the
largest upload it will accept. Then build until dotnet build is clean.
Text
The plan has to state two settings. A Blazor Web App renders a page statically unless the page opts into interactivity, as the render modes guide explains. OpenReadStream rejects any file over 512,000 bytes unless the call raises that limit, per Microsoft's Blazor file upload guide, and phone photos run far larger. When you run agy -p with --dangerously-skip-permissions, every tool call is approved, so review the plan and the diff together.
Give the agent the variable name IRONQR_LICENSE_KEY, never the value. An agent that writes files and runs commands can copy a pasted key into a commit or a log. IronQR also reads an IronQr.LicenseKey entry from appsettings.json, which is easy to commit by accident. The license key guide covers every option.
Start from a Blazor Web App that builds and runs on localhost
Make the scanner page interactive and cap uploads above your largest photo
Have /browser upload every test image and screenshot what the page shows
Compare each displayed value with the expected list
What is Google Antigravity, and how is it different from a chat assistant?
Antigravity runs agents that plan a change, edit the code, use a terminal, and drive a browser, then report back in Artifacts you can review instead of watching each step. Google describes Antigravity 2.0 as a standalone desktop app for managing agents, and the CLI installs as agy.
An agent left to pick a package may guess its name, and attackers register plausible names ahead of time and fill them with malware, a practice called slopsquatting. Name the package IronQR and the namespace IronQr.
IronQR's documentation links for agents and a one-prompt acceptance test are in the AI coding assistants guide.
What should you check in the plan, the diff and the Walkthrough?
The Implementation Plan should also say what the page shows for an image with no code and for an image with several.
In the code diff, check how the upload becomes an AnyBitmap, and look up every IronQR member the diff uses in the API reference.
For browser tasks, Google says the Walkthrough often holds screenshots and recordings of what the agent did. In our CLI run, every task in the conversation rewrote the same walkthrough.md, so it held only the latest task's report. Copy any table you want to keep before you send the next prompt.
How do you test the IronQR upload page in a real browser?
The /browser command starts a sandboxed Chrome subagent that opens pages and captures screenshots, per the slash commands reference. Ask for a table it can fill only by uploading files.
/browser Run the app with dotnet run (the http profile listens onhttp://localhost:5181) and open /scanner. Upload each image in TestData/scansthrough the page's file input, one at a time, wait until the page shows aresult or a message, and screenshot it. TestData/scans/expected.csv lists thevalues each image carries, separated by | when there are several, and an emptycell means no code. Return a Walkthrough table of file, bytes, expected values,values the page displayed, and pass or fail. When a row fails, quote the serverlog or browser console line that explains it, or say that neither recordedanything, before you edit anything. Then fix the page, restart the app, andupload all seven images again. Stop the app at the end.
/browser Run the app with dotnet run (the http profile listens on
http://localhost:5181) and open /scanner. Upload each image in TestData/scans
through the page's file input, one at a time, wait until the page shows a
result or a message, and screenshot it. TestData/scans/expected.csv lists the
values each image carries, separated by | when there are several, and an empty
cell means no code. Return a Walkthrough table of file, bytes, expected values,
values the page displayed, and pass or fail. When a row fails, quote the server
log or browser console line that explains it, or say that neither recorded
anything, before you edit anything. Then fix the page, restart the app, and
upload all seven images again. Stop the app at the end.
Text
A fix to the result list changes the markup for every image, so only a complete rerun shows that nothing regressed.
Please note: Open the screenshot for the largest photo first. An oversized upload can fail without a line in the server log, so a report built from logs alone would call that row a pass.
Every screenshot is saved as an image artifact. When one shows a problem, such as a pass number cut short, comment on it rather than describing it in a new prompt.
What causes missing or wrong QR values?
Most failed rows trace back to a few causes, and naming the likely one in a follow-up task saves the agent a round of trial and error. We put the page from IronQR's Blazor scanner guide into a fresh .NET 9 template and uploaded the seven test images through headless Chrome with IronQR 2026.10.1. Every defect below compiled cleanly. With the render mode added, five of seven images showed the right value.
A Scan button that stays disabled for every image needs the render mode the Quickstart plan states.
The 48 MP photo (17,955,400 bytes) exceeds the guide's 10,000,000-byte cap and leaves the button disabled with no message. Set a cap above your largest photo and show a message for larger files.
When an image of two passes shows one value, list every result.
How do you set up the project and its rules for Antigravity?
Give the agent a project that already builds and runs. Create it with dotnet new blazor --interactivity Server --framework net9.0, run it once, and note the URL in Properties/launchSettings.json. The template picks a different port for each project, and the /browser prompt needs it.
Then delegate the package and key check.
Tell me which IronQR version the project references and paste itsPackageReference line. Then show me where the license key is applied. It hasto be set once at startup from IRONQR_LICENSE_KEY, appsettings.json andappsettings.Development.json must carry no IronQr.LicenseKey entry, and whenthe variable is missing the app should log one startup warning that names thevariable but prints no value. Change no other file, and touch Program.csonly if that warning is missing.
Tell me which IronQR version the project references and paste its
PackageReference line. Then show me where the license key is applied. It has
to be set once at startup from IRONQR_LICENSE_KEY, appsettings.json and
appsettings.Development.json must carry no IronQr.LicenseKey entry, and when
the variable is missing the app should log one startup warning that names the
variable but prints no value. Change no other file, and touch Program.cs
only if that warning is missing.
Text
Use the reported version in every later task. Without a key, decoding fails with LicensingException and "Production License Required", so a missing variable fails the first scan.
Once the upload table passes, write the rules down. Antigravity loads AGENTS.md and GEMINI.md at every folder level up to the workspace root, per its rules documentation, and /learn turns a session's corrections into persistent rules.
/learn Write the rules this session established into a new AGENTS.md at therepository root (the always-on rules file, not .antigravity/rules.md) under a"QR check-in page" heading. Cover the render mode the scanner page needs, theupload cap and the reason for its value, showing every decoded value insteadof the first, keeping IRONQR_LICENSE_KEY out of files, and that no scannerchange is done until the /browser upload table passes for every image inTestData/scans.
/learn Write the rules this session established into a new AGENTS.md at the
repository root (the always-on rules file, not .antigravity/rules.md) under a
"QR check-in page" heading. Cover the render mode the scanner page needs, the
upload cap and the reason for its value, showing every decoded value instead
of the first, keeping IRONQR_LICENSE_KEY out of files, and that no scanner
change is done until the /browser upload table passes for every image in
TestData/scans.
Text
Every later task loads that file, so prompts stop repeating the render mode. A task that changes a rule has to change the file too. In our run, AGENTS.md kept the 30 MB cap from the Quickstart plan after the upload refactor moved the service to 25 MB.
How do you decode an upload without a temporary file?
IronQR reads images as AnyBitmap, so the page has to turn the browser upload into one. The Blazor scanner guide copies the upload to a temporary file named after the client's file name and opens it from disk. Two desks that upload files with the same name at once can collide, and Microsoft says never to save an upload under a client-supplied name.
AnyBitmap.FromStream avoids the file, but the browser stream behind the upload cannot seek and rejects synchronous reads. In IronQR 2026.10.1, a stream with those limits passed straight in throws NotSupportedException. Copying the upload into a MemoryStream with CopyToAsync before the read works. Microsoft advises against buffering whole uploads in server memory, so the upload cap also caps the buffer.
Open an Implementation Plan Artifact for this refactor before editing. Movethe decoding out of the Razor page into Services/PassScanService.cs,registered as a singleton. The service takes the IBrowserFile, rejects filesover 25 MB with a message the page shows, copies the upload into aMemoryStream with CopyToAsync, builds the AnyBitmap from that buffer, reads itwith QrReader.ReadAsync and QrScanMode.Auto, and returns value, QrType and Urlfor every result. No temporary files and no client file names on disk. Showthe scan time in milliseconds under the results. Then repeat the /browserupload table for all seven images.
Open an Implementation Plan Artifact for this refactor before editing. Move
the decoding out of the Razor page into Services/PassScanService.cs,
registered as a singleton. The service takes the IBrowserFile, rejects files
over 25 MB with a message the page shows, copies the upload into a
MemoryStream with CopyToAsync, builds the AnyBitmap from that buffer, reads it
with QrReader.ReadAsync and QrScanMode.Auto, and returns value, QrType and Url
for every result. No temporary files and no client file names on disk. Show
the scan time in milliseconds under the results. Then repeat the /browser
upload table for all seven images.
Text
Look for a service along these lines in the diff. We compiled this version against IronQR 2026.10.1, and a page that called it showed the right values for all seven test images.
using IronQr;using IronQr.Enum;using IronSoftware.Drawing;using Microsoft.AspNetCore.Components.Forms;public sealed class PassScanService{ public const longMaxUploadBytes = 25 * 1024 * 1024; public async Task<List<QrResult>> ScanAsync(IBrowserFile file) { using var buffer = new MemoryStream(); await using (var upload = file.OpenReadStream(MaxUploadBytes)) { await upload.CopyToAsync(buffer); } buffer.Position = 0; using var image = AnyBitmap.FromStream(buffer); var results = await new QrReader().ReadAsync(new QrImageInput(image, QrScanMode.Auto)); return results.ToList(); }}
using IronQr;
using IronQr.Enum;
using IronSoftware.Drawing;
using Microsoft.AspNetCore.Components.Forms;
public sealed class PassScanService
{
public const long MaxUploadBytes = 25 * 1024 * 1024;
public async Task<List<QrResult>> ScanAsync(IBrowserFile file)
{
using var buffer = new MemoryStream();
await using (var upload = file.OpenReadStream(MaxUploadBytes))
{
await upload.CopyToAsync(buffer);
}
buffer.Position = 0;
using var image = AnyBitmap.FromStream(buffer);
var results = await new QrReader().ReadAsync(new QrImageInput(image, QrScanMode.Auto));
return results.ToList();
}
}
C#
QrResult.Url is set only when the value is a valid URI, which suits a desk where some passes carry a link and others a plain pass number.
Why is the first scan after a restart slow?
The IronQR package includes IronQrDetection.dll, a 44 MB assembly in 2026.10.1 that is almost entirely one embedded resource, and depends on Microsoft.ML.OnnxRuntime 1.20.1 to run the detection model. QrScanMode.Auto tries the detector before a basic scan, which runs only when the detector finds nothing.
In three fresh console processes reading a clean image, the first Auto read took 3.1 to 4.7 seconds and later reads took 0.5 to 1.0 seconds. A new reader per read did not bring the delay back, so the cost is per process. The first OnlyBasicScan read, loading the file included, took 0.33 to 0.56 seconds. On Blazor Server, the first visitor after a restart or scale-out waits unless the app loads the detector at startup.
/browser Measure the cold-start cost of the detection model. Restart the app,upload TestData/scans/pass-clean.png three times on /scanner, and note thescan time the page shows each time. Then add an IHostedService that reads onesmall blank AnyBitmap with QrReader at startup, off the request path, restart,repeat the three uploads, and put both sets of timings side by side in theWalkthrough with a screenshot of each first upload.
/browser Measure the cold-start cost of the detection model. Restart the app,
upload TestData/scans/pass-clean.png three times on /scanner, and note the
scan time the page shows each time. Then add an IHostedService that reads one
small blank AnyBitmap with QrReader at startup, off the request path, restart,
repeat the three uploads, and put both sets of timings side by side in the
Walkthrough with a screenshot of each first upload.
Text
An IHostedService warm-up that blocks startup trades a slow first upload for a slow first page load, and the agent's timings show which one it built.
When should you dispatch more than one agent?
A test agent and a browser agent check different things, the service's results in code and what staff see on the page.
Agent A: create an xUnit project QrScanner.Tests that calls PassScanServicefor every row of TestData/scans/expected.csv through a test double ofIBrowserFile, and asserts the exact set of values in any order. Add one testthat a file over the cap is rejected before IronQR reads it. Never compareQR images byte for byte.Agent B: start only after Agent A's tests pass against the current code. Use/browser to upload the same seven images on /scanner, attach a screenshot ofeach result to the Walkthrough, and state there that the tests passed first.
Agent A: create an xUnit project QrScanner.Tests that calls PassScanService
for every row of TestData/scans/expected.csv through a test double of
IBrowserFile, and asserts the exact set of values in any order. Add one test
that a file over the cap is rejected before IronQR reads it. Never compare
QR images byte for byte.
Agent B: start only after Agent A's tests pass against the current code. Use
/browser to upload the same seven images on /scanner, attach a screenshot of
each result to the Walkthrough, and state there that the tests passed first.
Text
In our tests, IronQR returned the two passes in a different order under Auto than under OnlyBasicScan, which is why the tests compare sets.
Please note: In the desktop app, give each agent its own task and compare the two Walkthroughs.
How do you deploy the scanner to a Linux container?
IronQR 2026.10.1 also depends on IronQR.Slim and IronSoftware.ReaderInternals.Windows, and IronQR.Linux swaps the Windows internals for IronSoftware.ReaderInternals.Linux.
This app will ship in a linux-x64 container built frommcr.microsoft.com/dotnet/aspnet:9.0. Do not change any C#. Make the projectreference IronQR for Windows builds and IronQR.Linux when RuntimeIdentifier islinux-x64, add a Dockerfile that publishes for linux-x64 and passesIRONQR_LICENSE_KEY in at run time, then run dotnet publish -r linux-x64--self-contained false -o publish-linux and list every .so file in thatfolder with the NuGet package it came from. Docker is not installed on thismachine, so stop before docker build.
This app will ship in a linux-x64 container built from
mcr.microsoft.com/dotnet/aspnet:9.0. Do not change any C#. Make the project
reference IronQR for Windows builds and IronQR.Linux when RuntimeIdentifier is
linux-x64, add a Dockerfile that publishes for linux-x64 and passes
IRONQR_LICENSE_KEY in at run time, then run dotnet publish -r linux-x64
--self-contained false -o publish-linux and list every .so file in that
folder with the NuGet package it came from. Docker is not installed on this
machine, so stop before docker build.
Text
The publish folder is the evidence, and you can inspect it on Windows. With the Windows IronQR package, a linux-x64 publish carried the ONNX Runtime libraries but not ReaderInterop.so, the Linux native reader. A PackageReference conditioned on RuntimeIdentifier picked the right package for each build.
Both native libraries link against glibc, and neither package ships a musl build, so an Alpine base image has nothing to load. IronSoftware.ReaderInternals.Linux 2026.3.47413 ships only a linux-x64 build, so ARM64 Linux is outside what IronQR.Linux 2026.10.1 covers. If the detector's DLLs miss the output folder, IronQR throws IronQrRuntimesCopyException, and the runtimes copy exception page gives the fix. The NuGet packages guide lists every platform package, and IronQR.Slim alone runs OnlyBasicScan without the detection model.
What did the prompts produce in a real run?
The eight prompts above ran in order in one Antigravity CLI conversation, unattended with --dangerously-skip-permissions, against a fresh .NET 9 Blazor Web App with Interactive Server render mode. The repository held seven test images with known values, listed in TestData/scans/expected.csv. They were a clean screenshot, a 12 MP and a 48 MP phone photo, an image of two passes, a tilted photo in poor light, a pass that carries a URL, and a desk photo with no pass. The key reached the agent only through the environment. After the run, we rebuilt the app in Release, ran the tests, and uploaded every image through headless Chrome ourselves.
Because the Quickstart plan already capped uploads above the 48 MP photo and listed every result, the guide page's two upload defects never appeared.
The first upload after a restart took 3,315 ms before the warm-up and 901 ms after it.
In the agent's own upload table, 6 of 7 rows passed, then 7 of 7 once it sorted the two-pass values.
Our upload check of the final app passed 7 of 7, and the xUnit tests passed 8 of 8.
The Release build finished with 0 warnings and 0 errors.
ReaderInterop.so in the linux-x64 publish came to 21,475,192 bytes.
Both plan-first prompts wrote the plan and changed files in the same turn, and one agent did the work of the two-agent prompt.
Browser screenshots and the linux-x64 publish folder were left in the repository.
The hub's run record lists the Antigravity version, model and run time, and the machine the run used.
Curtis Chau holds a Bachelor’s degree in Computer Science (Carleton University) and specializes in front-end development with expertise in Node.js, TypeScript, JavaScript, and React. Passionate about crafting intuitive and aesthetically pleasing user interfaces, Curtis enjoys working with modern frameworks and creating well-structured, visually appealing manuals.