37Limits for untrusted input & thread safety
Labels from uploads, web services, ERP interfaces or coding agents are untrusted input. zplCore limits what such input may cost – labels, dots, graphics, field data, printer storage, image files – and documents which classes may be shared between threads.
ZplLimits: one scope for everything
For untrusted input (uploads, web services, agents) ZplLimits.Enforce sets hard limits for every renderer in its scope – also those that the compliance check, scan-proof, import, extraction, GS1 migration, printer twin, cut plan and Vector-Lift create internally, across tasks and Parallel loops. Settings are clamped to the stricter value, font providers, system fonts and background images are turned off; optionally a fixed clock. The MCP server runs every call inside this scope.
// using ZplCore.Compliance;
using (ZplLimits.Enforce(new ZplLimits { MaxLabels = 10, MaxGraphicBytes = 4 << 20, FixedClock = DateTime.UnixEpoch }))
{
// applies to every renderer in this scope – also those that compliance, scan-proof,
// import, extraction, printer twin and Vector-Lift create internally (also across tasks/Parallel)
var report = ZplCompliance.Check(untrusted, CompliancePacks.Gs1LogisticLabel);
}
Images and files: inside a scope, image items (sourceFile or a path in bound data) read no local files unless they lie below one of the AllowedImageRoots (default: none; no links below the root) – a refused, missing or unreadable file always gives the same answer, so no path can be probed. Images are checked before decoding against MaxImageBytes, MaxSvgBytes and MaxImagePixels (header size, SVG raster and scaled target raster; exceeding throws ZplLimitExceededException). A CancellationToken stops rendering and label generation at the next command or label, so a host can end work at its time limit. Outside a scope nothing changes: the SDK reads image files as before.
using (ZplLimits.Enforce(new ZplLimits { AllowedImageRoots = ["/srv/label-images"], MaxImagePixels = 16_000_000,
MaxImageBytes = 4 << 20, CancellationToken = requestTimeout.Token }))
zpl = label.ToZpl(records);
Further members: MaxQuantity, MaxLabelDots, MaxLabelSide, MaxFieldDataLength, MaxStoreBytes (printer storage) and Isolated (default true: no FontProvider, no system fonts, no background or watermark image, no cache of decoded graphics – fonts then come only from downloads in the same input, ~DY/~DU). A limit that is not set is unlimited; nested scopes combine to the strictest values (image folders: only what every scope allows; any token cancels). ZplLimits.Current returns the limits in force, ZplLimitExceededException.Limit names the exceeded property, e.g. "MaxImagePixels".
Limits for untrusted input & thread safety
Defaults: MaxLabelSide 32000, MaxLabelDots 64 million, MaxQuantity 10000, MaxLabels 100000, MaxGraphicBytes 32 MB (also the Z64 decompression limit against zip bombs), MaxFieldDataLength 1 million, ZplResourceStore.MaxTotalBytes 256 MB. ZplRenderer/EplRenderer hold printer state and are not thread-safe – use one renderer per thread; ZplResourceStore, FontManager, ZplCaches, PropertyPath and configured LabelBindings are. ConcurrencyTests compares the SHA-256 of all outputs from 16 threads with the single-threaded run; samples/FuzzRunner feeds the parser random and mutated input, samples/LeakTest renders 1 million labels and logs heap and RSS. Details: THREADING.md, DIAGNOSTICS.md.
var shared = new ZplResourceStore(); // thread-safe, shareable by many renderers
var safe = new ZplRenderSettings
{
MaxLabelSide = 8000, MaxLabelDots = 16_000_000, MaxQuantity = 100,
MaxLabels = 1000, MaxGraphicBytes = 4 * 1024 * 1024, MaxFieldDataLength = 10_000,
};
Parallel.For(0, 8, _ =>
{
var r = new ZplRenderer(safe) { Store = shared }; // one renderer per thread
r.ProcessCommands(untrusted);
});

