using Codecrete.SwissQRBill.Generator; using DevExpress.Drawing; using DevExpress.Pdf; using DocumentOperator.Application.Common.Interfaces; using DocumentOperator.Domain.Common.Exceptions; using DocumentOperator.Domain.Exceptions; using SkiaSharp; using SkiaSharp.QrCode; using System.Collections.Concurrent; using System.Runtime.Versioning; namespace DocumentOperator.Infrastructure.Services.QrCodeProcessing; /// /// 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 /// public sealed class DevExpressSwissQrCodeProcessor : ISwissQrCodeProcessor { /// public async Task<(Bill Bill, string[] RawLines)> ExtractSwissQrCodeAsync( byte[] pdfBytes, int[]? pageNumbers = null, CancellationToken cancellationToken = default) { if (pdfBytes.Length == 0) throw new ArgumentException("PDF document contains no byte data.", nameof(pdfBytes)); using var pdfDocument = new PdfDocumentProcessor(); using var pdfStream = new MemoryStream(pdfBytes); pdfDocument.LoadDocument(pdfStream); if (pdfDocument.Document.Pages.Count == 0) throw new ArgumentException("PDF document contains no pages.", nameof(pdfBytes)); // 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(new[] { "\r\n", "\n" }, StringSplitOptions.None) .Select(line => line.Trim('\r', '\n')) .ToArray(); return (success: true, bill: bill, rawLines: rawLines, pageNumber: 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)}."); } /// /// Determines which pages to scan based on optional page numbers parameter. /// Default: Last page first, then all pages in reverse order. /// 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)]; } /// /// Decodes QR code from a DXBitmap image using SkiaSharp.QrCode. /// Converts DXBitmap to SKBitmap and attempts decoding. /// 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; } }