- Replace System.Drawing.Bitmap with DevExpress.Drawing.DXBitmap - Fix CA1416 warnings (Windows-specific API usage) - Fix CS0618 warning (TryInverted property moved to Options.TryInverted) - Add [SupportedOSPlatform(windows)] attribute to DecodeQrCodeFromImage() - Add Swiss QR Bill backward compatibility documentation - Suppress CS0618 for AddressLine1/AddressLine2 (deprecated since Nov 2025) - Use modern C# 12 collection expression syntax Technical changes: - CreateBitmap() to CreateDXBitmap() (returns DXBitmap) - Convert DXBitmap to PNG stream to System.Drawing.Bitmap for ZXing - Add using DevExpress.Drawing and System.Runtime.Versioning Result: 0 CA1416 warnings, 0 CS0618 warnings in DevExpressSwissQrCodeProcessor
196 lines
7.3 KiB
C#
196 lines
7.3 KiB
C#
using Codecrete.SwissQRBill.Generator;
|
|
using DevExpress.Drawing;
|
|
using DevExpress.Pdf;
|
|
using DocumentOperator.Application.Common.Interfaces;
|
|
using DocumentOperator.Domain.Exceptions;
|
|
using DocumentOperator.Domain.ValueObjects;
|
|
using System.Drawing;
|
|
using System.Runtime.Versioning;
|
|
using ZXing;
|
|
|
|
namespace DocumentOperator.Infrastructure.Services.QrCodeProcessing;
|
|
|
|
/// <summary>
|
|
/// Swiss QR Code processor using DevExpress PDF API for PDF access
|
|
/// and Codecrete.SwissQRBill.Generator for QR Code parsing.
|
|
/// </summary>
|
|
public sealed class DevExpressSwissQrCodeProcessor : ISwissQrCodeProcessor
|
|
{
|
|
private const int QrCodeSearchDpi = 300; // High DPI for better QR code recognition
|
|
|
|
/// <inheritdoc />
|
|
[SupportedOSPlatform("windows")]
|
|
public async Task<SwissQrCodeData> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders a PDF page to a high-resolution bitmap for QR code detection
|
|
/// </summary>
|
|
private static DXBitmap RenderPageToImage(PdfDocumentProcessor processor, int pageIndex)
|
|
{
|
|
// Render page at high DPI for better QR code recognition
|
|
var pageImage = processor.CreateDXBitmap(pageIndex + 1, QrCodeSearchDpi);
|
|
return pageImage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decodes QR code from a DXBitmap image using ZXing library
|
|
/// </summary>
|
|
[SupportedOSPlatform("windows")]
|
|
private static string? DecodeQrCodeFromImage(DXBitmap dxImage)
|
|
{
|
|
// Convert DXBitmap to System.Drawing.Bitmap via MemoryStream
|
|
using var ms = new MemoryStream();
|
|
dxImage.Save(ms, DXImageFormat.Png);
|
|
ms.Position = 0;
|
|
|
|
using var gdiImage = Image.FromStream(ms);
|
|
using var gdiBitmap = new Bitmap(gdiImage);
|
|
|
|
var reader = new ZXing.Windows.Compatibility.BarcodeReader
|
|
{
|
|
AutoRotate = true,
|
|
Options = new ZXing.Common.DecodingOptions
|
|
{
|
|
PossibleFormats = [BarcodeFormat.QR_CODE],
|
|
TryHarder = true,
|
|
TryInverted = true
|
|
}
|
|
};
|
|
|
|
var result = reader.Decode(gdiBitmap);
|
|
return result?.Text;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses Swiss QR Bill content using Codecrete library
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines reference type based on reference string format
|
|
/// </summary>
|
|
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";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps Codecrete Address to our AddressData value object.
|
|
///
|
|
/// NOTE: AddressLine1 and AddressLine2 (Combined Address / K-Type) are deprecated
|
|
/// as of Swiss Payment Standards 2025 (effective 21 Nov 2025).
|
|
/// The Swiss QR Bill now mandates Structured Address (S-Type) format.
|
|
/// These fields are retained for backward compatibility with legacy QR codes
|
|
/// generated before the deprecation date.
|
|
/// </summary>
|
|
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,
|
|
#pragma warning disable CS0618 // AddressLine1/AddressLine2 obsolete but required for backward compatibility
|
|
AddressLine1 = address.AddressLine1,
|
|
AddressLine2 = address.AddressLine2,
|
|
#pragma warning restore CS0618
|
|
PostalCode = address.PostalCode ?? string.Empty,
|
|
City = address.Town ?? string.Empty,
|
|
Country = address.CountryCode ?? string.Empty
|
|
};
|
|
}
|
|
}
|