Refactor: Rename DocumentOperator to DocumentService

Updated project and solution files to rename and restructure
`DocumentOperator` to `DocumentService`. Modified project
references in `DocumentService.API.csproj`, `DocumentService.Application.csproj`,
and `DocumentService.Infrastructure.csproj` to reflect the new
naming convention. Updated `DocumentService.sln` to remove
old project references, add new ones, and adjust configuration
and nested project mappings accordingly. This change aligns
the project structure with the new naming convention.
This commit is contained in:
2026-08-30 21:33:25 +02:00
parent 050cb55bf9
commit 207c778f3a
108 changed files with 37 additions and 37 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
using Codecrete.SwissQRBill.Generator;
using DevExpress.Drawing;
using DevExpress.Pdf;
using DocumentService.Application.Common.Interfaces;
using DocumentService.Domain.Common.Exceptions;
using SkiaSharp;
using SkiaSharp.QrCode;
using System.Collections.Concurrent;
namespace DocumentService.Infrastructure.Services.QrCodeProcessing;
/// <summary>
/// Swiss QR Code processor using DevExpress PDF API for image extraction,
/// SkiaSharp.QrCode for QR decoding, and Codecrete.SwissQRBill.Generator for Swiss QR parsing.
///
/// Strategy:
/// 1. Extract all embedded images from PDF using GetDXImages()
/// 2. Try to decode QR code from each image using SkiaSharp.QrCode
/// 3. Parse Swiss QR format with Codecrete library
/// 4. Return both parsed Bill and raw QR text lines
/// </summary>
public sealed class DevExpressSwissQrCodeProcessor : ISwissQrCodeProcessor
{
/// <inheritdoc />
public async Task<(Bill Bill, string[] RawLines)> ExtractSwissQrCodeAsync(
Stream pdfStream,
int[]? pageNumbers = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
if (pdfStream.Length == 0)
throw new ArgumentException("PDF stream is empty.", nameof(pdfStream));
// Defensive validation: Seekable streams must be at Position = 0
// Non-seekable streams (e.g., NetworkStream) are not checked
if (pdfStream.CanSeek && pdfStream.Position != 0)
throw new BadRequestException(null, new ArgumentException(
"PDF stream must be positioned at the beginning (Position = 0).",
nameof(pdfStream)));
using var pdfDocument = new PdfDocumentProcessor();
pdfDocument.LoadDocument(pdfStream);
if (pdfDocument.Document.Pages.Count == 0)
throw new ArgumentException("PDF document contains no pages.", nameof(pdfStream));
// Determine which pages to scan
int[] pagesToScan = DeterminePageNumbers(pdfDocument.Document.Pages.Count, pageNumbers);
// Extract all images from specified pages (no pre-filtering)
var allImages = new ConcurrentBag<(int pageNumber, DXBitmap image)>();
foreach (int pageNumber in pagesToScan)
{
cancellationToken.ThrowIfCancellationRequested();
// Define area: entire page
var page = pdfDocument.Document.Pages[pageNumber - 1];
var area = new PdfDocumentArea(pageNumber,
new PdfRectangle(0, 0, page.CropBox.Width, page.CropBox.Height));
// Extract images from this page
var images = pdfDocument.GetDXImages(area);
foreach (var image in images)
{
// Collect ALL images - QRCodeDecoder will determine if it's a QR code
allImages.Add((pageNumber, image));
}
}
if (allImages.IsEmpty)
throw new NotFoundException($"No images found in pages: {string.Join(", ", pagesToScan)}");
// Parallel scan all images
var qrCodeTasks = allImages.Select(imageData =>
Task.Run(() =>
{
using (imageData.image)
{
string? qrText = DecodeQrCodeFromImage(imageData.image);
if (!string.IsNullOrEmpty(qrText))
{
try
{
// Parse with Codecrete
var bill = QRBill.DecodeQrCodeText(qrText);
// Split raw text into lines (handle both \r\n and \n)
// Remove leading/trailing \r and \n from each line
var rawLines = qrText
.Split(["\r\n", "\n"], StringSplitOptions.None)
.Select(line => line.Trim('\r', '\n'))
.ToArray();
return (success: true, bill, rawLines, imageData.pageNumber);
}
catch
{
// Not a valid Swiss QR Bill, ignore
return (success: false, bill: (Bill?)null, rawLines: (string[]?)null, pageNumber: 0);
}
}
return (success: false, bill: (Bill?)null, rawLines: (string[]?)null, pageNumber: 0);
}
}, cancellationToken)
).ToList();
// Wait for all tasks and find first valid Swiss QR
var results = await Task.WhenAll(qrCodeTasks);
var validResult = results.FirstOrDefault(r => r.success);
if (validResult.success && validResult.bill != null)
{
return (validResult.bill, validResult.rawLines!);
}
throw new NotFoundException(
$"No valid Swiss QR Code found in {allImages.Count} images across pages: {string.Join(", ", pagesToScan)}.");
}
/// <summary>
/// Determines which pages to scan based on optional page numbers parameter.
/// Default: Last page first, then all pages in reverse order.
/// </summary>
private static int[] DeterminePageNumbers(int totalPages, int[]? pageNumbers)
{
if (pageNumbers != null && pageNumbers.Length > 0)
{
// Validate page numbers
foreach (int pageNum in pageNumbers)
if (pageNum < 1 || pageNum > totalPages)
throw new ArgumentException(
$"Invalid page number {pageNum}. Document has {totalPages} pages.",
nameof(pageNumbers));
return pageNumbers;
}
// Default: Scan all pages, last page first (Swiss QR standard)
return [.. Enumerable.Range(1, totalPages).OrderByDescending(p => p)];
}
/// <summary>
/// Decodes QR code from a DXBitmap image using SkiaSharp.QrCode.
/// Converts DXBitmap to SKBitmap and attempts decoding.
/// </summary>
private static string? DecodeQrCodeFromImage(DXBitmap dxImage)
{
// Convert DXBitmap to SKBitmap via MemoryStream (PNG format)
using var ms = new MemoryStream();
dxImage.Save(ms, DXImageFormat.Png);
ms.Position = 0;
// Decode using SkiaSharp.QrCode
using var skBitmap = SKBitmap.Decode(ms);
if (skBitmap == null)
return null;
// TryDecode returns true if QR code found and decoded successfully
bool success = QRCodeDecoder.TryDecode(skBitmap, out var text, out _);
return success ? text : null;
}
}

View File

@@ -0,0 +1,16 @@
using System.Xml.Serialization;
namespace DocumentService.Infrastructure.Services;
public static class StringExtensions
{
public static T? DeserializeXml<T>(this string xmlText)
{
if (string.IsNullOrWhiteSpace(xmlText))
return default;
var serializer = new XmlSerializer(typeof(T));
using var reader = new StringReader(xmlText);
return (T?)serializer.Deserialize(reader);
}
}