using Codecrete.SwissQRBill.Generator; using DevExpress.Pdf; using DocumentOperator.Application.Common.Interfaces; using DocumentOperator.Domain.Exceptions; using DocumentOperator.Domain.ValueObjects; using System.Drawing; using ZXing; namespace DocumentOperator.Infrastructure.Services.QrCodeProcessing; /// /// Swiss QR Code processor using DevExpress PDF API for PDF access /// and Codecrete.SwissQRBill.Generator for QR Code parsing. /// public sealed class DevExpressSwissQrCodeProcessor : ISwissQrCodeProcessor { private const int QrCodeSearchDpi = 300; // High DPI for better QR code recognition /// public async Task ExtractSwissQrCodeAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(pdfBytes); try { using var pdfDocument = new PdfDocumentProcessor(); pdfDocument.LoadDocument(new MemoryStream(pdfBytes)); if (pdfDocument.Document.Pages.Count == 0) { throw new ArgumentException("PDF document contains no pages.", nameof(pdfBytes)); } // Get last page int lastPageIndex = pdfDocument.Document.Pages.Count - 1; // Convert last page to image for QR code detection using var pageImage = RenderPageToImage(pdfDocument, lastPageIndex); // Detect and decode QR code string? qrCodeContent = DecodeQrCodeFromImage(pageImage); if (string.IsNullOrEmpty(qrCodeContent)) { throw new SwissQrCodeNotFoundException( $"No QR Code found on the last page (page {lastPageIndex + 1}) of the PDF document."); } // Parse Swiss QR Bill content using Codecrete library SwissQrCodeData qrCodeData = ParseSwissQrBillContent(qrCodeContent); return await Task.FromResult(qrCodeData); } catch (SwissQrCodeNotFoundException) { throw; } catch (Exception ex) { throw new ArgumentException("Failed to extract Swiss QR Code from PDF.", nameof(pdfBytes), ex); } } /// /// Renders a PDF page to a high-resolution bitmap for QR code detection /// private static Bitmap RenderPageToImage(PdfDocumentProcessor processor, int pageIndex) { // Render page at high DPI for better QR code recognition var pageImage = processor.CreateBitmap(pageIndex + 1, QrCodeSearchDpi); return pageImage; } /// /// Decodes QR code from an image using ZXing library /// private static string? DecodeQrCodeFromImage(Bitmap image) { var reader = new ZXing.Windows.Compatibility.BarcodeReader { AutoRotate = true, TryInverted = true, Options = new ZXing.Common.DecodingOptions { PossibleFormats = new[] { BarcodeFormat.QR_CODE }, TryHarder = true } }; var result = reader.Decode(image); return result?.Text; } /// /// Parses Swiss QR Bill content using Codecrete library /// private static SwissQrCodeData ParseSwissQrBillContent(string qrCodeText) { try { // Decode Swiss QR Bill using Codecrete library var bill = QRBill.DecodeQrCodeText(qrCodeText); // Determine reference type based on presence and format of reference string referenceType = DetermineReferenceType(bill.Reference); // Map AlternativeSchemes to string array var alternativeParams = bill.AlternativeSchemes? .Select(s => $"{s.Name}: {s.Instruction}") .ToArray(); // Map to our domain value object return new SwissQrCodeData { QrType = "SPC", // Always SPC for Swiss Payment Code Version = bill.Version.ToString("D4"), // e.g., "0200" for version 2.0 CodingType = "1", // Always UTF-8 Iban = bill.Account ?? string.Empty, Creditor = MapAddress(bill.Creditor), UltimateCreditor = null, // Not exposed in Codecrete Bill model Amount = bill.Amount, Currency = bill.Currency ?? "CHF", UltimateDebtor = bill.Debtor != null ? MapAddress(bill.Debtor) : null, ReferenceType = referenceType, Reference = bill.Reference, UnstructuredMessage = bill.UnstructuredMessage, BillInformation = bill.BillInformation, AlternativeProcedureParameters = alternativeParams }; } catch (Exception ex) { throw new ArgumentException( "Failed to parse Swiss QR Code content. The QR code may not be a valid Swiss QR Bill.", "qrCodeText", ex); } } /// /// Determines reference type based on reference string format /// private static string DetermineReferenceType(string? reference) { if (string.IsNullOrWhiteSpace(reference)) return "NON"; // QRR (QR Reference): 27 digits if (reference.Length == 27 && reference.All(char.IsDigit)) return "QRR"; // SCOR (Creditor Reference ISO 11649): starts with RF and has check digits if (reference.StartsWith("RF", StringComparison.OrdinalIgnoreCase) && reference.Length >= 5) return "SCOR"; return "NON"; } /// /// Maps Codecrete Address to our AddressData value object /// private static AddressData MapAddress(Codecrete.SwissQRBill.Generator.Address address) { return new AddressData { AddressType = address.Type == Codecrete.SwissQRBill.Generator.Address.AddressType.Structured ? "S" : "K", Name = address.Name ?? string.Empty, Street = address.Street, BuildingNumber = address.HouseNo, AddressLine1 = address.AddressLine1, AddressLine2 = address.AddressLine2, PostalCode = address.PostalCode ?? string.Empty, City = address.Town ?? string.Empty, Country = address.CountryCode ?? string.Empty }; } }