Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]

### Added
- **Print engine for scripts and warehouse systems** (`rotula.exe`): print `.rotula` labels with no window. `rotula print` takes a label, field values (`--set`) or records from a CSV, Excel or JSON file, and a Windows printer or network address, and prints through the designer's own print path: resolution conversion, darkness and speed, variables, questions (`--answer`, otherwise the designer default; never a sample value), copies, and serial numbers used once. Before every job it checks the printer status (`~HS`) and holds the job back on a fault; after the last job it waits until the printer reports it finished. `rotula status` and `rotula printers` report printers, `--json` gives a machine-readable result, and exit codes tell a script what happened. `rotula serve` offers the same as a REST API (`/api/v1/jobs`, printer status and lists, OpenAPI at `/openapi/v1.json`) that can run as a Windows service: off until started, on `127.0.0.1` by default, an API key on every call (only its hash is stored), and only the label folders and printers its settings file lists. One job at a time per printer. Messages in English, Spanish, Portuguese (Brazil) and French (Canada). A new `PrintEngine` policy lets an administrator switch it off. See [docs/print-engine.md](docs/print-engine.md).
- **Label sheets for office printers** (File > Export > Label sheets (PDF), or Label sheets in the Print dialog): put the label on sheets for a laser or inkjet printer and save them as a PDF at the sheet's real size, each label at its own physical size. Pick a generic template (Letter 30-up, 20-up, 14-up, 10-up, 6-up, 80-up and half sheets; A4 3 x 8, 21-up, 65-up, 16-up, 14-up, 8-up and 2-up) or type your own layout, start at any position of a partly used sheet, and add crop marks or label outlines. With data loaded the ticked records go on the sheets with their copies, and serial numbers are used once, exactly as when printing. The layout is saved with the label; older versions of Rotula open such a file and ignore it. Sending the sheets straight to a Windows printer is not offered: print the PDF at actual size.
- **Machine-wide MSI for IT departments**: every release now also has `Rotula-X.Y.Z-win-x64.msi`, signed like the setup program. It installs to Program Files for every user with a Start menu entry and the `.rotula` association, upgrades older versions in place, and is made for silent deployment with Intune, Group Policy or Configuration Manager. The setup program can now also install for all users silently with `/ALLUSERS`. Silent install and uninstall commands, exit codes, Intune detection rules and Group Policy steps are in the new [docs/deployment.md](docs/deployment.md).
- **Settings your organization can manage**: administrators can fix the language and switch the update check on or off for everyone, suggest a default printer, keep add-ons in the user's own folder from loading, and switch printing or database access of the MCP server off, through Group Policy or Intune. Administrative templates (English and Spanish) are in `packaging/policies`. A setting fixed this way is greyed out in Rotula with "Managed by your organization".
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ These are product rules; changes that break them will not be merged.
address the user entered and the databases the user connects. The one exception is the **opt-in update check**: it sends
nothing unless the user (Help menu) or an administrator (policy) explicitly switches it on, and then it is at most one
anonymous `GET` of the public GitHub "latest release" API a day, with nothing about the user or the computer in it. It never
downloads or installs anything. Do not add other network calls, and do not add anything to that request.
downloads or installs anything. Do not add other network calls, and do not add anything to that request. The print engine's
REST server (`rotula serve`) is not a call out: it listens only when someone starts it, only on the addresses in its settings
file (`127.0.0.1` by default), and talks only to the printers that file allows.
- **Light theme only.**
- **English, Spanish, Portuguese (Brazil) and French (Canada)** through `.resx` resources in `src/Rotula.App/Resources`
(`Strings.en.resx`, `Strings.es.resx`, `Strings.pt-BR.resx`, `Strings.fr-CA.resx`). Every new user-visible string needs all
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Design it, preview it, merge your data, print it. No account, no cloud, no telem
- [Printing](#printing)
- [Print station](#print-station)
- [MCP server](#mcp-server)
- [Print engine (command line and REST API)](#print-engine-command-line-and-rest-api)
- [Barcode support](#barcode-support)
- [Languages](#languages)
- [Privacy](#privacy)
Expand Down Expand Up @@ -452,6 +453,27 @@ on in `rotula-mcp.settings.json`. Printing needs a two-step confirmation. The in

---

## Print engine (command line and REST API)

`rotula.exe` prints Rotula labels with no window, for scripts, scheduled tasks and warehouse systems (WMS, ERP, shipping software).
It uses the designer's own print path: resolution conversion, darkness and speed, variables and questions, data merge, and serial
numbers that are reserved before a job and used up only for labels that reached the printer. Before every job it asks the printer
for its status (ZPL `~HS`) and holds the job back while the printer is out of labels, open, paused or out of ribbon; after the last
job it waits until the printer reports it finished. Results come back as exit codes or JSON.

```bat
rotula print carton.rotula --printer "Dock 1" --data wave-1207.csv --reference WAVE-1207 --json
rotula status --host 10.20.0.31
```

`rotula serve` exposes the same engine as a REST API (`POST /api/v1/jobs`, printer lists and status, an OpenAPI description) that
can run as a Windows service. It is off until you start it, listens on `127.0.0.1` unless you configure otherwise, needs an API
key on every call, and prints only labels from the folders and to the printers its settings file lists. Messages follow the
caller's language (English, Spanish, Portuguese (Brazil) or French (Canada)). Commands, API, settings and exit codes:
[docs/print-engine.md](docs/print-engine.md). The installer does not include `rotula.exe` yet; build it from `src/Rotula.Cli`.

---

## Languages

The interface is available in **English**, **Spanish**, **Portuguese (Brazil)** and **French (Canada)**. By default Rotula
Expand All @@ -471,6 +493,8 @@ one. It never downloads or installs anything. An administrator can switch the ch
([deployment](docs/deployment.md)). Your settings
(language, units, recent files, last printer) are a small JSON file in `%AppData%\Rotula`. If the program ever fails, it
writes a log to `%AppData%\Rotula\rotula.log` on your PC and sends it nowhere.
The print engine's REST server (`rotula serve`) listens
for calls only when you start it, only on the addresses in its settings file, and makes no outgoing calls except to printers.

---

Expand Down Expand Up @@ -538,6 +562,8 @@ flowchart LR
| `src/Rotula.Printing` | Windows spooler and TCP 9100 printing, printer detection |
| `src/Rotula.Data` | Database providers, presets, the read-only query guard and the connection store |
| `src/Rotula.Mcp` | The MCP server (a separate executable) |
| `src/Rotula.Engine` | The headless print engine: print requests, printer status, settings and API keys. No UI |
| `src/Rotula.Cli` | `rotula.exe`: the print engine's command line and REST server |
| `src/Rotula.App` | The WPF application |
| `tests/` | xUnit tests and the golden ZPL fixtures |
| `packaging/` | Installer script, MSI project, Group Policy templates and winget manifest templates |
Expand Down
30 changes: 30 additions & 0 deletions Rotula.sln
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rotula.Data.Pack.ClickHouse
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rotula.Data.Packs.Tests", "tests\Rotula.Data.Packs.Tests\Rotula.Data.Packs.Tests.csproj", "{3DC421B4-0B47-4308-8791-4225E9A0CCAD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rotula.Engine", "src\Rotula.Engine\Rotula.Engine.csproj", "{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rotula.Cli", "src\Rotula.Cli\Rotula.Cli.csproj", "{1CB2C052-D923-4083-A606-C1E4B81D36C7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -285,6 +289,30 @@ Global
{3DC421B4-0B47-4308-8791-4225E9A0CCAD}.Release|x64.Build.0 = Release|Any CPU
{3DC421B4-0B47-4308-8791-4225E9A0CCAD}.Release|x86.ActiveCfg = Release|Any CPU
{3DC421B4-0B47-4308-8791-4225E9A0CCAD}.Release|x86.Build.0 = Release|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|x64.ActiveCfg = Debug|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|x64.Build.0 = Debug|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|x86.ActiveCfg = Debug|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|x86.Build.0 = Debug|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|Any CPU.Build.0 = Release|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|x64.ActiveCfg = Release|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|x64.Build.0 = Release|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|x86.ActiveCfg = Release|Any CPU
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|x86.Build.0 = Release|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|x64.ActiveCfg = Debug|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|x64.Build.0 = Debug|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|x86.ActiveCfg = Debug|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|x86.Build.0 = Debug|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|Any CPU.Build.0 = Release|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|x64.ActiveCfg = Release|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|x64.Build.0 = Release|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|x86.ActiveCfg = Release|Any CPU
{1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -309,5 +337,7 @@ Global
{A93A2F2F-9C16-49E9-B046-5A0CC310AB2F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{12DDAC38-D696-4C2E-A2E4-7DADB7D08D28} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{3DC421B4-0B47-4308-8791-4225E9A0CCAD} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{AEEF329C-5A0F-42F6-BE98-2D444BFA3904} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{1CB2C052-D923-4083-A606-C1E4B81D36C7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
7 changes: 7 additions & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ read from the project files and the license metadata in each package's `.nuspec`
| System.Configuration.ConfigurationManager (via the Microsoft.Extensions.* packages) | 9.0.18 | MIT | Rotula.Mcp | https://github.com/dotnet/runtime |
| System.Security.Cryptography.Pkcs (pinned to the patched version in Rotula.Data) | 10.0.13 | MIT | Rotula.Mcp | https://github.com/dotnet/runtime |
| System.Diagnostics.EventLog (via Microsoft.Extensions.Logging.EventLog) | 10.0.10 | MIT | Rotula.Mcp | https://github.com/dotnet/runtime |
| ASP.NET Core shared framework (Microsoft.AspNetCore.App, part of .NET; copied into a self-contained `rotula.exe`) | 10.0 | MIT | Rotula.Cli | https://github.com/dotnet/aspnetcore |
| Microsoft.AspNetCore.OpenApi (OpenAPI description of the print engine's REST API) | 10.0.13 | MIT | Rotula.Cli | https://github.com/dotnet/aspnetcore |
| Microsoft.OpenApi (via Microsoft.AspNetCore.OpenApi) | 2.12.2 | MIT | Rotula.Cli | https://github.com/microsoft/OpenAPI.NET |
| Microsoft.Extensions.Hosting.WindowsServices (runs `rotula serve` as a Windows service) | 10.0.13 | MIT | Rotula.Cli | https://github.com/dotnet/runtime |
| System.ServiceProcess.ServiceController (via Microsoft.Extensions.Hosting.WindowsServices) | 10.0.13 | MIT | Rotula.Cli | https://github.com/dotnet/runtime |
| Liberation Sans Regular / Bold and Liberation Mono Regular (embedded stand-in fonts) | 2.x | SIL Open Font License 1.1 | Rotula.Rendering | https://github.com/liberationfonts/liberation-fonts |

The MCP server (`Rotula.Mcp`) is a separate executable from the app. It uses the packages marked `Rotula.Mcp` above; its license
expressions were read from each package's `.nuspec` in the restored NuGet cache (the SDK is Apache-2.0, used unmodified).
The print engine (`rotula.exe`, `Rotula.Cli` and `Rotula.Engine`) is another separate executable; it uses the packages marked
`Rotula.Cli` above besides Rotula.Rendering's and Rotula.Core's.

The Liberation fonts are embedded unmodified in `Rotula.Rendering`; their license text is in
`src/Rotula.Rendering/Fonts/LICENSE-LiberationFonts.txt` and is also embedded in the assembly. Apache-2.0 components are
Expand Down
27 changes: 27 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,3 +636,30 @@ The design canvas stays upright because editing an upside-down label (handles, r
"Prints upside down" / "Prints mirrored" instead. PNG, JPEG and PDF exports and the MCP preview stay as designed: they are artwork for documents,
proofs and screens, not a picture of the media leaving the printer, and there is no "as printed" export option to honour. The two flips commute
(inverted and mirrored together is a top-to-bottom flip), so their order is not a question.

## Print engine (Rotula.Engine, Rotula.Cli)

### A library and a thin executable, on the app's print path
`Rotula.Engine` (net10.0, no UI) turns a request into a print through `IPrintService`, `LabelPrintRun` / `VariableJob`, `PromptSet`,
`LabelDpiConverter` and `PrinterSetup`, the same pieces the Print dialog and the data merge use, so a label prints byte for byte the same from
the designer, the station and the engine. `Rotula.Cli` (`rotula.exe`) is only argument parsing, the REST host and the console. Results are values
(`PrintJobResult` with an outcome, an error code and localized notices), never exceptions, so the command line and the API answer the same thing.
Messages come from the app's own `.resx` files, embedded in the engine, so the engine needs no WPF and every string exists in all four languages.

### Questions take their default, never the sample value
An unattended job cannot ask. An unanswered question takes the default set in the designer; a required question with no default stops the job
with every question listed. The sample value is design-time data and printing it on a real carton would be silent, wrong output.

### One job at a time per printer, status before every job
Requests for the same printer (a queue name, or host:port) wait on one `SemaphoreSlim`, so two callers never interleave labels on one roll. The
`~HS` check runs before each job and verification once after the last, as in the app's data merge. Counters are reserved before and confirmed
only for the labels sent, so a held-back or failed job gives its serial numbers back unless part of it may have printed.

### The REST server is opt-in and allow-listed
Offline-first stays true: the server listens only when started, on `127.0.0.1` unless configured, and calls out only to printers. Every route but
health and the OpenAPI document needs a key; keys are 32 random bytes shown once and stored as SHA-256 hashes, compared with `FixedTimeEquals`
(a slow hash is not needed for 256-bit random keys). Labels are named relative to configured folders (no rooted paths, `..`, drives, UNC or
wildcards), printers must be listed, inline label content is off by default, and request bodies and label files have size limits. The settings
file and request bodies refuse unknown names, so a typo never widens what is allowed. The audit log carries an 8-character key id from the hash,
never the key or label data. `PrintEngine = 0` (policy) switches it all off. Kestrel and ASP.NET Core come with .NET; the only packages added are
Microsoft's OpenAPI and Windows service hosting (MIT).
3 changes: 2 additions & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ the logon requirement to **Whether or not a user is logged on**, and add the new
Rotula reads the policies once at start-up from `HKLM\SOFTWARE\Policies\P4Software\Rotula` (the computer) and
`HKCU\SOFTWARE\Policies\P4Software\Rotula` (the user). When both set the same value, **the computer's value wins**. A
setting fixed by a policy is shown disabled in Rotula with the note "Managed by your organization", and the user's own
choice is kept in `settings.json` for the day the policy is removed. The MCP server (`Rotula.Mcp.exe`) reads the same key.
choice is kept in `settings.json` for the day the policy is removed. The MCP server (`Rotula.Mcp.exe`) and the print engine (`rotula.exe`) read the same key.

| Policy (template name) | Registry value | Type | Values | Effect |
|---|---|---|---|---|
Expand All @@ -215,6 +215,7 @@ choice is kept in `settings.json` for the day the policy is removed. The MCP ser
| Allow data source add-ons from the user's folder | `UserAddOns` | REG_DWORD | `0` blocks | Add-ons in `%AppData%\Rotula\packs` are neither loaded nor offered for approval; the installed `packs` folder still works ([data-providers.md](data-providers.md)) |
| Allow the MCP server to print | `McpPrinting` | REG_DWORD | `0` blocks | The MCP server refuses every print request, whatever its settings file says |
| Allow the MCP server to read databases | `McpDatabase` | REG_DWORD | `0` blocks | The MCP server refuses every database query |
| Allow the print engine to print | `PrintEngine` | REG_DWORD | `0` blocks | `rotula.exe` refuses to print or query printers (exit code 5) and its REST server answers 403, whatever its settings file says ([print-engine.md](print-engine.md)) |

A value that Rotula does not understand (a language other than the five values above, a number other than 0 or 1) is ignored, so
a typo never forces a wrong setting on users. The MCP policies can only take something away: setting them to `1` does not
Expand Down
Loading
Loading