Updated PHASENPLAN.md and ROADMAP.md to reflect the new feature order, making "ExtractSwissQrCode" Feature 2 and renumbering previous Features 2-5 to 3-6. Added detailed steps, endpoints, and acceptance criteria for the new feature. Implemented `ISwissQrCodeProcessor` interface with `DevExpressSwissQrCodeProcessor` for extracting and parsing Swiss QR Codes using DevExpress and Codecrete libraries. Registered the new service in DependencyInjection.cs. Introduced `SwissQrCodeData` value object and `SwissQrCodeNotFoundException` for domain modeling and error handling. Updated project dependencies to include libraries for QR code processing. Adjusted existing feature descriptions and steps to align with the new feature order.
176 lines
6.3 KiB
C#
176 lines
6.3 KiB
C#
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;
|
|
|
|
/// <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 />
|
|
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 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decodes QR code from an image using ZXing library
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <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
|
|
/// </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,
|
|
AddressLine1 = address.AddressLine1,
|
|
AddressLine2 = address.AddressLine2,
|
|
PostalCode = address.PostalCode ?? string.Empty,
|
|
City = address.Town ?? string.Empty,
|
|
Country = address.CountryCode ?? string.Empty
|
|
};
|
|
}
|
|
}
|