diff --git a/.gitignore b/.gitignore
index 90263dd..1e7cb60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,9 +24,9 @@ build/publish/
*.cache
*.log
-# Installers (generated files)
-installer/*.exe
-installer/*.sha256
+# Installers (generated files; includes installer/Output/)
+installer/**/*.exe
+installer/**/*.sha256
# WinGet temporary files
winget-pkgs-*/
@@ -41,6 +41,10 @@ Assets/
# Release artifacts (build outputs uploaded to GitHub Release; do not version)
release/*
!release/README.md
+!release/scripts/
+!release/scripts/**
+!release/packages/
+!release/packages/**
# Generated EditorConfig
*.editorconfig
@@ -60,9 +64,6 @@ release/*
Thumbs.db
Desktop.ini
-# Cursor
-.cursor/
-
# Test directories and script output
test-docker/
test-*/
@@ -87,11 +88,12 @@ TESTES-MANUAIS-*.md
coverage-out/
TestResults/
-# Cursor (local IDE/agent config)
+# Cursor / local tooling (optional)
.cursor/
# GitHub prompts/skills (not versioned)
-.github/
-# Agent instructions (not versioned)
+.github/prompts/
+.github/skills/
+# Agent instructions (optional local-only; remove this line to version AGENTS.md)
AGENTS.md
# OpenSpec (local change tracking)
openspec/
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6301a3..9898fc3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
---
+## [3.1.1] - 2026-03-20
+
+### 🔧 Changed
+
+- **Release layout**: Release automation scripts live under `release/scripts/`; Linux package sources under `scripts/release/packages/` (paths updated in docs and `CONTRIBUTING.md`).
+- **Conversion pipeline**: Shared Labelary PDF fallback path in `ConversionService` for CLI, REST API, TCP server, and daemon queue; internal cleanup of dimension types and daemon PID handling (no reflection).
+
+### 🛠️ Maintenance
+
+- **Windows installer build**: `release/scripts/08-build-installer.ps1` — safer post-build cleanup; optional `release/scripts/cleanup-installer-output.ps1` for `installer/Output`.
+- **Docker build context**: `.dockerignore` excludes non-essential trees (e.g. `tests`, `docs`, `release`, `.cursor`) for smaller/faster `docker build`.
+- **Repository**: `.gitignore` — track `.github` except `.github/prompts/` and `.github/skills/` (local Cursor/GitHub Copilot prompts stay private).
+
+---
+
## [3.1.0] - 2026-03-19
### ✨ Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9228aa5..929bf98 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -49,7 +49,7 @@ ZPL2PDF/
│ ├── ZPL2PDF.Unit/ # Unit tests
│ └── ZPL2PDF.Integration/ # Integration tests
├── docs/ # Documentation
-└── build/ # Build scripts
+└── scripts/ # Build and utility scripts
```
## 🧪 Testing
diff --git a/Dockerfile b/Dockerfile
index 0927e6c..93306a2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -102,4 +102,4 @@ CMD ["/app/ZPL2PDF", "run", "-l", "/app/watch"]
# Metadata
LABEL maintainer="brunoleocam" \
description="ZPL2PDF - Alpine Linux (Ultra Lightweight)" \
- version="3.1.0"
+ version="3.1.1"
diff --git a/README.md b/README.md
index 2d22707..0ae6283 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# ZPL2PDF - ZPL to PDF Converter
-[](https://github.com/brunoleocam/ZPL2PDF/releases)
+[](https://github.com/brunoleocam/ZPL2PDF/releases)

[](https://dotnet.microsoft.com/download)
[](https://github.com/brunoleocam/ZPL2PDF)
@@ -16,78 +16,23 @@ A powerful, cross-platform command-line tool that converts ZPL (Zebra Programmin
---
-## 🔜 **CLI & packaging (unreleased / main branch)**
+## 🚀 **What's New in v3.1.1**
-These items are documented in [CHANGELOG.md](CHANGELOG.md) under **Unreleased**:
+### 🔧 Changed
-- **`--stdout`**: write the PDF to standard output (binary). `-o` is not required; no status text is written to stdout.
-- **Default output name (`-n`)**: if omitted, uses the input file base name (e.g. `label.txt` → `label.pdf`); with `-z`, a timestamped `ZPL2PDF_*.pdf` name is used.
-- **Dimensions**: `-u` is required whenever `-w` / `-h` are set, including for `mm`.
-- **Linux package scripts**: `scripts/build-deb.sh` and `scripts/build-rpm.sh` read the version from `ZPL2PDF.csproj`.
-- **Rendering stack**: PDFsharp 6.x and updated BinaryKits packages; Aztec `^B0` is normalized for offline rendering (`^BO`).
+- **Release tooling**: Scripts under `release/scripts/`; Linux packaging assets under `scripts/release/packages/` (documentation and contributor paths updated).
+- **Internals**: Single Labelary PDF fallback path in `ConversionService` across CLI, API, TCP server, and daemon; dimension/value-object consolidation and daemon PID handling without reflection.
----
-
-## 🚀 **What's New in v3.1.0**
-
-### 🐛 Bug Fixes
-- **Fixed Issue #45**: Duplicate or blank labels when `^XA` appears inside `~DGR:` base64 payload — `^XA` is now treated as label start only at line start or after `^XZ`.
-
-### ✨ New Features
-- **Issue #48 – TCP Server**: Virtual Zebra printer mode is now implemented. Use `ZPL2PDF server start --port 9101 -o output/`, `server stop`, and `server status`.
-- **REST API (PR #47)**: Run `ZPL2PDF --api --host localhost --port 5000` for `POST /api/convert` (ZPL to PDF or PNG) and `GET /api/health`.
-
----
-
-## 🚀 **What's New in v3.1.0**
-
-### 🐛 Bug Fixes
-- **Fixed Issue #39**: Sequential graphic processing for multiple graphics with same name
- - ZPL files with multiple `~DGR` graphics now process correctly
- - Each label uses the correct graphic based on sequential state
- - `^IDR` cleanup commands no longer generate blank pages
- - Resolves issue where all labels were identical in Shopee shipping label files
-
-### 🔧 Improvements
-- Added input validation in public methods
-- Improved exception handling
-- Performance optimizations with compiled regex
-- Code cleanup and removal of unused methods
+### 🛠️ Maintenance
----
-
-## 🚀 **What's New in v3.1.0**
-
-### 🎉 Major New Features
-- 🎨 **Labelary API Integration** - High-fidelity ZPL rendering with vector PDF output
-- 🖨️ **TCP Server Mode** - Virtual Zebra printer on TCP port (default: 9101)
-- 🔤 **Custom Fonts** - Load TrueType/OpenType fonts with `--fonts-dir` and `--font`
-- 📁 **Extended File Support** - Added `.zpl` and `.imp` file extensions
-- 📝 **Custom Naming** - Set output filename via `^FX FileName:` in ZPL
+- **Installer pipeline**: More reliable cleanup in `08-build-installer.ps1`; optional `cleanup-installer-output.ps1` for `installer/Output`.
+- **Docker**: Leaner default build context via `.dockerignore`.
+- **Repo hygiene**: `.github/prompts/` and `.github/skills/` remain untracked; the rest of `.github` (workflows, templates) stays versioned.
-### 🔧 Rendering Options
-```bash
---renderer offline # BinaryKits (default, works offline)
---renderer labelary # Labelary API (high-fidelity, requires internet)
---renderer auto # Try Labelary, fallback to BinaryKits
-```
-
-### 🖨️ TCP Server (Virtual Printer)
-```bash
-ZPL2PDF server start --port 9101 -o output/
-ZPL2PDF server status
-ZPL2PDF server stop
-```
+### Recent highlights (v3.1.1)
-### v2.x Features (Still Available)
-- 🌍 **Multi-language Support** - 8 languages (EN, PT, ES, FR, DE, IT, JA, ZH)
-- 🔄 **Daemon Mode** - Automatic folder monitoring and batch conversion
-- 🏗️ **Clean Architecture** - Completely refactored with SOLID principles
-- 🌍 **Cross-Platform** - Native support for Windows, Linux, and macOS
-- 📐 **Smart Dimensions** - Automatic ZPL dimension extraction (`^PW`, `^LL`)
-- ⚡ **High Performance** - Async processing with retry mechanisms
-- 🐳 **Docker Support** - Alpine Linux optimized (470MB)
-- 📦 **Professional Installer** - Windows installer with multi-language setup
+- **`--stdout`**, smarter default PDF naming, BinaryKits/PDFsharp bumps, dimension validation fix, Aztec `^B0` → `^BO` preprocessing.
+- Thanks to Jacques Caruso (jacques.caruso@exhibitgroup.fr) for contributions that landed in v3.1.0.
---
@@ -122,7 +67,7 @@ ZPL2PDF server start --port 9101 -o output/
Set your preferred language:
```bash
# Temporary (current session)
-ZPL2PDF --language pt-BR status
+ZPL2PDF status --language pt-BR
# Permanent (all sessions)
ZPL2PDF --set-language pt-BR
@@ -163,25 +108,25 @@ winget install brunoleocam.ZPL2PDF
#### Ubuntu/Debian (.deb package)
```bash
# Download .deb package from releases
-wget https://github.com/brunoleocam/ZPL2PDF/releases/download/v3.1.0/ZPL2PDF-v3.1.0-linux-amd64.deb
+wget https://github.com/brunoleocam/ZPL2PDF/releases/download/v3.1.1/ZPL2PDF-v3.1.1-linux-amd64.deb
# Install package
-sudo dpkg -i ZPL2PDF-v3.1.0-linux-amd64.deb
+sudo dpkg -i ZPL2PDF-v3.1.1-linux-amd64.deb
# Fix dependencies if needed
sudo apt-get install -f
# Verify installation
-zpl2pdf --help
+zpl2pdf -help
```
#### Fedora/CentOS/RHEL (.tar.gz)
```bash
# Download tarball from releases
-wget https://github.com/brunoleocam/ZPL2PDF/releases/download/v3.1.0/ZPL2PDF-v3.1.0-linux-x64-rpm.tar.gz
+wget https://github.com/brunoleocam/ZPL2PDF/releases/download/v3.1.1/ZPL2PDF-v3.1.1-linux-x64-rpm.tar.gz
# Extract to system
-sudo tar -xzf ZPL2PDF-v3.1.0-linux-x64-rpm.tar.gz -C /
+sudo tar -xzf ZPL2PDF-v3.1.1-linux-x64-rpm.tar.gz -C /
# Make executable
sudo chmod +x /usr/bin/ZPL2PDF
@@ -190,7 +135,7 @@ sudo chmod +x /usr/bin/ZPL2PDF
sudo ln -s /usr/bin/ZPL2PDF /usr/bin/zpl2pdf
# Verify installation
-zpl2pdf --help
+zpl2pdf -help
```
#### Docker (All Linux distributions)
@@ -204,7 +149,7 @@ docker run -v ./watch:/app/watch -v ./output:/app/output brunoleocam/zpl2pdf:lat
#### Intel Macs
```bash
# Download
-curl -L https://github.com/brunoleocam/ZPL2PDF/releases/download/v3.1.0/ZPL2PDF-v3.1.0-osx-x64.tar.gz -o zpl2pdf.tar.gz
+curl -L https://github.com/brunoleocam/ZPL2PDF/releases/download/v3.1.1/ZPL2PDF-v3.1.1-osx-x64.tar.gz -o zpl2pdf.tar.gz
# Extract and run
tar -xzf zpl2pdf.tar.gz
@@ -213,7 +158,7 @@ tar -xzf zpl2pdf.tar.gz
#### Apple Silicon (M1/M2/M3)
```bash
-curl -L https://github.com/brunoleocam/ZPL2PDF/releases/download/v3.1.0/ZPL2PDF-v3.1.0-osx-arm64.tar.gz -o zpl2pdf.tar.gz
+curl -L https://github.com/brunoleocam/ZPL2PDF/releases/download/v3.1.1/ZPL2PDF-v3.1.1-osx-arm64.tar.gz -o zpl2pdf.tar.gz
tar -xzf zpl2pdf.tar.gz
./ZPL2PDF -help
```
@@ -342,8 +287,8 @@ ZPL2PDF -i label.txt -o output/ --renderer offline
ZPL2PDF -i label.txt -o output/ --renderer labelary
```
- ✅ Exact Zebra printer emulation
-- ✅ Vector PDF output (smaller files)
-- ✅ Automatic batching for 50+ labels
+- ✅ High-fidelity rendering via Labelary (uses Labelary to generate PNGs)
+- ✅ Works for multi-label ZPL inputs
- ⚠️ Requires internet connection
### **Auto (Fallback)**
@@ -355,6 +300,126 @@ ZPL2PDF -i label.txt -o output/ --renderer auto
---
+## 🌐 REST API
+Start the API server:
+
+```bash
+ZPL2PDF --api --host localhost --port 5000
+```
+
+### Health check
+
+```bash
+curl -s http://localhost:5000/api/health
+```
+
+```json
+{
+ "status": "ok",
+ "service": "ZPL2PDF API"
+}
+```
+
+### Convert (ZPL to PDF/PNG)
+
+Endpoint: `POST /api/convert`
+
+#### Request body
+
+```json
+{
+ "zpl": "^XA...^XZ",
+ "zplArray": ["^XA...^XZ"],
+ "format": "pdf",
+ "width": 7.5,
+ "height": 15,
+ "unit": "in",
+ "dpi": 203,
+ "renderer": "offline"
+}
+```
+
+Notes:
+- You must provide either `zpl` or `zplArray` (at least one non-empty ZPL string).
+- `renderer` supports `offline` (BinaryKits), `labelary` (Labelary online API), or `auto` (try Labelary then fall back).
+- If `width`/`height` are not set or are `0`, dimensions are extracted from ZPL (`^PW` / `^LL`) by default.
+
+#### Example: PDF (offline renderer)
+
+```bash
+curl -s -X POST http://localhost:5000/api/convert -H "Content-Type: application/json" -d '{
+ "zpl": "^XA^FO50,50^A0N,50,50^FDHello^FS^XZ",
+ "format": "pdf",
+ "renderer": "offline",
+ "width": 7.5,
+ "height": 3,
+ "unit": "in",
+ "dpi": 203
+}'
+```
+
+```json
+{
+ "success": true,
+ "format": "pdf",
+ "pdf": "JVBERi0xLjQKJc...base64...",
+ "pages": 1,
+ "message": "Conversion successful"
+}
+```
+
+#### Example: PDF (Labelary renderer - direct PDF)
+
+```bash
+curl -s -X POST http://localhost:5000/api/convert -H "Content-Type: application/json" -d '{
+ "zpl": "^XA^FO50,50^A0N,50,50^FDHello^FS^XZ",
+ "format": "pdf",
+ "renderer": "labelary",
+ "width": 7.5,
+ "height": 3,
+ "unit": "in",
+ "dpi": 203
+}'
+```
+
+```json
+{
+ "success": true,
+ "format": "pdf",
+ "pdf": "JVBERi0xLjQKJc...base64...",
+ "pages": 1,
+ "message": "Conversion successful"
+}
+```
+
+#### Example: PNG (Labelary renderer)
+
+```bash
+curl -s -X POST http://localhost:5000/api/convert -H "Content-Type: application/json" -d '{
+ "zpl": "^XA^FO50,50^A0N,50,50^FDHello^FS^XZ",
+ "format": "png",
+ "renderer": "labelary",
+ "width": 7.5,
+ "height": 3,
+ "unit": "in",
+ "dpi": 203
+}'
+```
+
+```json
+{
+ "success": true,
+ "format": "png",
+ "image": "iVBORw0KGgo...base64...",
+ "pages": 1,
+ "message": "Conversion successful"
+}
+```
+
+If more than one label/image is produced, the response uses `images` (array) instead of `image` (single string).
+
+---
+
## 🐳 **Docker Usage**
### **Quick Start with Docker**
@@ -396,7 +461,7 @@ Run:
docker-compose up -d
```
-📘 **Full Docker Guide:** [docs/DOCKER_GUIDE.md](docs/DOCKER_GUIDE.md)
+📘 **Full Docker Guide:** [docs/guides/DOCKER_GUIDE.md](docs/guides/DOCKER_GUIDE.md)
---
@@ -409,7 +474,7 @@ Create a `zpl2pdf.json` file in the application directory:
```json
{
"language": "en-US",
- "defaultWatchFolder": "C:\\Users\\user\\Documents\\ZPL2PDF Auto Converter",
+ "defaultListenFolder": "C:\\Users\\user\\Documents\\ZPL2PDF Auto Converter",
"labelWidth": 10,
"labelHeight": 5,
"unit": "cm",
@@ -427,9 +492,8 @@ See [zpl2pdf.json.example](zpl2pdf.json.example) for full configuration options.
| Variable | Description | Example |
|----------|-------------|---------|
| `ZPL2PDF_LANGUAGE` | Application language | `pt-BR` |
-| `ZPL2PDF_LOG_LEVEL` | Logging level | `Debug` |
-📘 **Language Configuration Guide:** [docs/LANGUAGE_CONFIGURATION.md](docs/LANGUAGE_CONFIGURATION.md)
+📘 **Language Configuration Guide:** [docs/guides/LANGUAGE_CONFIGURATION.md](docs/guides/LANGUAGE_CONFIGURATION.md)
---
@@ -474,7 +538,7 @@ src/
│ ├── Services/ # Business logic
│ └── Interfaces/ # Service contracts
├── Domain/ # Business entities & rules
-│ ├── ValueObjects/ # Immutable data objects
+│ ├── ValueObjects/ # Value objects
│ └── Services/ # Domain interfaces
├── Infrastructure/ # External concerns
│ ├── FileSystem/ # File operations
@@ -518,15 +582,15 @@ dotnet test --collect:"XPlat Code Coverage"
### **User Guides**
- 📖 [Complete Documentation](docs/README.md) - Full user manual
-- 🌍 [Multi-language Configuration](docs/LANGUAGE_CONFIGURATION.md)
-- 🐳 [Docker Usage Guide](docs/DOCKER_GUIDE.md)
+- 🌍 [Multi-language Configuration](docs/guides/LANGUAGE_CONFIGURATION.md)
+- 🐳 [Docker Usage Guide](docs/guides/DOCKER_GUIDE.md)
- 📦 [Inno Setup Guide](docs/INNO_SETUP_GUIDE.md)
### **Developer Guides**
- 🛠️ [Contributing Guide](CONTRIBUTING.md)
- 📋 [Changelog](CHANGELOG.md)
-- 🏗️ [Architecture Overview](docs/ARCHITECTURE.md)
-- 🔄 [CI/CD Workflow](docs/CI_CD_WORKFLOW.md)
+- 🏗️ [Architecture Overview](wiki/Architecture-Overview.md)
+- 🔄 [CI/CD Workflow](docs/development/CI_CD_WORKFLOW.md)
### **Build & Deployment**
- 🔨 [Build Scripts](scripts/README.md)
@@ -655,8 +719,9 @@ ZPL2PDF/
### **Debug Mode**
```bash
-# Enable verbose logging
-ZPL2PDF -i label.txt -o output/ --log-level Debug
+# Enable verbose logging by setting "logLevel": "Debug" in `zpl2pdf.json`
+# (then re-run your command)
+ZPL2PDF -i label.txt -o output/
```
### **Get Help**
diff --git a/ZPL2PDF.csproj b/ZPL2PDF.csproj
index 9da5a31..d25469a 100644
--- a/ZPL2PDF.csproj
+++ b/ZPL2PDF.csproj
@@ -5,7 +5,7 @@
net9.0
enable
enable
- 3.1.0
+ 3.1.1
bin\Debug\net9.0\ZPL2PDF.xml
true
diff --git a/docs/README.md b/docs/README.md
index dbe6027..290fdf5 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -61,7 +61,7 @@ Documentation for contributors and developers:
- 🔄 [Git Workflow](development/GIT_WORKFLOW_GUIDE.md) - Git workflow and branching strategy
### Internal Documentation
-- 🏗️ [Architecture Overview](development/ARCHITECTURE.md) *(Coming soon)*
+- 🏗️ [Architecture Overview](../wiki/Architecture-Overview.md)
- 📚 [API Documentation](development/API.md) *(Coming soon)*
---
diff --git a/docs/guides/DOCKER_GUIDE.md b/docs/guides/DOCKER_GUIDE.md
index 5388e54..a888aae 100644
--- a/docs/guides/DOCKER_GUIDE.md
+++ b/docs/guides/DOCKER_GUIDE.md
@@ -431,7 +431,7 @@ docker run --rm -e ZPL2PDF_LANGUAGE=es-ES zpl2pdf:2.0.0 /app/ZPL2PDF -help
cat > zpl2pdf.json <List of image data for PDF generation
List ConvertWithExplicitDimensions(string zplContent, double width, double height, string unit, int dpi,
string? fontsDirectory = null,
- IReadOnlyList<(string Id, string Path)>? fontMappings = null);
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null,
+ RendererEngine rendererEngine = RendererEngine.Offline);
///
/// Converts ZPL content to PDF by extracting dimensions from ZPL
@@ -33,7 +35,8 @@ List ConvertWithExplicitDimensions(string zplContent, double width, doub
/// List of image data for PDF generation
List ConvertWithExtractedDimensions(string zplContent, string unit, int dpi,
string? fontsDirectory = null,
- IReadOnlyList<(string Id, string Path)>? fontMappings = null);
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null,
+ RendererEngine rendererEngine = RendererEngine.Offline);
///
/// Converts ZPL content to PDF using mixed approach (explicit or extracted)
@@ -47,6 +50,35 @@ List ConvertWithExtractedDimensions(string zplContent, string unit, int
/// Optional font ID to path mappings
/// List of image data for PDF generation
List Convert(string zplContent, double explicitWidth, double explicitHeight, string unit, int dpi,
+ string? fontsDirectory = null,
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null,
+ RendererEngine rendererEngine = RendererEngine.Offline);
+
+ ///
+ /// Converts the entire ZPL template directly to a PDF using the Labelary API.
+ ///
+ byte[] ConvertPdfDirectWithLabelary(
+ string zplContent,
+ double explicitWidth,
+ double explicitHeight,
+ string unit,
+ int dpi,
+ string? fontsDirectory = null,
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null);
+
+ ///
+ /// Tries direct PDF conversion via Labelary according to renderer policy.
+ /// Returns false when direct mode is not enabled or when auto mode falls back.
+ /// Throws when renderer is Labelary and direct conversion fails.
+ ///
+ bool TryConvertPdfDirectWithLabelary(
+ string zplContent,
+ double explicitWidth,
+ double explicitHeight,
+ string unit,
+ int dpi,
+ RendererEngine rendererEngine,
+ out byte[]? pdfBytes,
string? fontsDirectory = null,
IReadOnlyList<(string Id, string Path)>? fontMappings = null);
}
diff --git a/src/Application/Services/ConversionService.cs b/src/Application/Services/ConversionService.cs
index fcb377e..fa1812e 100644
--- a/src/Application/Services/ConversionService.cs
+++ b/src/Application/Services/ConversionService.cs
@@ -3,6 +3,7 @@
using ZPL2PDF.Application.Interfaces;
using ZPL2PDF.Shared;
using ZPL2PDF.Domain.Services;
+using ZPL2PDF.Shared.Constants;
namespace ZPL2PDF.Application.Services
{
@@ -44,14 +45,19 @@ private List PrepareLabels(string zplContent)
///
public List ConvertWithExplicitDimensions(string zplContent, double width, double height, string unit, int dpi,
string? fontsDirectory = null,
- IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null,
+ RendererEngine rendererEngine = RendererEngine.Offline)
{
var labels = PrepareLabels(zplContent);
if (labels.Count == 0)
return new List();
- var renderer = new LabelRenderer(width, height, dpi, unit, fontsDirectory, fontMappings);
- return renderer.RenderLabels(labels);
+ return rendererEngine switch
+ {
+ RendererEngine.Labelary => new LabelaryRenderer(width, height, unit, dpi).RenderLabels(labels),
+ RendererEngine.Auto => new AutoRenderer(width, height, unit, dpi, fontsDirectory, fontMappings).RenderLabels(labels),
+ _ => new LabelRenderer(width, height, dpi, unit, fontsDirectory, fontMappings).RenderLabels(labels)
+ };
}
///
@@ -59,7 +65,8 @@ public List ConvertWithExplicitDimensions(string zplContent, double widt
///
public List ConvertWithExtractedDimensions(string zplContent, string unit, int dpi,
string? fontsDirectory = null,
- IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null,
+ RendererEngine rendererEngine = RendererEngine.Offline)
{
var labels = PrepareLabels(zplContent);
if (labels.Count == 0)
@@ -73,8 +80,12 @@ public List ConvertWithExtractedDimensions(string zplContent, string uni
var label = labels[i];
var labelDimensions = i < extractedDimensionsList.Count ? extractedDimensionsList[i] : extractedDimensionsList[0];
var finalDimensions = _dimensionExtractor.ApplyPriorityLogic(null, null, unit, labelDimensions, dpi);
- var labelRenderer = new LabelRenderer(finalDimensions, fontsDirectory, fontMappings);
- var labelImages = labelRenderer.RenderLabels(new List { label });
+ var labelImages = rendererEngine switch
+ {
+ RendererEngine.Labelary => new LabelaryRenderer(finalDimensions).RenderLabels(new List { label }),
+ RendererEngine.Auto => new AutoRenderer(finalDimensions, fontsDirectory, fontMappings).RenderLabels(new List { label }),
+ _ => new LabelRenderer(finalDimensions, fontsDirectory, fontMappings).RenderLabels(new List { label })
+ };
allImageData.AddRange(labelImages);
}
@@ -86,12 +97,94 @@ public List ConvertWithExtractedDimensions(string zplContent, string uni
///
public List Convert(string zplContent, double explicitWidth, double explicitHeight, string unit, int dpi,
string? fontsDirectory = null,
- IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null,
+ RendererEngine rendererEngine = RendererEngine.Offline)
{
bool hasExplicitDimensions = explicitWidth > 0 && explicitHeight > 0;
if (hasExplicitDimensions)
- return ConvertWithExplicitDimensions(zplContent, explicitWidth, explicitHeight, unit, dpi, fontsDirectory, fontMappings);
- return ConvertWithExtractedDimensions(zplContent, unit, dpi, fontsDirectory, fontMappings);
+ return ConvertWithExplicitDimensions(zplContent, explicitWidth, explicitHeight, unit, dpi, fontsDirectory, fontMappings, rendererEngine);
+ return ConvertWithExtractedDimensions(zplContent, unit, dpi, fontsDirectory, fontMappings, rendererEngine);
}
+
+ ///
+ /// Converts the entire ZPL template directly to a PDF using the Labelary API
+ /// (Labelary returns application/pdf when requested via Accept header).
+ ///
+ public byte[] ConvertPdfDirectWithLabelary(
+ string zplContent,
+ double explicitWidth,
+ double explicitHeight,
+ string unit,
+ int dpi,
+ string? fontsDirectory = null,
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ {
+ // Labelary handles label splitting internally for PDF requests (index omitted).
+ // We still preprocess for compatibility with BinaryKits workarounds (e.g. ^B0 -> ^BO).
+ var processedContent = LabelFileReader.PreprocessZpl(zplContent);
+
+ if (explicitWidth > 0 && explicitHeight > 0)
+ {
+ return new LabelaryRenderer(explicitWidth, explicitHeight, unit, dpi).RenderPdf(processedContent);
+ }
+
+ // Otherwise, extract dimensions and use the first label as a best-effort size for Labelary.
+ var extractedDimensionsList = _dimensionExtractor.ExtractDimensions(zplContent);
+ var firstExtracted = extractedDimensionsList.Count > 0
+ ? extractedDimensionsList[0]
+ : _dimensionExtractor.GetDefaultDimensions();
+
+ var finalDimensions = _dimensionExtractor.ApplyPriorityLogic(
+ explicitWidth: null,
+ explicitHeight: null,
+ explicitUnit: unit,
+ zplDimensions: firstExtracted,
+ dpi: dpi);
+
+ // Labelary width/height are expressed in inches; we pass mm and let LabelaryRenderer convert.
+ return new LabelaryRenderer(finalDimensions.WidthMm, finalDimensions.HeightMm, "mm", dpi)
+ .RenderPdf(processedContent);
+ }
+
+ public bool TryConvertPdfDirectWithLabelary(
+ string zplContent,
+ double explicitWidth,
+ double explicitHeight,
+ string unit,
+ int dpi,
+ RendererEngine rendererEngine,
+ out byte[]? pdfBytes,
+ string? fontsDirectory = null,
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ {
+ pdfBytes = null;
+ if (rendererEngine != RendererEngine.Labelary && rendererEngine != RendererEngine.Auto)
+ {
+ return false;
+ }
+
+ try
+ {
+ pdfBytes = ConvertPdfDirectWithLabelary(
+ zplContent,
+ explicitWidth,
+ explicitHeight,
+ unit,
+ dpi,
+ fontsDirectory,
+ fontMappings);
+ return true;
+ }
+ catch
+ {
+ if (rendererEngine == RendererEngine.Labelary)
+ {
+ throw;
+ }
+
+ return false;
+ }
+ }
+
}
}
diff --git a/src/Application/Services/FileValidationService.cs b/src/Application/Services/FileValidationService.cs
index a32a197..c51898b 100644
--- a/src/Application/Services/FileValidationService.cs
+++ b/src/Application/Services/FileValidationService.cs
@@ -12,7 +12,7 @@ public class FileValidationService : IFileValidationService
///
/// Valid file extensions for ZPL processing
///
- private static readonly string[] ValidExtensions = { ".txt", ".prn" };
+ private static readonly string[] ValidExtensions = { ".txt", ".prn", ".zpl", ".imp" };
///
/// Checks if the file is valid for processing
diff --git a/src/Domain/Services/IDimensionExtractor.cs b/src/Domain/Services/IDimensionExtractor.cs
index 3471a58..e0b7784 100644
--- a/src/Domain/Services/IDimensionExtractor.cs
+++ b/src/Domain/Services/IDimensionExtractor.cs
@@ -12,14 +12,14 @@ public interface IDimensionExtractor
///
/// ZPL content to analyze
/// List of extracted dimensions for each label
- List ExtractDimensions(string zplContent);
+ List ExtractDimensions(string zplContent);
///
/// Extracts dimensions from a single ZPL label
///
/// Single ZPL label string
/// Extracted dimensions or null if not found
- LabelDimensions ExtractDimensionsFromLabel(string zplLabel);
+ global::ZPL2PDF.LabelDimensions ExtractDimensionsFromLabel(string zplLabel);
///
/// Applies priority logic to determine final dimensions
@@ -30,7 +30,12 @@ public interface IDimensionExtractor
/// Extracted dimensions from ZPL
/// DPI to use for conversions
/// Final dimensions to use
- LabelDimensions ApplyPriorityLogic(double? explicitWidth, double? explicitHeight, string unit, LabelDimensions extractedDimensions, int dpi = 203);
+ global::ZPL2PDF.LabelDimensions ApplyPriorityLogic(
+ double? explicitWidth,
+ double? explicitHeight,
+ string unit,
+ global::ZPL2PDF.LabelDimensions extractedDimensions,
+ int dpi = 203);
///
/// Converts points to millimeters
@@ -41,14 +46,4 @@ public interface IDimensionExtractor
double ConvertPointsToMm(int points, int dpi = 203);
}
- ///
- /// Represents label dimensions
- ///
- public class LabelDimensions
- {
- public double Width { get; set; }
- public double Height { get; set; }
- public string Unit { get; set; } = "mm";
- public int Dpi { get; set; } = 203;
- }
}
diff --git a/src/Domain/ValueObjects/DaemonConfiguration.cs b/src/Domain/ValueObjects/DaemonConfiguration.cs
index ec26aba..4ff94d1 100644
--- a/src/Domain/ValueObjects/DaemonConfiguration.cs
+++ b/src/Domain/ValueObjects/DaemonConfiguration.cs
@@ -61,7 +61,7 @@ public class DaemonConfiguration
///
/// Gets or sets the file filter pattern
///
- public string FileFilter { get; set; } = "*.txt;*.prn";
+ public string FileFilter { get; set; } = "*.txt;*.prn;*.zpl;*.imp";
///
/// Gets or sets the language/culture code (e.g., "pt-BR", "en-US", "es-ES")
diff --git a/src/Domain/ValueObjects/FileInfo.cs b/src/Domain/ValueObjects/FileInfo.cs
index 3441f70..df24980 100644
--- a/src/Domain/ValueObjects/FileInfo.cs
+++ b/src/Domain/ValueObjects/FileInfo.cs
@@ -139,7 +139,7 @@ public bool IsValid()
/// True if valid, False otherwise
public bool IsValidExtension()
{
- var validExtensions = new[] { ".txt", ".prn" };
+ var validExtensions = new[] { ".txt", ".prn", ".zpl", ".imp" };
return Array.Exists(validExtensions, ext =>
ext.Equals(Extension, StringComparison.OrdinalIgnoreCase));
}
@@ -157,7 +157,7 @@ public string GetValidationError()
return "File extension cannot be null or empty";
if (!IsValidExtension())
- return $"Invalid file extension: {Extension}. Valid extensions are: .txt, .prn";
+ return $"Invalid file extension: {Extension}. Valid extensions are: .txt, .prn, .zpl, .imp";
return string.Empty;
}
diff --git a/src/Domain/ValueObjects/LabelDimensions.cs b/src/Domain/ValueObjects/LabelDimensions.cs
deleted file mode 100644
index 6846832..0000000
--- a/src/Domain/ValueObjects/LabelDimensions.cs
+++ /dev/null
@@ -1,233 +0,0 @@
-using System;
-using ZPL2PDF.Shared.Constants;
-
-namespace ZPL2PDF.Domain.ValueObjects
-{
- ///
- /// Represents label dimensions with validation and conversion methods
- ///
- public class LabelDimensions
- {
- ///
- /// Gets or sets the width
- ///
- public double Width { get; set; }
-
- ///
- /// Gets or sets the height
- ///
- public double Height { get; set; }
-
- ///
- /// Gets or sets the unit of measurement
- ///
- public string Unit { get; set; } = "mm";
-
- ///
- /// Gets or sets the print density in DPI
- ///
- public int Dpi { get; set; } = 203;
-
- ///
- /// Initializes a new instance of LabelDimensions
- ///
- public LabelDimensions()
- {
- }
-
- ///
- /// Initializes a new instance of LabelDimensions with values
- ///
- /// Width value
- /// Height value
- /// Unit of measurement
- /// Print density
- public LabelDimensions(double width, double height, string unit = "mm", int dpi = 203)
- {
- Width = width;
- Height = height;
- Unit = unit;
- Dpi = dpi;
- }
-
- ///
- /// Validates the label dimensions
- ///
- /// True if valid, False otherwise
- public bool IsValid()
- {
- if (Width <= 0)
- return false;
-
- if (Height <= 0)
- return false;
-
- if (string.IsNullOrWhiteSpace(Unit))
- return false;
-
- if (Dpi <= 0)
- return false;
-
- return IsValidUnit(Unit);
- }
-
- ///
- /// Gets validation error message
- ///
- /// Error message if invalid, empty string if valid
- public string GetValidationError()
- {
- if (Width <= 0)
- return "Width must be greater than 0";
-
- if (Height <= 0)
- return "Height must be greater than 0";
-
- if (string.IsNullOrWhiteSpace(Unit))
- return "Unit cannot be null or empty";
-
- if (Dpi <= 0)
- return "DPI must be greater than 0";
-
- if (!IsValidUnit(Unit))
- return $"Invalid unit: {Unit}. Valid units are: mm, cm, in";
-
- return string.Empty;
- }
-
- ///
- /// Checks if the unit is valid
- ///
- /// Unit to check
- /// True if valid, False otherwise
- public static bool IsValidUnit(string unit)
- {
- if (string.IsNullOrWhiteSpace(unit))
- return false;
-
- var validUnits = new[] { "mm", "cm", "in" };
- return Array.Exists(validUnits, u =>
- u.Equals(unit, StringComparison.OrdinalIgnoreCase));
- }
-
- ///
- /// Converts dimensions to millimeters
- ///
- /// New LabelDimensions instance in millimeters
- public LabelDimensions ToMillimeters()
- {
- if (Unit.Equals("mm", StringComparison.OrdinalIgnoreCase))
- return Clone();
-
- var widthMm = ConvertToMillimeters(Width, Unit);
- var heightMm = ConvertToMillimeters(Height, Unit);
-
- return new LabelDimensions(widthMm, heightMm, "mm", Dpi);
- }
-
- ///
- /// Converts dimensions to centimeters
- ///
- /// New LabelDimensions instance in centimeters
- public LabelDimensions ToCentimeters()
- {
- var mmDimensions = ToMillimeters();
- return new LabelDimensions(
- mmDimensions.Width / 10.0,
- mmDimensions.Height / 10.0,
- "cm",
- Dpi
- );
- }
-
- ///
- /// Converts dimensions to inches
- ///
- /// New LabelDimensions instance in inches
- public LabelDimensions ToInches()
- {
- var mmDimensions = ToMillimeters();
- return new LabelDimensions(
- mmDimensions.Width / 25.4,
- mmDimensions.Height / 25.4,
- "in",
- Dpi
- );
- }
-
- ///
- /// Converts a value to millimeters based on unit
- ///
- /// Value to convert
- /// Source unit
- /// Value in millimeters
- private static double ConvertToMillimeters(double value, string fromUnit)
- {
- return fromUnit.ToLowerInvariant() switch
- {
- "mm" => value,
- "cm" => value * 10.0,
- "in" => value * 25.4,
- _ => value
- };
- }
-
- ///
- /// Converts dimensions to points (for PDF generation)
- ///
- /// Tuple with width and height in points
- public (int widthPoints, int heightPoints) ToPoints()
- {
- var mmDimensions = ToMillimeters();
- var widthPoints = (int)Math.Round((mmDimensions.Width / 25.4) * Dpi);
- var heightPoints = (int)Math.Round((mmDimensions.Height / 25.4) * Dpi);
- return (widthPoints, heightPoints);
- }
-
- ///
- /// Creates a copy of the label dimensions
- ///
- /// New instance with same values
- public LabelDimensions Clone()
- {
- return new LabelDimensions(Width, Height, Unit, Dpi);
- }
-
- ///
- /// Returns a string representation of the label dimensions
- ///
- /// String representation
- public override string ToString()
- {
- return $"LabelDimensions: {Width}x{Height} {Unit}, Print Density: {ApplicationConstants.ConvertDpiToDpmm(ApplicationConstants.DEFAULT_DPI):F1} dpmm ({ApplicationConstants.DEFAULT_DPI} dpi)";
- }
-
- ///
- /// Determines if two LabelDimensions instances are equal
- ///
- /// Object to compare
- /// True if equal, False otherwise
- public override bool Equals(object? obj)
- {
- if (obj is LabelDimensions other)
- {
- return Math.Abs(Width - other.Width) < 0.001 &&
- Math.Abs(Height - other.Height) < 0.001 &&
- Unit.Equals(other.Unit, StringComparison.OrdinalIgnoreCase) &&
- Dpi == other.Dpi;
- }
- return false;
- }
-
- ///
- /// Gets hash code for the label dimensions
- ///
- /// Hash code
- public override int GetHashCode()
- {
- // Align with Equals: unit comparison is ordinal-ignore-case.
- var unitKey = Unit?.ToLowerInvariant() ?? string.Empty;
- return HashCode.Combine(Width, Height, unitKey, Dpi);
- }
- }
-}
diff --git a/src/Infrastructure/DaemonManager.cs b/src/Infrastructure/DaemonManager.cs
index 7282c78..b49c9e2 100644
--- a/src/Infrastructure/DaemonManager.cs
+++ b/src/Infrastructure/DaemonManager.cs
@@ -275,9 +275,7 @@ private void SaveDaemonInfoToFile(int pid)
ProcessId = pid
};
- // Get the directory where the PID file is stored
- var pidFilePath = _pidManager.GetType().GetField("_pidFilePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(_pidManager)?.ToString();
- var infoFilePath = Path.Combine(Path.GetDirectoryName(pidFilePath ?? ""), "zpl2pdf.info");
+ var infoFilePath = GetDaemonInfoFilePath();
// Save daemon info to file in a simple format
var lines = new[]
@@ -307,9 +305,7 @@ private DaemonInfo GetDaemonInfoFromFile()
{
try
{
- // Get the directory where the PID file is stored
- var pidFilePath = _pidManager.GetType().GetField("_pidFilePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(_pidManager)?.ToString();
- var infoFilePath = Path.Combine(Path.GetDirectoryName(pidFilePath ?? ""), "zpl2pdf.info");
+ var infoFilePath = GetDaemonInfoFilePath();
if (!File.Exists(infoFilePath))
{
@@ -364,5 +360,16 @@ private DaemonInfo GetDaemonInfoFromFile()
return null;
}
}
+
+ private string GetDaemonInfoFilePath()
+ {
+ var pidDirectory = Path.GetDirectoryName(_pidManager.PidFilePath);
+ if (string.IsNullOrWhiteSpace(pidDirectory))
+ {
+ return "zpl2pdf.info";
+ }
+
+ return Path.Combine(pidDirectory, "zpl2pdf.info");
+ }
}
}
\ No newline at end of file
diff --git a/src/Infrastructure/FileSystem/FolderMonitor.cs b/src/Infrastructure/FileSystem/FolderMonitor.cs
index e555d05..8dabde6 100644
--- a/src/Infrastructure/FileSystem/FolderMonitor.cs
+++ b/src/Infrastructure/FileSystem/FolderMonitor.cs
@@ -6,7 +6,7 @@
namespace ZPL2PDF
{
///
- /// Monitors a folder to detect .txt and .prn files and adds them to the processing queue
+ /// Monitors a folder to detect ZPL text files and adds them to the processing queue.
///
public class FolderMonitor : IDisposable
{
@@ -110,7 +110,7 @@ public void StartWatching()
Console.WriteLine($"Polling timer started (interval: {_pollingIntervalMs}ms)");
Console.WriteLine($"Monitoring folder: {_listenFolder}");
- Console.WriteLine($"File types: .txt, .prn");
+ Console.WriteLine($"File types: .txt, .prn, .zpl, .imp");
Console.WriteLine($"Dimensions: {(_useFixedDimensions ? "Fixed" : "Extracted from ZPL")}");
if (_useFixedDimensions)
@@ -399,7 +399,7 @@ private async Task ProcessFileAsync(string filePath, string eventType)
private bool IsValidFile(string filePath)
{
var extension = Path.GetExtension(filePath).ToLowerInvariant();
- return extension == ".txt" || extension == ".prn";
+ return extension == ".txt" || extension == ".prn" || extension == ".zpl" || extension == ".imp";
}
///
diff --git a/src/Infrastructure/PidManager.cs b/src/Infrastructure/PidManager.cs
index 727ec79..41deb19 100644
--- a/src/Infrastructure/PidManager.cs
+++ b/src/Infrastructure/PidManager.cs
@@ -9,6 +9,7 @@ namespace ZPL2PDF
public class PidManager
{
private readonly string _pidFilePath;
+ public string PidFilePath => _pidFilePath;
///
/// Initializes a new instance of the PidManager (default PID file: zpl2pdf.pid).
diff --git a/src/Infrastructure/Processing/ProcessingQueue.cs b/src/Infrastructure/Processing/ProcessingQueue.cs
index 051fc29..6c1051b 100644
--- a/src/Infrastructure/Processing/ProcessingQueue.cs
+++ b/src/Infrastructure/Processing/ProcessingQueue.cs
@@ -2,6 +2,10 @@
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
+using ZPL2PDF.Application.Interfaces;
+using ZPL2PDF.Application.Services;
+using ZPL2PDF.Domain.Services;
+using ZPL2PDF.Shared.Constants;
namespace ZPL2PDF
{
@@ -17,6 +21,8 @@ public class ProcessingQueue : IDisposable
private readonly ZplDimensionExtractor _dimensionExtractor;
private readonly ConfigManager _configManager;
private readonly string? _customOutputFolder;
+ private readonly RendererEngine _rendererEngine;
+ private readonly IConversionService _conversionService;
private bool _isDisposed = false;
private bool _isProcessing = false;
@@ -50,11 +56,35 @@ public ProcessingQueue(ZplDimensionExtractor dimensionExtractor, ConfigManager c
_dimensionExtractor = dimensionExtractor;
_configManager = configManager;
_customOutputFolder = customOutputFolder;
+ _rendererEngine = RendererEngine.Offline;
+ _conversionService = new ConversionService();
// Start processing task
_processingTask = Task.Run(ProcessQueueAsync);
}
+ ///
+ /// ProcessingQueue constructor (with renderer engine).
+ ///
+ public ProcessingQueue(
+ ZplDimensionExtractor dimensionExtractor,
+ ConfigManager configManager,
+ int maxConcurrentFiles,
+ string? customOutputFolder,
+ RendererEngine rendererEngine)
+ {
+ _queue = new ConcurrentQueue();
+ _semaphore = new SemaphoreSlim(maxConcurrentFiles, maxConcurrentFiles);
+ _cancellationTokenSource = new CancellationTokenSource();
+ _dimensionExtractor = dimensionExtractor;
+ _configManager = configManager;
+ _customOutputFolder = customOutputFolder;
+ _rendererEngine = rendererEngine;
+ _conversionService = new ConversionService();
+
+ _processingTask = Task.Run(ProcessQueueAsync);
+ }
+
///
/// Adds a file to the processing queue
///
@@ -207,22 +237,40 @@ private async Task ConvertFileAsync(ProcessingItem item)
item.Dimensions = finalDimensions;
}
-
- for (int i = 0; i < labels.Count; i++)
+
+ // Output file info (used by both Labelary-direct PDF and PNG->PDF fallback).
+ var outputFileName = Path.ChangeExtension(item.FileName, ".pdf");
+ var outputFolder = _customOutputFolder ?? Path.GetDirectoryName(item.FilePath)!;
+ var outputPath = Path.Combine(outputFolder, outputFileName);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
+
+ // If requested, try to return PDF directly from Labelary (no PNG composition).
+ if (_conversionService.TryConvertPdfDirectWithLabelary(
+ item.Content,
+ finalDimensions.WidthMm,
+ finalDimensions.HeightMm,
+ "mm",
+ finalDimensions.Dpi,
+ _rendererEngine,
+ out var directPdfBytes))
{
- var label = labels[i];
-
- // Use the computed dimensions for this file.
- // Debug: Log what we're using
- //Console.WriteLine($"DEBUG - Using dimensions for label {i + 1}: {finalDimensions.WidthMm:F1}mm x {finalDimensions.HeightMm:F1}mm [{finalDimensions.Source}]");
-
- // Create specific renderer for this label
- var labelRenderer = new LabelRenderer(finalDimensions);
- var labelImages = labelRenderer.RenderLabels(new List { label });
- allImageData.AddRange(labelImages);
-
- Console.WriteLine($"Label {i + 1}: {finalDimensions.WidthMm:F1}mm x {finalDimensions.HeightMm:F1}mm [{finalDimensions.Source}]");
+ File.WriteAllBytes(outputPath, directPdfBytes!);
+ var directFileInfo = new FileInfo(outputPath);
+ Console.WriteLine($"PDF generated successfully: {outputFileName} ({directFileInfo.Length} bytes)");
+ return true;
}
+
+ var renderer = _rendererEngine switch
+ {
+ RendererEngine.Labelary => (ILabelRenderer)new LabelaryRenderer(finalDimensions),
+ RendererEngine.Auto => (ILabelRenderer)new AutoRenderer(finalDimensions),
+ _ => (ILabelRenderer)new LabelRenderer(finalDimensions)
+ };
+
+ var labelImages = renderer.RenderLabels(labels);
+ allImageData.AddRange(labelImages);
+ Console.WriteLine($"Rendered {labels.Count} label(s) using renderer: {_rendererEngine}");
if (allImageData.Count == 0)
{
@@ -230,17 +278,8 @@ private async Task ConvertFileAsync(ProcessingItem item)
return false;
}
- // Generate PDF
- var outputFileName = Path.ChangeExtension(item.FileName, ".pdf");
- var outputFolder = _customOutputFolder ?? Path.GetDirectoryName(item.FilePath)!;
- var outputPath = Path.Combine(outputFolder, outputFileName);
-
Console.WriteLine($"Output folder: {outputFolder}");
Console.WriteLine($"Output path: {outputPath}");
-
- // Ensure destination folder exists
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
-
Console.WriteLine($"Calling PdfGenerator.GeneratePdf with {allImageData.Count} images...");
PdfGenerator.GeneratePdf(allImageData, outputPath);
diff --git a/src/Infrastructure/Rendering/AutoRenderer.cs b/src/Infrastructure/Rendering/AutoRenderer.cs
new file mode 100644
index 0000000..65658cb
--- /dev/null
+++ b/src/Infrastructure/Rendering/AutoRenderer.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using ZPL2PDF.Domain.Services;
+
+namespace ZPL2PDF
+{
+ ///
+ /// Renderer that tries Labelary first and falls back to BinaryKits offline rendering per label.
+ ///
+ public class AutoRenderer : ILabelRenderer
+ {
+ private readonly LabelaryRenderer _labelaryRenderer;
+ private readonly LabelRenderer _offlineRenderer;
+
+ public AutoRenderer(
+ double labelWidth,
+ double labelHeight,
+ string unit,
+ int dpi,
+ string? fontsDirectory = null,
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ {
+ _labelaryRenderer = new LabelaryRenderer(labelWidth, labelHeight, unit, dpi);
+ _offlineRenderer = new LabelRenderer(labelWidth, labelHeight, dpi, unit, fontsDirectory, fontMappings);
+ }
+
+ public AutoRenderer(
+ LabelDimensions dimensions,
+ string? fontsDirectory = null,
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ {
+ _labelaryRenderer = new LabelaryRenderer(dimensions);
+ _offlineRenderer = new LabelRenderer(dimensions, fontsDirectory, fontMappings);
+ }
+
+ public (double width, double height, string unit, int dpi) GetDimensions()
+ => _offlineRenderer.GetDimensions();
+
+ public List RenderLabels(List labels)
+ {
+ if (labels == null) throw new ArgumentNullException(nameof(labels));
+
+ var images = new List();
+ foreach (var label in labels)
+ {
+ images.Add(RenderSingleLabel(label));
+ }
+ return images;
+ }
+
+ private byte[] RenderSingleLabel(string zpl)
+ {
+ try
+ {
+ var res = _labelaryRenderer.RenderLabels(new List { zpl });
+ return res.Count > 0 ? res[0] : Array.Empty();
+ }
+ catch
+ {
+ var res = _offlineRenderer.RenderLabels(new List { zpl });
+ return res.Count > 0 ? res[0] : Array.Empty();
+ }
+ }
+ }
+}
+
diff --git a/src/Infrastructure/Rendering/LabelRenderer.cs b/src/Infrastructure/Rendering/LabelRenderer.cs
index b1b2e25..04ead94 100644
--- a/src/Infrastructure/Rendering/LabelRenderer.cs
+++ b/src/Infrastructure/Rendering/LabelRenderer.cs
@@ -6,18 +6,23 @@
using BinaryKits.Zpl.Viewer;
using BinaryKits.Zpl.Viewer.ElementDrawers;
using SkiaSharp;
+using ZPL2PDF.Domain.Services;
namespace ZPL2PDF {
///
/// Responsible for processing labels, generating images in memory, and returning image data.
///
- public class LabelRenderer {
+ public class LabelRenderer : ILabelRenderer {
private readonly IPrinterStorage _printerStorage;
private readonly ZplAnalyzer _analyzer;
private readonly ZplElementDrawer _drawer;
private readonly double _labelWidthMm;
private readonly double _labelHeightMm;
private readonly int _printDpi;
+ private readonly double _labelWidthInput;
+ private readonly double _labelHeightInput;
+ private readonly string _labelUnitInput;
+ private readonly int _labelDpi;
private readonly string? _fontsDirectory;
private readonly IReadOnlyList<(string Id, string Path)>? _fontMappings;
@@ -113,6 +118,10 @@ public LabelRenderer(double labelWidth, double labelHeight, int printDpi, string
// Store DPI (will be converted to DPMM when rendering)
_printDpi = printDpi;
+ _labelWidthInput = labelWidth;
+ _labelHeightInput = labelHeight;
+ _labelUnitInput = unit;
+ _labelDpi = printDpi;
}
///
@@ -132,6 +141,10 @@ public LabelRenderer(LabelDimensions dimensions,
_labelWidthMm = dimensions.WidthMm;
_labelHeightMm = dimensions.HeightMm;
_printDpi = dimensions.Dpi;
+ _labelWidthInput = dimensions.WidthMm;
+ _labelHeightInput = dimensions.HeightMm;
+ _labelUnitInput = "mm";
+ _labelDpi = dimensions.Dpi;
}
///
@@ -178,5 +191,11 @@ public List RenderLabels(List labels) {
}
return images;
}
+
+ ///
+ public (double width, double height, string unit, int dpi) GetDimensions()
+ {
+ return (_labelWidthInput, _labelHeightInput, _labelUnitInput, _labelDpi);
+ }
}
}
\ No newline at end of file
diff --git a/src/Infrastructure/Rendering/LabelaryRenderer.cs b/src/Infrastructure/Rendering/LabelaryRenderer.cs
new file mode 100644
index 0000000..cec3b4c
--- /dev/null
+++ b/src/Infrastructure/Rendering/LabelaryRenderer.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using ZPL2PDF.Domain.Services;
+
+namespace ZPL2PDF
+{
+ ///
+ /// Renders ZPL labels using Labelary online API (returns PNG bytes).
+ ///
+ public class LabelaryRenderer : ILabelRenderer
+ {
+ private static readonly HttpClient HttpClient = new HttpClient
+ {
+ Timeout = TimeSpan.FromSeconds(30)
+ };
+
+ private static readonly int[] AllowedDpmm = { 6, 8, 12, 24 };
+
+ private readonly double _labelWidth;
+ private readonly double _labelHeight;
+ private readonly string _unit;
+ private readonly int _dpi;
+
+ public LabelaryRenderer(double labelWidth, double labelHeight, string unit, int dpi)
+ {
+ _labelWidth = labelWidth;
+ _labelHeight = labelHeight;
+ _unit = unit;
+ _dpi = dpi;
+ }
+
+ // Extracted dimensions are already in millimeters.
+ public LabelaryRenderer(LabelDimensions dimensions)
+ {
+ _labelWidth = dimensions.WidthMm;
+ _labelHeight = dimensions.HeightMm;
+ _unit = "mm";
+ _dpi = dimensions.Dpi;
+ }
+
+ public (double width, double height, string unit, int dpi) GetDimensions()
+ => (_labelWidth, _labelHeight, _unit, _dpi);
+
+ public List RenderLabels(List labels)
+ {
+ if (labels == null) throw new ArgumentNullException(nameof(labels));
+
+ var images = new List();
+ foreach (var label in labels)
+ {
+ images.Add(RenderSingleLabel(label));
+ }
+ return images;
+ }
+
+ ///
+ /// Renders the entire ZPL template to a PDF directly from Labelary.
+ /// This requests application/pdf and omits the label index so Labelary can include all labels.
+ ///
+ public byte[] RenderPdf(string zpl)
+ {
+ if (string.IsNullOrWhiteSpace(zpl))
+ throw new ArgumentException("ZPL label content is required.", nameof(zpl));
+
+ var dpmm = MapDpiToDpmm(_dpi);
+ var widthInches = ConvertToInches(_labelWidth, _unit);
+ var heightInches = ConvertToInches(_labelHeight, _unit);
+
+ // For PDF requests, the index parameter is optional.
+ // If omitted, Labelary returns a PDF with all labels (one page per label).
+ var width = widthInches.ToString("0.###", CultureInfo.InvariantCulture);
+ var height = heightInches.ToString("0.###", CultureInfo.InvariantCulture);
+
+ var url = $"http://api.labelary.com/v1/printers/{dpmm}dpmm/labels/{width}x{height}/";
+
+ using var content = new StringContent(zpl, Encoding.UTF8, "application/x-www-form-urlencoded");
+ using var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
+
+ request.Headers.Accept.Clear();
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/pdf"));
+
+ using var response = HttpClient.SendAsync(request).GetAwaiter().GetResult();
+ var bytes = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var msg = Encoding.UTF8.GetString(bytes);
+ throw new InvalidOperationException($"Labelary request failed (HTTP {(int)response.StatusCode}): {msg}");
+ }
+
+ return bytes;
+ }
+
+ private byte[] RenderSingleLabel(string zpl)
+ {
+ if (string.IsNullOrWhiteSpace(zpl))
+ throw new ArgumentException("ZPL label content is required.", nameof(zpl));
+
+ var dpmm = MapDpiToDpmm(_dpi);
+ var widthInches = ConvertToInches(_labelWidth, _unit);
+ var heightInches = ConvertToInches(_labelHeight, _unit);
+
+ // Labelary expects width/height in inches in the URL (any numeric value is allowed).
+ var width = widthInches.ToString("0.###", CultureInfo.InvariantCulture);
+ var height = heightInches.ToString("0.###", CultureInfo.InvariantCulture);
+
+ // Labelary docs (service.html): POST http://api.labelary.com/v1/printers/{dpmm}dpmm/labels/{width}x{height}/{index}/
+ var url = $"http://api.labelary.com/v1/printers/{dpmm}dpmm/labels/{width}x{height}/0/";
+
+ using var content = new StringContent(zpl, Encoding.UTF8, "application/x-www-form-urlencoded");
+
+ using var request = new HttpRequestMessage(HttpMethod.Post, url)
+ {
+ Content = content
+ };
+
+ request.Headers.Accept.Clear();
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("image/png"));
+
+ using var response = HttpClient.SendAsync(request).GetAwaiter().GetResult();
+ var bytes = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
+
+ if (!response.IsSuccessStatusCode)
+ {
+ // Error bodies are small (typically UTF-8 text).
+ var msg = Encoding.UTF8.GetString(bytes);
+ throw new InvalidOperationException($"Labelary request failed (HTTP {(int)response.StatusCode}): {msg}");
+ }
+
+ return bytes;
+ }
+
+ private static double ConvertToInches(double value, string unit)
+ {
+ if (unit.Equals("in", StringComparison.OrdinalIgnoreCase))
+ return value;
+ if (unit.Equals("cm", StringComparison.OrdinalIgnoreCase))
+ return value / 2.54;
+ if (unit.Equals("mm", StringComparison.OrdinalIgnoreCase))
+ return value / 25.4;
+
+ // Fallback: treat as mm.
+ return value / 25.4;
+ }
+
+ private static int MapDpiToDpmm(int dpi)
+ {
+ // dpmm = dpi / 25.4, but Labelary only accepts 6/8/12/24.
+ var dpmmRaw = dpi / 25.4;
+ var best = AllowedDpmm[0];
+ var bestDelta = Math.Abs(best - dpmmRaw);
+
+ foreach (var candidate in AllowedDpmm)
+ {
+ var delta = Math.Abs(candidate - dpmmRaw);
+ if (delta < bestDelta)
+ {
+ best = candidate;
+ bestDelta = delta;
+ }
+ }
+
+ return best;
+ }
+ }
+}
+
diff --git a/src/Infrastructure/Rendering/PdfGenerator.cs b/src/Infrastructure/Rendering/PdfGenerator.cs
index 902387a..9aeffa7 100644
--- a/src/Infrastructure/Rendering/PdfGenerator.cs
+++ b/src/Infrastructure/Rendering/PdfGenerator.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.IO;
using PdfSharp.Pdf;
+using PdfSharp.Pdf.IO;
using PdfSharp.Drawing;
namespace ZPL2PDF {
@@ -65,5 +66,31 @@ public static byte[] GeneratePdfToBytes(List imageDataList) {
}
}
}
+
+ ///
+ /// Merges multiple PDF documents (given as bytes) into a single PDF.
+ ///
+ public static byte[] MergePdfsToBytes(List pdfDocuments)
+ {
+ using var outputDocument = new PdfDocument();
+
+ foreach (var pdfBytes in pdfDocuments)
+ {
+ if (pdfBytes == null || pdfBytes.Length == 0)
+ continue;
+
+ using var ms = new MemoryStream(pdfBytes);
+ using var inputDocument = PdfReader.Open(ms, PdfDocumentOpenMode.Import);
+
+ for (int i = 0; i < inputDocument.PageCount; i++)
+ {
+ outputDocument.AddPage(inputDocument.Pages[i]);
+ }
+ }
+
+ using var outStream = new MemoryStream();
+ outputDocument.Save(outStream, false);
+ return outStream.ToArray();
+ }
}
}
\ No newline at end of file
diff --git a/src/Infrastructure/TcpServer/TcpPrinterServer.cs b/src/Infrastructure/TcpServer/TcpPrinterServer.cs
index 283c15e..1fe6f49 100644
--- a/src/Infrastructure/TcpServer/TcpPrinterServer.cs
+++ b/src/Infrastructure/TcpServer/TcpPrinterServer.cs
@@ -6,6 +6,7 @@
using System.Text;
using System.Threading;
using ZPL2PDF.Application.Services;
+using ZPL2PDF.Shared.Constants;
namespace ZPL2PDF
{
@@ -17,17 +18,19 @@ public class TcpPrinterServer
private readonly int _port;
private readonly string _outputFolder;
private readonly ConversionService _conversionService;
+ private readonly RendererEngine _rendererEngine;
private TcpListener? _listener;
private volatile bool _running;
private const int ReadTimeoutMs = 30000;
private const int DefaultDpi = 203;
private const string DefaultUnit = "mm";
- public TcpPrinterServer(int port, string outputFolder)
+ public TcpPrinterServer(int port, string outputFolder, RendererEngine rendererEngine = RendererEngine.Offline)
{
_port = port;
_outputFolder = outputFolder ?? throw new ArgumentNullException(nameof(outputFolder));
_conversionService = new ConversionService();
+ _rendererEngine = rendererEngine;
}
///
@@ -109,15 +112,31 @@ private void ProcessClient(TcpClient client)
try
{
- var imageDataList = _conversionService.ConvertWithExtractedDimensions(zplContent, DefaultUnit, DefaultDpi);
- if (imageDataList == null || imageDataList.Count == 0)
+ var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
+ var fileName = $"ZPL2PDF_TCP_{timestamp}.pdf";
+ var outputPath = Path.Combine(_outputFolder, fileName);
+
+ if (_conversionService.TryConvertPdfDirectWithLabelary(
+ zplContent,
+ explicitWidth: 0,
+ explicitHeight: 0,
+ unit: DefaultUnit,
+ dpi: DefaultDpi,
+ rendererEngine: _rendererEngine,
+ out var pdfBytes))
{
+ File.WriteAllBytes(outputPath, pdfBytes!);
return;
}
- var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
- var fileName = $"ZPL2PDF_TCP_{timestamp}.pdf";
- var outputPath = Path.Combine(_outputFolder, fileName);
+ var imageDataList = _conversionService.ConvertWithExtractedDimensions(
+ zplContent,
+ DefaultUnit,
+ DefaultDpi,
+ rendererEngine: _rendererEngine);
+ if (imageDataList == null || imageDataList.Count == 0)
+ return;
+
PdfGenerator.GeneratePdf(imageDataList, outputPath);
}
catch (Exception ex)
diff --git a/src/Presentation/Api/Models/ConvertRequest.cs b/src/Presentation/Api/Models/ConvertRequest.cs
index 3ae1700..63b3ef6 100644
--- a/src/Presentation/Api/Models/ConvertRequest.cs
+++ b/src/Presentation/Api/Models/ConvertRequest.cs
@@ -43,5 +43,10 @@ public class ConvertRequest
/// Print density in DPI (optional, default: 203)
///
public int? Dpi { get; set; }
+
+ ///
+ /// Rendering engine: "offline" (default), "labelary", or "auto"
+ ///
+ public string Renderer { get; set; } = "offline";
}
}
diff --git a/src/Presentation/ArgumentParser.cs b/src/Presentation/ArgumentParser.cs
index 16d56bb..d5797d8 100644
--- a/src/Presentation/ArgumentParser.cs
+++ b/src/Presentation/ArgumentParser.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using ZPL2PDF.Shared.Constants;
namespace ZPL2PDF
{
@@ -87,6 +88,20 @@ public ConversionArguments ParseConversionMode(string[] args, int startIndex)
case "--stdout":
result.StandardOutput = true;
break;
+ case "--renderer":
+ if (!string.IsNullOrWhiteSpace(nextArg))
+ {
+ var renderer = nextArg.Trim().ToLowerInvariant();
+ result.RendererEngine = renderer switch
+ {
+ "offline" => RendererEngine.Offline,
+ "labelary" => RendererEngine.Labelary,
+ "auto" => RendererEngine.Auto,
+ _ => result.RendererEngine
+ };
+ i++; // Skip next argument as it's the value
+ }
+ break;
case "-d":
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int dpi))
{
@@ -190,6 +205,20 @@ public DaemonArguments ParseDaemonMode(string[] args, int startIndex)
i++; // Skip next argument as it's the value
}
break;
+ case "--renderer":
+ if (i + 1 < args.Length && !string.IsNullOrWhiteSpace(args[i + 1]))
+ {
+ var renderer = args[i + 1].Trim().ToLowerInvariant();
+ result.RendererEngine = renderer switch
+ {
+ "offline" => RendererEngine.Offline,
+ "labelary" => RendererEngine.Labelary,
+ "auto" => RendererEngine.Auto,
+ _ => result.RendererEngine
+ };
+ i++; // Skip next argument as it's the value
+ }
+ break;
}
}
@@ -240,6 +269,18 @@ public ServerArguments ParseServerMode(string[] args, int startIndex)
result.OutputFolder = args[i + 1];
i++;
}
+ else if (arg.Equals("--renderer", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length && !string.IsNullOrWhiteSpace(args[i + 1]))
+ {
+ var renderer = args[i + 1].Trim().ToLowerInvariant();
+ result.RendererEngine = renderer switch
+ {
+ "offline" => RendererEngine.Offline,
+ "labelary" => RendererEngine.Labelary,
+ "auto" => RendererEngine.Auto,
+ _ => result.RendererEngine
+ };
+ i++;
+ }
else if (arg.Equals("--foreground", StringComparison.OrdinalIgnoreCase))
{
result.Foreground = true;
@@ -287,6 +328,8 @@ public class ConversionArguments
public int Dpi { get; set; } = 203;
/// When enabled, the conversion writes PDF bytes to stdout.
public bool StandardOutput { get; set; } = false;
+ /// Rendering engine selection (offline/labelary/auto).
+ public RendererEngine RendererEngine { get; set; } = RendererEngine.Offline;
/// Directory containing TTF/OTF fonts (e.g. for --fonts-dir).
public string FontsDirectory { get; set; } = string.Empty;
/// Font ID to file path (e.g. A=arial.ttf, B=another.ttf).
@@ -303,6 +346,8 @@ public class DaemonArguments
public double Height { get; set; } = 0;
public string Unit { get; set; } = "mm";
public int Dpi { get; set; } = 203;
+ /// Rendering engine selection (offline/labelary/auto).
+ public RendererEngine RendererEngine { get; set; } = RendererEngine.Offline;
}
///
@@ -313,5 +358,7 @@ public class ServerArguments
public int Port { get; set; } = 9101;
public string OutputFolder { get; set; } = string.Empty;
public bool Foreground { get; set; } = false;
+ /// Rendering engine selection (offline/labelary/auto).
+ public RendererEngine RendererEngine { get; set; } = RendererEngine.Offline;
}
}
diff --git a/src/Presentation/ArgumentProcessor.cs b/src/Presentation/ArgumentProcessor.cs
index 5f52c74..ed881be 100644
--- a/src/Presentation/ArgumentProcessor.cs
+++ b/src/Presentation/ArgumentProcessor.cs
@@ -85,6 +85,11 @@ public ArgumentProcessor()
///
public bool ServerForeground { get; private set; } = false;
+ ///
+ /// Rendering engine selection (offline/labelary/auto).
+ ///
+ public RendererEngine RendererEngine { get; private set; } = RendererEngine.Offline;
+
///
/// Gets or sets the input file path.
///
@@ -218,6 +223,7 @@ private void ProcessConversionMode(string[] args)
Height = conversionArgs.Height;
Unit = conversionArgs.Unit;
Dpi = conversionArgs.Dpi;
+ RendererEngine = conversionArgs.RendererEngine;
FontsDirectory = conversionArgs.FontsDirectory ?? string.Empty;
FontMappings = conversionArgs.FontMappings ?? new List<(string, string)>();
@@ -287,6 +293,7 @@ private void ProcessDaemonMode(string[] args)
Unit = daemonArgs.Unit;
Dpi = daemonArgs.Dpi;
+ RendererEngine = daemonArgs.RendererEngine;
// Only validate dimensions if they were explicitly provided (not auto-applied)
// Check if dimensions are different from defaults (indicating explicit user input)
@@ -322,6 +329,7 @@ private void ProcessServerMode(string[] args)
ServerPort = serverArgs.Port;
ServerOutputFolder = serverArgs.OutputFolder;
ServerForeground = serverArgs.Foreground;
+ RendererEngine = serverArgs.RendererEngine;
if (ServerCommand == "start")
{
diff --git a/src/Presentation/ArgumentValidator.cs b/src/Presentation/ArgumentValidator.cs
index 9ce8ef1..55967fd 100644
--- a/src/Presentation/ArgumentValidator.cs
+++ b/src/Presentation/ArgumentValidator.cs
@@ -50,9 +50,9 @@ public class ArgumentValidator
}
var extension = Path.GetExtension(inputFilePath).ToLowerInvariant();
- if (extension != ".txt" && extension != ".prn")
+ if (extension != ".txt" && extension != ".prn" && extension != ".zpl" && extension != ".imp")
{
- return (false, "Input file must be .txt or .prn");
+ return (false, "Input file must be .txt, .prn, .zpl or .imp");
}
}
diff --git a/src/Presentation/ConversionModeHandler.cs b/src/Presentation/ConversionModeHandler.cs
index 625d299..3c75b58 100644
--- a/src/Presentation/ConversionModeHandler.cs
+++ b/src/Presentation/ConversionModeHandler.cs
@@ -3,6 +3,7 @@
using System.IO;
using ZPL2PDF.Shared;
using ZPL2PDF.Domain.Services;
+using ZPL2PDF.Shared.Constants;
using ZPL2PDF.Application.Interfaces;
using ZPL2PDF.Application.Services;
@@ -44,6 +45,33 @@ public void HandleConversion(ArgumentProcessor argumentProcessor)
fileContent = argumentProcessor.ZplContent;
}
+ // If requested, try direct PDF generation via Labelary.
+ if (_conversionService.TryConvertPdfDirectWithLabelary(
+ fileContent,
+ argumentProcessor.Width,
+ argumentProcessor.Height,
+ argumentProcessor.Unit,
+ argumentProcessor.Dpi,
+ argumentProcessor.RendererEngine,
+ out var pdfBytes,
+ string.IsNullOrWhiteSpace(argumentProcessor.FontsDirectory) ? null : argumentProcessor.FontsDirectory,
+ argumentProcessor.FontMappings?.Count > 0 ? argumentProcessor.FontMappings : null))
+ {
+ if (argumentProcessor.StandardOutput)
+ {
+ Stream stdout = Console.OpenStandardOutput();
+ stdout.Write(pdfBytes!, 0, pdfBytes!.Length);
+ stdout.Flush();
+ return;
+ }
+
+ _pathService.EnsureDirectoryExists(argumentProcessor.OutputFolderPath);
+ string outputPdf = Path.Combine(argumentProcessor.OutputFolderPath, argumentProcessor.OutputFileName);
+ File.WriteAllBytes(outputPdf, pdfBytes!);
+ Console.WriteLine($"PDF generated successfully: {outputPdf}");
+ return;
+ }
+
var imageDataList = _conversionService.Convert(
fileContent,
argumentProcessor.Width,
@@ -51,10 +79,10 @@ public void HandleConversion(ArgumentProcessor argumentProcessor)
argumentProcessor.Unit,
argumentProcessor.Dpi,
string.IsNullOrWhiteSpace(argumentProcessor.FontsDirectory) ? null : argumentProcessor.FontsDirectory,
- argumentProcessor.FontMappings?.Count > 0 ? argumentProcessor.FontMappings : null
- );
+ argumentProcessor.FontMappings?.Count > 0 ? argumentProcessor.FontMappings : null,
+ argumentProcessor.RendererEngine);
- // Process the generated images
+ // Process the generated PNG images and compose the final PDF locally.
ProcessImages(imageDataList, argumentProcessor);
}
catch (FileNotFoundException ex)
diff --git a/src/Presentation/DaemonModeHandler.cs b/src/Presentation/DaemonModeHandler.cs
index 920fb13..65ad715 100644
--- a/src/Presentation/DaemonModeHandler.cs
+++ b/src/Presentation/DaemonModeHandler.cs
@@ -64,7 +64,12 @@ private async Task RunDaemonMode(ArgumentProcessor argumentProcessor)
var configManager = new ConfigManager();
// Create processing queue
- var processingQueue = new ProcessingQueue(dimensionExtractor, configManager);
+ var processingQueue = new ProcessingQueue(
+ dimensionExtractor,
+ configManager,
+ maxConcurrentFiles: 1,
+ customOutputFolder: null,
+ rendererEngine: argumentProcessor.RendererEngine);
// Create fixed dimensions if using fixed dimensions
LabelDimensions? fixedDimensions = null;
diff --git a/src/Presentation/Program.cs b/src/Presentation/Program.cs
index 9f2c7e5..64802b8 100644
--- a/src/Presentation/Program.cs
+++ b/src/Presentation/Program.cs
@@ -8,6 +8,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Cors.Infrastructure;
using ZPL2PDF.Shared.Localization;
+using ZPL2PDF.Shared.Constants;
using ZPL2PDF.Presentation.Api.Models;
using ZPL2PDF.Application.Services;
@@ -224,37 +225,40 @@ static async Task StartApiMode(string[] args)
var unit = request.Unit ?? "mm";
var dpi = request.Dpi ?? 203;
+ // Renderer selection ("offline" | "labelary" | "auto")
+ if (!TryGetRendererEngine(request.Renderer, out var rendererEngine))
+ {
+ await WriteBadRequest(context, "Renderer must be 'offline', 'labelary', or 'auto'");
+ return;
+ }
+
// Convert ZPL(s)
var conversionService = new ConversionService();
- var imageDataList = new List();
- if (!string.IsNullOrWhiteSpace(request.Zpl))
+ // When format=pdf, try direct PDF via Labelary according to renderer policy.
+ // In auto mode, failures fall back to PNG pipeline.
+ if (format == "pdf" && TryConvertDirectPdfResponse(request, conversionService, width, height, unit, dpi, rendererEngine, out var directPdfBytes, out var pages))
{
- // Single ZPL string (can contain multiple labels)
- imageDataList = conversionService.Convert(request.Zpl, width, height, unit, dpi);
- }
- else if (request.ZplArray != null && request.ZplArray.Count > 0)
- {
- // Multiple ZPL strings - convert each and combine
- foreach (var zpl in request.ZplArray)
+ var directResponse = new ConvertResponse
{
- if (!string.IsNullOrWhiteSpace(zpl))
- {
- var images = conversionService.Convert(zpl, width, height, unit, dpi);
- imageDataList.AddRange(images);
- }
- }
+ Success = true,
+ Format = format,
+ Pdf = Convert.ToBase64String(directPdfBytes!),
+ Pages = pages,
+ Message = "Conversion successful"
+ };
+
+ context.Response.StatusCode = 200;
+ await context.Response.WriteAsJsonAsync(directResponse);
+ return;
}
+ // PNG pipeline (also used as fallback for PDF).
+ var imageDataList = ConvertToImages(request, conversionService, width, height, unit, dpi, rendererEngine);
+
if (imageDataList == null || imageDataList.Count == 0)
{
- var errorResponse = new ConvertResponse
- {
- Success = false,
- Message = "No images generated from ZPL content"
- };
- context.Response.StatusCode = 400;
- await context.Response.WriteAsJsonAsync(errorResponse);
+ await WriteBadRequest(context, "No images generated from ZPL content");
return;
}
@@ -269,21 +273,17 @@ static async Task StartApiMode(string[] args)
if (format == "pdf")
{
- // Generate PDF from images
var pdfBytes = PdfGenerator.GeneratePdfToBytes(imageDataList);
response.Pdf = Convert.ToBase64String(pdfBytes);
}
else // png
{
- // Convert images to base64
if (imageDataList.Count == 1)
{
- // Single image
response.Image = Convert.ToBase64String(imageDataList[0]);
}
else
{
- // Multiple images
response.Images = imageDataList.Select(img => Convert.ToBase64String(img)).ToList();
}
}
@@ -313,5 +313,129 @@ static async Task StartApiMode(string[] args)
await app.RunAsync($"http://{host}:{port}");
}
+
+ private static async Task WriteBadRequest(HttpContext context, string message)
+ {
+ var errorResponse = new ConvertResponse
+ {
+ Success = false,
+ Message = message
+ };
+ context.Response.StatusCode = 400;
+ await context.Response.WriteAsJsonAsync(errorResponse);
+ }
+
+ private static bool TryGetRendererEngine(string? rendererRaw, out RendererEngine rendererEngine)
+ {
+ var renderer = (rendererRaw ?? "offline").Trim().ToLowerInvariant();
+ rendererEngine = renderer switch
+ {
+ "offline" => RendererEngine.Offline,
+ "labelary" => RendererEngine.Labelary,
+ "auto" => RendererEngine.Auto,
+ _ => RendererEngine.Offline
+ };
+
+ return renderer == "offline" || renderer == "labelary" || renderer == "auto";
+ }
+
+ private static bool TryConvertDirectPdfResponse(
+ ConvertRequest request,
+ ConversionService conversionService,
+ double width,
+ double height,
+ string unit,
+ int dpi,
+ RendererEngine rendererEngine,
+ out byte[]? directPdfBytes,
+ out int pages)
+ {
+ directPdfBytes = null;
+ pages = 0;
+
+ if (!string.IsNullOrWhiteSpace(request.Zpl))
+ {
+ var preprocessed = LabelFileReader.PreprocessZpl(request.Zpl);
+ pages = LabelFileReader.SplitLabels(preprocessed).Count;
+
+ conversionService.TryConvertPdfDirectWithLabelary(
+ request.Zpl,
+ width,
+ height,
+ unit,
+ dpi,
+ rendererEngine,
+ out directPdfBytes);
+ }
+ else if (request.ZplArray != null && request.ZplArray.Count > 0)
+ {
+ var pdfParts = new List();
+ foreach (var zpl in request.ZplArray)
+ {
+ if (string.IsNullOrWhiteSpace(zpl))
+ continue;
+
+ var preprocessed = LabelFileReader.PreprocessZpl(zpl);
+ pages += LabelFileReader.SplitLabels(preprocessed).Count;
+
+ if (!conversionService.TryConvertPdfDirectWithLabelary(
+ zpl,
+ width,
+ height,
+ unit,
+ dpi,
+ rendererEngine,
+ out var partPdfBytes))
+ {
+ pdfParts.Clear();
+ break;
+ }
+
+ pdfParts.Add(partPdfBytes!);
+ }
+
+ if (pdfParts.Count > 0)
+ {
+ directPdfBytes = pdfParts.Count == 1
+ ? pdfParts[0]
+ : PdfGenerator.MergePdfsToBytes(pdfParts);
+ }
+ }
+
+ return directPdfBytes != null;
+ }
+
+ private static List ConvertToImages(
+ ConvertRequest request,
+ ConversionService conversionService,
+ double width,
+ double height,
+ string unit,
+ int dpi,
+ RendererEngine rendererEngine)
+ {
+ var imageDataList = new List();
+
+ if (!string.IsNullOrWhiteSpace(request.Zpl))
+ {
+ return conversionService.Convert(request.Zpl, width, height, unit, dpi, rendererEngine: rendererEngine);
+ }
+
+ if (request.ZplArray == null || request.ZplArray.Count == 0)
+ {
+ return imageDataList;
+ }
+
+ foreach (var zpl in request.ZplArray)
+ {
+ if (string.IsNullOrWhiteSpace(zpl))
+ continue;
+
+ var images = conversionService.Convert(zpl, width, height, unit, dpi, rendererEngine: rendererEngine);
+ imageDataList.AddRange(images);
+ }
+
+ return imageDataList;
+ }
}
}
\ No newline at end of file
diff --git a/src/Presentation/TcpServerModeHandler.cs b/src/Presentation/TcpServerModeHandler.cs
index f60ceda..9d7dbe1 100644
--- a/src/Presentation/TcpServerModeHandler.cs
+++ b/src/Presentation/TcpServerModeHandler.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Threading;
+using ZPL2PDF.Shared.Constants;
namespace ZPL2PDF
{
@@ -53,13 +54,13 @@ private void HandleStart(ArgumentProcessor args)
if (args.ServerForeground)
{
- RunServerInForeground(args.ServerPort, args.ServerOutputFolder);
+ RunServerInForeground(args.ServerPort, args.ServerOutputFolder, args.RendererEngine);
return;
}
// Background: spawn process with --foreground so the child runs the server
var processManager = new ProcessManager();
- var serverArgs = BuildServerArguments(args.ServerPort, args.ServerOutputFolder, foreground: true);
+ var serverArgs = BuildServerArguments(args.ServerPort, args.ServerOutputFolder, args.RendererEngine, foreground: true);
var startedPid = processManager.StartBackgroundProcess(serverArgs);
if (startedPid <= 0)
{
@@ -143,9 +144,9 @@ private static bool IsServerProcessRunning(PidManager pidManager)
}
}
- private static void RunServerInForeground(int port, string outputFolder)
+ private static void RunServerInForeground(int port, string outputFolder, RendererEngine rendererEngine)
{
- var server = new TcpPrinterServer(port, outputFolder);
+ var server = new TcpPrinterServer(port, outputFolder, rendererEngine);
Console.WriteLine($"TCP server listening on port {port}. Output folder: {outputFolder}");
Console.WriteLine("Press Ctrl+C to stop.");
using var cts = new CancellationTokenSource();
@@ -165,10 +166,11 @@ private static void RunServerInForeground(int port, string outputFolder)
}
}
- private static string BuildServerArguments(int port, string outputFolder, bool foreground)
+ private static string BuildServerArguments(int port, string outputFolder, RendererEngine rendererEngine, bool foreground)
{
var output = outputFolder.Contains(" ") ? $"\"{outputFolder}\"" : outputFolder;
- var args = $"server start --port {port} -o {output}";
+ var renderer = rendererEngine.ToString().ToLowerInvariant();
+ var args = $"server start --port {port} -o {output} --renderer {renderer}";
if (foreground)
args += " --foreground";
return args;
diff --git a/src/Shared/Constants/ApplicationConstants.cs b/src/Shared/Constants/ApplicationConstants.cs
index 90438ec..d10828b 100644
--- a/src/Shared/Constants/ApplicationConstants.cs
+++ b/src/Shared/Constants/ApplicationConstants.cs
@@ -17,8 +17,8 @@ public static class ApplicationConstants
#endregion
#region File Extensions
- public static readonly string[] VALID_FILE_EXTENSIONS = { ".txt", ".prn" };
- public const string DEFAULT_FILE_FILTER = "*.txt;*.prn";
+ public static readonly string[] VALID_FILE_EXTENSIONS = { ".txt", ".prn", ".zpl", ".imp" };
+ public const string DEFAULT_FILE_FILTER = "*.txt;*.prn;*.zpl;*.imp";
public const string PDF_EXTENSION = ".pdf";
public const string CONFIG_EXTENSION = ".json";
public const string PID_EXTENSION = ".pid";
diff --git a/src/Shared/Constants/RendererEngine.cs b/src/Shared/Constants/RendererEngine.cs
new file mode 100644
index 0000000..c09b3eb
--- /dev/null
+++ b/src/Shared/Constants/RendererEngine.cs
@@ -0,0 +1,24 @@
+namespace ZPL2PDF.Shared.Constants
+{
+ ///
+ /// Rendering engine selection for ZPL to PDF.
+ ///
+ public enum RendererEngine
+ {
+ ///
+ /// Offline rendering using BinaryKits (no internet).
+ ///
+ Offline,
+
+ ///
+ /// Rendering via Labelary online API.
+ ///
+ Labelary,
+
+ ///
+ /// Try Labelary first, fallback to Offline on failure.
+ ///
+ Auto
+ }
+}
+
diff --git a/src/Shared/ZplDimensionExtractor.cs b/src/Shared/ZplDimensionExtractor.cs
index 269fc2d..aa69aba 100644
--- a/src/Shared/ZplDimensionExtractor.cs
+++ b/src/Shared/ZplDimensionExtractor.cs
@@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
+using ZPL2PDF.Domain.Services;
namespace ZPL2PDF
{
///
/// Extracts ZPL label dimensions (^PW and ^LL) and converts points to mm
///
- public class ZplDimensionExtractor
+ public class ZplDimensionExtractor : IDimensionExtractor
{
///
diff --git a/tests/ZPL2PDF.Unit/Mocks/MockConversionService.cs b/tests/ZPL2PDF.Unit/Mocks/MockConversionService.cs
index b05defe..1d9b4f6 100644
--- a/tests/ZPL2PDF.Unit/Mocks/MockConversionService.cs
+++ b/tests/ZPL2PDF.Unit/Mocks/MockConversionService.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using ZPL2PDF.Application.Interfaces;
+using ZPL2PDF.Shared.Constants;
namespace ZPL2PDF.Tests.Mocks
{
@@ -16,7 +17,8 @@ public List ConvertWithExplicitDimensions(
string unit,
int dpi,
string? fontsDirectory = null,
- IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null,
+ RendererEngine rendererEngine = RendererEngine.Offline)
{
// Mock implementation - returns fake PDF data
return new List { GenerateMockPdfData(zplContent, width, height, unit, dpi) };
@@ -27,7 +29,8 @@ public List ConvertWithExtractedDimensions(
string unit,
int dpi,
string? fontsDirectory = null,
- IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null,
+ RendererEngine rendererEngine = RendererEngine.Offline)
{
// Mock implementation - returns fake PDF data
return new List { GenerateMockPdfData(zplContent, 100, 150, unit, dpi) };
@@ -40,12 +43,55 @@ public List Convert(
string unit,
int dpi,
string? fontsDirectory = null,
- IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null,
+ RendererEngine rendererEngine = RendererEngine.Offline)
{
// Mock implementation - returns fake PDF data
return new List { GenerateMockPdfData(zplContent, explicitWidth, explicitHeight, unit, dpi) };
}
+ public byte[] ConvertPdfDirectWithLabelary(
+ string zplContent,
+ double explicitWidth,
+ double explicitHeight,
+ string unit,
+ int dpi,
+ string? fontsDirectory = null,
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ {
+ // Mock implementation - return some bytes that look like a PDF payload.
+ var content = $"Mock Labelary PDF for ZPL: {zplContent.Substring(0, Math.Min(50, zplContent.Length))}...";
+ return System.Text.Encoding.UTF8.GetBytes(content);
+ }
+
+ public bool TryConvertPdfDirectWithLabelary(
+ string zplContent,
+ double explicitWidth,
+ double explicitHeight,
+ string unit,
+ int dpi,
+ RendererEngine rendererEngine,
+ out byte[]? pdfBytes,
+ string? fontsDirectory = null,
+ IReadOnlyList<(string Id, string Path)>? fontMappings = null)
+ {
+ if (rendererEngine == RendererEngine.Offline)
+ {
+ pdfBytes = null;
+ return false;
+ }
+
+ pdfBytes = ConvertPdfDirectWithLabelary(
+ zplContent,
+ explicitWidth,
+ explicitHeight,
+ unit,
+ dpi,
+ fontsDirectory,
+ fontMappings);
+ return true;
+ }
+
private byte[] GenerateMockPdfData(string zplContent, double width, double height, string unit, int dpi)
{
// Generate fake PDF data for testing
diff --git a/tests/ZPL2PDF.Unit/UnitTests/Domain/ValueObjects/FileInfoTests.cs b/tests/ZPL2PDF.Unit/UnitTests/Domain/ValueObjects/FileInfoTests.cs
index 62a7c41..519709d 100644
--- a/tests/ZPL2PDF.Unit/UnitTests/Domain/ValueObjects/FileInfoTests.cs
+++ b/tests/ZPL2PDF.Unit/UnitTests/Domain/ValueObjects/FileInfoTests.cs
@@ -214,6 +214,40 @@ public void IsValid_WithValidPrnFile_ReturnsTrue()
result.Should().BeTrue();
}
+ [Fact]
+ public void IsValid_WithValidZplFile_ReturnsTrue()
+ {
+ // Arrange
+ var fileInfo = new FileInfo
+ {
+ FileName = "test.zpl",
+ Extension = ".zpl"
+ };
+
+ // Act
+ var result = fileInfo.IsValid();
+
+ // Assert
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsValid_WithValidImpFile_ReturnsTrue()
+ {
+ // Arrange
+ var fileInfo = new FileInfo
+ {
+ FileName = "test.imp",
+ Extension = ".imp"
+ };
+
+ // Act
+ var result = fileInfo.IsValid();
+
+ // Assert
+ result.Should().BeTrue();
+ }
+
[Fact]
public void IsValid_WithEmptyFileName_ReturnsFalse()
{
@@ -312,6 +346,32 @@ public void IsValidExtension_WithPrnExtension_ReturnsTrue()
result.Should().BeTrue();
}
+ [Fact]
+ public void IsValidExtension_WithZplExtension_ReturnsTrue()
+ {
+ // Arrange
+ var fileInfo = new FileInfo { Extension = ".zpl" };
+
+ // Act
+ var result = fileInfo.IsValidExtension();
+
+ // Assert
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsValidExtension_WithImpExtension_ReturnsTrue()
+ {
+ // Arrange
+ var fileInfo = new FileInfo { Extension = ".imp" };
+
+ // Act
+ var result = fileInfo.IsValidExtension();
+
+ // Assert
+ result.Should().BeTrue();
+ }
+
[Fact]
public void IsValidExtension_WithTxtExtensionUpperCase_ReturnsTrue()
{
@@ -446,7 +506,7 @@ public void GetValidationError_WithInvalidExtension_ReturnsExtensionError()
var result = fileInfo.GetValidationError();
// Assert
- result.Should().Be("Invalid file extension: .doc. Valid extensions are: .txt, .prn");
+ result.Should().Be("Invalid file extension: .doc. Valid extensions are: .txt, .prn, .zpl, .imp");
}
#endregion
diff --git a/tests/ZPL2PDF.Unit/UnitTests/Domain/ValueObjects/LabelDimensionsTests.cs b/tests/ZPL2PDF.Unit/UnitTests/Domain/ValueObjects/LabelDimensionsTests.cs
deleted file mode 100644
index c6547c2..0000000
--- a/tests/ZPL2PDF.Unit/UnitTests/Domain/ValueObjects/LabelDimensionsTests.cs
+++ /dev/null
@@ -1,205 +0,0 @@
-using FluentAssertions;
-using Xunit;
-// Avoid collision with ZPL2PDF.LabelDimensions (Shared / dimension extractor DTO).
-using DomainLabelDimensions = ZPL2PDF.Domain.ValueObjects.LabelDimensions;
-
-namespace ZPL2PDF.Tests.UnitTests.Domain.ValueObjects
-{
- ///
- /// Unit tests for .
- ///
- public class LabelDimensionsTests
- {
- [Fact]
- public void DefaultConstructor_SetsReasonableDefaults()
- {
- var d = new DomainLabelDimensions();
-
- d.Width.Should().Be(0);
- d.Height.Should().Be(0);
- d.Unit.Should().Be("mm");
- d.Dpi.Should().Be(203);
- }
-
- [Fact]
- public void Constructor_WithValidParameters_SetsProperties()
- {
- var d = new DomainLabelDimensions(100, 50, "cm", 300);
-
- d.Width.Should().Be(100);
- d.Height.Should().Be(50);
- d.Unit.Should().Be("cm");
- d.Dpi.Should().Be(300);
- }
-
- [Theory]
- [InlineData(10, 20, "mm", 203)]
- [InlineData(1, 2, "cm", 203)]
- [InlineData(4, 2, "in", 300)]
- public void IsValid_WithPositiveDimensionsAndKnownUnit_ReturnsTrue(
- double w, double h, string unit, int dpi)
- {
- var d = new DomainLabelDimensions(w, h, unit, dpi);
- d.IsValid().Should().BeTrue();
- d.GetValidationError().Should().BeEmpty();
- }
-
- [Fact]
- public void IsValid_WithZeroWidth_ReturnsFalse()
- {
- var d = new DomainLabelDimensions(0, 20, "mm", 203);
- d.IsValid().Should().BeFalse();
- d.GetValidationError().Should().Contain("Width");
- }
-
- [Fact]
- public void IsValid_WithZeroHeight_ReturnsFalse()
- {
- var d = new DomainLabelDimensions(10, 0, "mm", 203);
- d.IsValid().Should().BeFalse();
- d.GetValidationError().Should().Contain("Height");
- }
-
- [Fact]
- public void IsValid_WithEmptyUnit_ReturnsFalse()
- {
- var d = new DomainLabelDimensions(10, 20, " ", 203);
- d.IsValid().Should().BeFalse();
- d.GetValidationError().Should().Contain("Unit");
- }
-
- [Fact]
- public void IsValid_WithZeroDpi_ReturnsFalse()
- {
- var d = new DomainLabelDimensions(10, 20, "mm", 0);
- d.IsValid().Should().BeFalse();
- d.GetValidationError().Should().Contain("DPI");
- }
-
- [Fact]
- public void IsValid_WithInvalidUnit_ReturnsFalse()
- {
- var d = new DomainLabelDimensions(10, 20, "px", 203);
- d.IsValid().Should().BeFalse();
- d.GetValidationError().Should().Contain("Invalid unit");
- }
-
- [Theory]
- [InlineData("mm", true)]
- [InlineData("MM", true)]
- [InlineData("cm", true)]
- [InlineData("in", true)]
- [InlineData("px", false)]
- [InlineData("", false)]
- [InlineData(" ", false)]
- public void IsValidUnit_MatchesExpected(string unit, bool expected)
- {
- DomainLabelDimensions.IsValidUnit(unit).Should().Be(expected);
- }
-
- [Fact]
- public void ToMillimeters_FromMm_ReturnsEquivalentValues()
- {
- var d = new DomainLabelDimensions(12.5, 8, "mm", 203);
- var mm = d.ToMillimeters();
-
- mm.Width.Should().BeApproximately(12.5, 0.001);
- mm.Height.Should().BeApproximately(8, 0.001);
- mm.Unit.Should().Be("mm");
- mm.Dpi.Should().Be(203);
- }
-
- [Fact]
- public void ToMillimeters_FromCm_ConvertsCorrectly()
- {
- var d = new DomainLabelDimensions(2, 3, "cm", 203);
- var mm = d.ToMillimeters();
-
- mm.Width.Should().BeApproximately(20, 0.001);
- mm.Height.Should().BeApproximately(30, 0.001);
- mm.Unit.Should().Be("mm");
- }
-
- [Fact]
- public void ToCentimeters_FromMm_ConvertsCorrectly()
- {
- var d = new DomainLabelDimensions(100, 50, "mm", 203);
- var cm = d.ToCentimeters();
-
- cm.Unit.Should().Be("cm");
- cm.Width.Should().BeApproximately(10, 0.001);
- cm.Height.Should().BeApproximately(5, 0.001);
- }
-
- [Fact]
- public void ToInches_FromMm_ConvertsCorrectly()
- {
- var d = new DomainLabelDimensions(25.4, 50.8, "mm", 203);
- var inches = d.ToInches();
-
- inches.Unit.Should().Be("in");
- inches.Width.Should().BeApproximately(1, 0.01);
- inches.Height.Should().BeApproximately(2, 0.01);
- }
-
- [Fact]
- public void ToPoints_UsesDpiAndMillimeters()
- {
- // 25.4 mm @ 203 dpi => 1 inch => 203 points width
- var d = new DomainLabelDimensions(25.4, 25.4, "mm", 203);
- (int w, int h) = d.ToPoints();
-
- w.Should().Be(203);
- h.Should().Be(203);
- }
-
- [Fact]
- public void Clone_ReturnsEqualButDistinctInstance()
- {
- var original = new DomainLabelDimensions(7, 8, "in", 300);
- var copy = original.Clone();
-
- copy.Should().NotBeSameAs(original);
- copy.Should().Be(original);
- }
-
- [Fact]
- public void Equals_WithSameValues_ReturnsTrue()
- {
- var a = new DomainLabelDimensions(10, 20, "MM", 203);
- var b = new DomainLabelDimensions(10, 20, "mm", 203);
-
- a.Equals(b).Should().BeTrue();
- (a == b).Should().BeFalse(); // no operator overload
- }
-
- [Fact]
- public void Equals_WithDifferentValues_ReturnsFalse()
- {
- var a = new DomainLabelDimensions(10, 20, "mm", 203);
- var b = new DomainLabelDimensions(11, 20, "mm", 203);
-
- a.Equals(b).Should().BeFalse();
- }
-
- [Fact]
- public void GetHashCode_WithSameValues_ReturnsSameHash()
- {
- var a = new DomainLabelDimensions(10, 20, "mm", 203);
- var b = new DomainLabelDimensions(10, 20, "MM", 203);
-
- a.GetHashCode().Should().Be(b.GetHashCode());
- }
-
- [Fact]
- public void ToString_IncludesDimensionsAndUnit()
- {
- var d = new DomainLabelDimensions(100, 50, "mm", 203);
- var s = d.ToString();
-
- s.Should().Contain("100");
- s.Should().Contain("50");
- s.Should().Contain("mm");
- }
- }
-}
diff --git a/tests/ZPL2PDF.Unit/UnitTests/Infrastructure/FolderMonitorTests.cs b/tests/ZPL2PDF.Unit/UnitTests/Infrastructure/FolderMonitorTests.cs
index 9bbf268..b09cd3a 100644
--- a/tests/ZPL2PDF.Unit/UnitTests/Infrastructure/FolderMonitorTests.cs
+++ b/tests/ZPL2PDF.Unit/UnitTests/Infrastructure/FolderMonitorTests.cs
@@ -296,6 +296,66 @@ public void FileDetection_WithValidPrnFile_DetectsFile()
fileDetected.Should().BeTrue();
}
+ [Fact]
+ public void FileDetection_WithValidZplFile_DetectsFile()
+ {
+ // Arrange
+ var fixedDimensions = new LabelDimensions { Width = 100, Height = 200, Dpi = 203 };
+ var folderMonitor = new FolderMonitor(
+ _testDirectory,
+ _processingQueue,
+ _dimensionExtractor,
+ _configManager,
+ fixedDimensions,
+ true
+ );
+
+ bool fileDetected = false;
+ folderMonitor.FileDetected += (sender, e) => fileDetected = true;
+
+ folderMonitor.StartWatching();
+
+ // Act
+ var testFile = Path.Combine(_testDirectory, "test.zpl");
+ File.WriteAllText(testFile, "^XA^FO50,50^A0N,50,50^FDTest Label^FS^XZ");
+
+ // Wait a bit for file detection
+ Thread.Sleep(500);
+
+ // Assert
+ fileDetected.Should().BeTrue();
+ }
+
+ [Fact]
+ public void FileDetection_WithValidImpFile_DetectsFile()
+ {
+ // Arrange
+ var fixedDimensions = new LabelDimensions { Width = 100, Height = 200, Dpi = 203 };
+ var folderMonitor = new FolderMonitor(
+ _testDirectory,
+ _processingQueue,
+ _dimensionExtractor,
+ _configManager,
+ fixedDimensions,
+ true
+ );
+
+ bool fileDetected = false;
+ folderMonitor.FileDetected += (sender, e) => fileDetected = true;
+
+ folderMonitor.StartWatching();
+
+ // Act
+ var testFile = Path.Combine(_testDirectory, "test.imp");
+ File.WriteAllText(testFile, "^XA^FO50,50^A0N,50,50^FDTest Label^FS^XZ");
+
+ // Wait a bit for file detection
+ Thread.Sleep(500);
+
+ // Assert
+ fileDetected.Should().BeTrue();
+ }
+
[Fact]
public void FileDetection_WithInvalidFile_IgnoresFile()
{
diff --git a/tests/ZPL2PDF.Unit/UnitTests/Infrastructure/PdfGeneratorTests.cs b/tests/ZPL2PDF.Unit/UnitTests/Infrastructure/PdfGeneratorTests.cs
new file mode 100644
index 0000000..22a78e2
--- /dev/null
+++ b/tests/ZPL2PDF.Unit/UnitTests/Infrastructure/PdfGeneratorTests.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using FluentAssertions;
+using PdfSharp.Pdf;
+using PdfSharp.Pdf.IO;
+using Xunit;
+using ZPL2PDF;
+
+namespace ZPL2PDF.Tests.UnitTests.Infrastructure
+{
+ ///
+ /// Unit tests for (PDF merge behavior).
+ ///
+ public class PdfGeneratorTests
+ {
+ private static byte[] CreatePdfWithPageCount(int pageCount)
+ {
+ pageCount.Should().BeGreaterThan(0, "test PDF must contain at least one page");
+
+ var document = new PdfDocument();
+ for (var i = 0; i < pageCount; i++)
+ document.AddPage();
+
+ using var ms = new MemoryStream();
+ document.Save(ms, false);
+ return ms.ToArray();
+ }
+
+ [Fact]
+ public void MergePdfsToBytes_WithTwoValidPdfDocuments_ReturnsMergedPdfWithTwoPages()
+ {
+ // Arrange
+ var pdf1 = CreatePdfWithPageCount(1);
+ var pdf2 = CreatePdfWithPageCount(1);
+
+ // Act
+ var merged = PdfGenerator.MergePdfsToBytes(new List { pdf1, pdf2 });
+
+ // Assert
+ using var mergedMs = new MemoryStream(merged);
+ using var mergedDoc = PdfReader.Open(mergedMs, PdfDocumentOpenMode.Import);
+ mergedDoc.PageCount.Should().Be(2);
+ }
+
+ [Fact]
+ public void MergePdfsToBytes_WithNullAndEmptyEntries_IgnoresThemAndMergesRemainingPages()
+ {
+ // Arrange
+ var valid = CreatePdfWithPageCount(2);
+ var empty = Array.Empty();
+
+ // Act
+ var merged = PdfGenerator.MergePdfsToBytes(new List { valid, null!, empty });
+
+ // Assert
+ using var mergedMs = new MemoryStream(merged);
+ using var mergedDoc = PdfReader.Open(mergedMs, PdfDocumentOpenMode.Import);
+ mergedDoc.PageCount.Should().Be(2);
+ }
+ }
+}
+
diff --git a/tests/ZPL2PDF.Unit/UnitTests/Presentation/ArgumentParserTests.cs b/tests/ZPL2PDF.Unit/UnitTests/Presentation/ArgumentParserTests.cs
index 1af832e..4c5ec46 100644
--- a/tests/ZPL2PDF.Unit/UnitTests/Presentation/ArgumentParserTests.cs
+++ b/tests/ZPL2PDF.Unit/UnitTests/Presentation/ArgumentParserTests.cs
@@ -1,6 +1,7 @@
using FluentAssertions;
using Xunit;
using ZPL2PDF;
+using ZPL2PDF.Shared.Constants;
namespace ZPL2PDF.Tests.UnitTests.Presentation
{
@@ -67,6 +68,48 @@ public void ParseConversionMode_WithStdoutFlag_SetsStandardOutput()
// Assert
result.StandardOutput.Should().BeTrue();
}
+
+ [Fact]
+ public void ParseConversionMode_WithRendererLabelary_SetsRendererEngine()
+ {
+ // Arrange
+ var parser = new ArgumentParser();
+ var zplContent = "^XA^FO50,50^A0N,50,50^FDTest Label^FS^XZ";
+
+ var args = new[]
+ {
+ "-z", zplContent,
+ "-o", "/tmp",
+ "--renderer", "labelary"
+ };
+
+ // Act
+ var result = parser.ParseConversionMode(args, 0);
+
+ // Assert
+ result.RendererEngine.Should().Be(RendererEngine.Labelary);
+ }
+
+ [Fact]
+ public void ParseConversionMode_WithRendererAuto_SetsRendererEngine()
+ {
+ // Arrange
+ var parser = new ArgumentParser();
+ var zplContent = "^XA^FO50,50^A0N,50,50^FDTest Label^FS^XZ";
+
+ var args = new[]
+ {
+ "-z", zplContent,
+ "-o", "/tmp",
+ "--renderer", "auto"
+ };
+
+ // Act
+ var result = parser.ParseConversionMode(args, 0);
+
+ // Assert
+ result.RendererEngine.Should().Be(RendererEngine.Auto);
+ }
}
}
diff --git a/tests/ZPL2PDF.Unit/UnitTests/Presentation/ArgumentValidatorTests.cs b/tests/ZPL2PDF.Unit/UnitTests/Presentation/ArgumentValidatorTests.cs
index e3f1eaf..358c088 100644
--- a/tests/ZPL2PDF.Unit/UnitTests/Presentation/ArgumentValidatorTests.cs
+++ b/tests/ZPL2PDF.Unit/UnitTests/Presentation/ArgumentValidatorTests.cs
@@ -105,7 +105,7 @@ public void ValidateConversionMode_WithInvalidFileExtension_ReturnsInvalid()
// Assert
result.IsValid.Should().BeFalse();
- result.ErrorMessage.Should().Contain("Input file must be .txt or .prn");
+ result.ErrorMessage.Should().Contain("Input file must be .txt, .prn, .zpl or .imp");
}
[Fact]
@@ -138,6 +138,36 @@ public void ValidateConversionMode_WithPrnFile_ReturnsValid()
result.ErrorMessage.Should().BeEmpty();
}
+ [Fact]
+ public void ValidateConversionMode_WithZplFile_ReturnsValid()
+ {
+ // Arrange
+ var testFile = Path.Combine(_testDirectory, "test.zpl");
+ File.WriteAllText(testFile, "^XA^FO50,50^A0N,50,50^FDTest Label^FS^XZ");
+
+ // Act
+ var result = _validator.ValidateConversionMode(testFile, "", _testDirectory, 0, 0, "mm", false);
+
+ // Assert
+ result.IsValid.Should().BeTrue();
+ result.ErrorMessage.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void ValidateConversionMode_WithImpFile_ReturnsValid()
+ {
+ // Arrange
+ var testFile = Path.Combine(_testDirectory, "test.imp");
+ File.WriteAllText(testFile, "^XA^FO50,50^A0N,50,50^FDTest Label^FS^XZ");
+
+ // Act
+ var result = _validator.ValidateConversionMode(testFile, "", _testDirectory, 0, 0, "mm", false);
+
+ // Assert
+ result.IsValid.Should().BeTrue();
+ result.ErrorMessage.Should().BeEmpty();
+ }
+
[Fact]
public void ValidateConversionMode_WithEmptyOutputFolder_ReturnsInvalid()
{
diff --git a/tests/ZPL2PDF.Unit/UnitTests/Presentation/ConvertRequestTests.cs b/tests/ZPL2PDF.Unit/UnitTests/Presentation/ConvertRequestTests.cs
new file mode 100644
index 0000000..e373974
--- /dev/null
+++ b/tests/ZPL2PDF.Unit/UnitTests/Presentation/ConvertRequestTests.cs
@@ -0,0 +1,59 @@
+using System.Text.Json;
+using FluentAssertions;
+using Xunit;
+using ZPL2PDF.Presentation.Api.Models;
+
+namespace ZPL2PDF.Tests.UnitTests.Presentation
+{
+ public class ConvertRequestTests
+ {
+ private static ConvertRequest Deserialize(string json)
+ {
+ return JsonSerializer.Deserialize(json, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ })!;
+ }
+
+ [Fact]
+ public void Deserialize_WhenOnlyZplProvided_UsesDefaults()
+ {
+ // Arrange
+ var json = """
+ {
+ "zpl": "^XA^FO10,10^A0N,30,30^FDHi^FS^XZ"
+ }
+ """;
+
+ // Act
+ var request = Deserialize(json);
+
+ // Assert
+ request.Format.Should().Be("pdf");
+ request.Renderer.Should().Be("offline");
+ request.Unit.Should().Be("mm");
+ request.Dpi.Should().BeNull();
+ }
+
+ [Fact]
+ public void Deserialize_WhenRendererProvided_SetsRenderer()
+ {
+ // Arrange
+ var json = """
+ {
+ "zpl": "^XA^FO10,10^A0N,30,30^FDHi^FS^XZ",
+ "format": "pdf",
+ "renderer": "labelary"
+ }
+ """;
+
+ // Act
+ var request = Deserialize(json);
+
+ // Assert
+ request.Format.Should().Be("pdf");
+ request.Renderer.Should().Be("labelary");
+ }
+ }
+}
+
diff --git a/wiki/Architecture-Overview.md b/wiki/Architecture-Overview.md
index bf1f5f1..ffec6cd 100644
--- a/wiki/Architecture-Overview.md
+++ b/wiki/Architecture-Overview.md
@@ -34,9 +34,8 @@ src/
│ │ ├── IPdfGenerator.cs
│ │ ├── IFileValidator.cs
│ │ └── IDimensionExtractor.cs
-│ └── ValueObjects/ # Immutable Value Objects
+│ └── ValueObjects/ # Value Objects
│ ├── ConversionOptions.cs
-│ ├── LabelDimensions.cs
│ ├── FileInfo.cs
│ ├── ProcessingResult.cs
│ └── DaemonConfiguration.cs
@@ -125,7 +124,6 @@ Output PDFs
**Purpose**: Business logic and domain rules
**Key Concepts**:
-- `LabelDimensions`: Width, height, unit, DPI
- `ConversionOptions`: User-specified options
- `ProcessingResult`: Conversion outcomes
- Domain interfaces (no implementations)
@@ -171,14 +169,12 @@ public class ConversionService : IConversionService
### Value Objects
```csharp
-public record LabelDimensions(
- double Width,
- double Height,
- string Unit,
- int Dpi)
+public class ConversionOptions
{
- public double WidthInMm => ConvertToMm(Width, Unit);
- public double HeightInMm => ConvertToMm(Height, Unit);
+ public double Width { get; set; }
+ public double Height { get; set; }
+ public string Unit { get; set; } = "mm";
+ public int Dpi { get; set; } = 203;
}
```
diff --git a/zpl2pdf.json.example b/zpl2pdf.json.example
index a138c2f..549d218 100644
--- a/zpl2pdf.json.example
+++ b/zpl2pdf.json.example
@@ -5,7 +5,7 @@
"language": "en-US",
// Daemon Configuration
- "defaultWatchFolder": "C:\\Users\\user\\Documents\\ZPL2PDF Auto Converter",
+ "defaultListenFolder": "C:\\Users\\user\\Documents\\ZPL2PDF Auto Converter",
"labelWidth": 7.5,
"labelHeight": 15,
"unit": "in",