diff --git a/DocumentService.Client/Clients/BaseDocumentClient.cs b/DocumentService.Client/Clients/BaseDocumentClient.cs new file mode 100644 index 0000000..76600c5 --- /dev/null +++ b/DocumentService.Client/Clients/BaseDocumentClient.cs @@ -0,0 +1,179 @@ +using Microsoft.Extensions.Logging; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; + +namespace DocumentService.Client.Clients; + +/// +/// Base class for all DocumentService HTTP clients. +/// Provides common HTTP operations and error handling. +/// +/// +/// +/// +/// +/// +/// +public abstract class BaseDocumentClient(HttpClient HttpClient, ILogger logger) +{ + /// + /// + /// + protected readonly ILogger Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// + /// Sends a multipart/form-data POST with a single file and deserializes the JSON response. + /// + protected async Task SendMultipartAsync( + string endpoint, + Stream fileStream, + string fileName = "file.pdf", + CancellationToken cancellationToken = default) + { + using var content = new MultipartFormDataContent(); + var streamContent = new StreamContent(fileStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); + content.Add(streamContent, "file", fileName); + + var response = await HttpClient.PostAsync(endpoint, content, cancellationToken); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadFromJsonAsync(cancellationToken); + } + + /// + /// Sends a multipart/form-data POST with a single file and returns the response as a stream. + /// + protected async Task SendMultipartForStreamAsync( + string endpoint, + Stream fileStream, + string fileName = "file.pdf", + CancellationToken cancellationToken = default) + { + using var content = new MultipartFormDataContent(); + var streamContent = new StreamContent(fileStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); + content.Add(streamContent, "file", fileName); + + var response = await HttpClient.PostAsync(endpoint, content, cancellationToken); + response.EnsureSuccessStatusCode(); + +#if NETFRAMEWORK + return await response.Content.ReadAsStreamAsync(); +#else + return await response.Content.ReadAsStreamAsync(cancellationToken); +#endif + } + + /// + /// Sends a multipart/form-data POST with a single file and returns the raw byte array. + /// + protected async Task SendMultipartForBinaryAsync( + string endpoint, + Stream fileStream, + string fileName = "file.pdf", + CancellationToken cancellationToken = default) + { + using var content = new MultipartFormDataContent(); + var streamContent = new StreamContent(fileStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); + content.Add(streamContent, "file", fileName); + + var response = await HttpClient.PostAsync(endpoint, content, cancellationToken); + response.EnsureSuccessStatusCode(); + +#if NETFRAMEWORK + return await response.Content.ReadAsByteArrayAsync(); +#else + return await response.Content.ReadAsByteArrayAsync(cancellationToken); +#endif + } + + /// + /// Sends a caller-built and returns the response as a stream. + /// Use this overload when the multipart body contains more than one file (e.g., merge). + /// + protected async Task SendMultipartContentForStreamAsync( + string endpoint, + MultipartFormDataContent content, + CancellationToken cancellationToken = default) + { + var response = await HttpClient.PostAsync(endpoint, content, cancellationToken); + response.EnsureSuccessStatusCode(); + +#if NETFRAMEWORK + return await response.Content.ReadAsStreamAsync(); +#else + return await response.Content.ReadAsStreamAsync(cancellationToken); +#endif + } + + /// + /// Serializes as JSON, POSTs it, and deserializes the response. + /// + protected async Task SendJsonAsync( + string endpoint, + TRequest request, + CancellationToken cancellationToken = default) + { + var response = await HttpClient.PostAsJsonAsync(endpoint, request, cancellationToken); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadFromJsonAsync(cancellationToken); + } + + /// + /// Serializes as JSON, POSTs it, and returns the response as a stream. + /// + protected async Task SendJsonForStreamAsync( + string endpoint, + TRequest request, + CancellationToken cancellationToken = default) + { + var response = await HttpClient.PostAsJsonAsync(endpoint, request, cancellationToken); + response.EnsureSuccessStatusCode(); + +#if NETFRAMEWORK + return await response.Content.ReadAsStreamAsync(); +#else + return await response.Content.ReadAsStreamAsync(cancellationToken); +#endif + } + + /// + /// Serializes as JSON, POSTs it, and returns the raw byte array. + /// + protected async Task SendJsonForBinaryAsync( + string endpoint, + TRequest request, + CancellationToken cancellationToken = default) + { + var response = await HttpClient.PostAsJsonAsync(endpoint, request, cancellationToken); + response.EnsureSuccessStatusCode(); + +#if NETFRAMEWORK + return await response.Content.ReadAsByteArrayAsync(); +#else + return await response.Content.ReadAsByteArrayAsync(cancellationToken); +#endif + } + + /// Converts a byte array to a Base64 string. + protected static string ToBase64(byte[] bytes) => Convert.ToBase64String(bytes); + + /// Reads a stream fully into a byte array. + protected static async Task StreamToBytesAsync(Stream stream, CancellationToken cancellationToken = default) + { + if (stream is MemoryStream ms) + return ms.ToArray(); + + using var memoryStream = new MemoryStream(); +#if NET8_0 + await stream.CopyToAsync(memoryStream, cancellationToken); +#else + await stream.CopyToAsync(memoryStream); +#endif + return memoryStream.ToArray(); + } +} diff --git a/DocumentService.Client/Clients/PdfAttachmentClient.cs b/DocumentService.Client/Clients/PdfAttachmentClient.cs new file mode 100644 index 0000000..21bb9f5 --- /dev/null +++ b/DocumentService.Client/Clients/PdfAttachmentClient.cs @@ -0,0 +1,124 @@ +using DocumentService.Client.Interfaces; +using DocumentService.Client.Models.Requests; +using Microsoft.Extensions.Logging; +using System.IO.Compression; +using System.Net.Http; + +namespace DocumentService.Client.Clients; + +/// +/// Implementation of PDF attachment client. +/// +public class PdfAttachmentClient(HttpClient httpClient, ILogger logger) + : BaseDocumentClient(httpClient, logger), IPdfAttachmentClient +{ + /// + public async Task CheckAttachmentsAsync(Stream pdfStream, CancellationToken cancellationToken = default) + { + Logger.LogDebug("Checking PDF attachments from stream (multipart)"); + + var result = await SendMultipartAsync( + "/api/pdf/attachments/check", + pdfStream, + "document.pdf", + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } + + /// + public async Task CheckAttachmentsAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) + { + Logger.LogDebug("Checking PDF attachments from byte array (Base64 JSON)"); + + var request = new CheckPdfAttachmentsRequest { Base64Pdf = ToBase64(pdfBytes) }; + + var result = await SendJsonAsync( + "/api/pdf/attachments/check", + request, + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } + + /// + public async Task> ExtractAttachmentsAsync(Stream pdfStream, CancellationToken cancellationToken = default) + { + Logger.LogDebug("Extracting PDF attachments from stream (multipart)"); + + var zipStream = await SendMultipartForStreamAsync( + "/api/pdf/attachments/extract", + pdfStream, + "document.pdf", + cancellationToken); + + return UnzipToStreams(zipStream); + } + + /// + public async Task> ExtractAttachmentsAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) + { + Logger.LogDebug("Extracting PDF attachments from byte array (Base64 JSON)"); + + var request = new ExtractPdfAttachmentsRequest { Base64Pdf = ToBase64(pdfBytes) }; + + var zipStream = await SendJsonForStreamAsync( + "/api/pdf/attachments/extract", + request, + cancellationToken); + + return UnzipToStreams(zipStream); + } + + /// + [Obsolete("API endpoint not implemented yet")] + public Task AddAttachmentsAsync(Stream pdfStream, List attachments, CancellationToken cancellationToken = default) + { + Logger.LogWarning("AddAttachments endpoint is not implemented yet in API"); + throw new NotImplementedException("API endpoint /api/pdf/attachments/add is not implemented yet"); + } + + /// + [Obsolete("API endpoint not implemented yet")] + public Task AddAttachmentsAsync(byte[] pdfBytes, List attachments, CancellationToken cancellationToken = default) + { + Logger.LogWarning("AddAttachments endpoint is not implemented yet in API"); + throw new NotImplementedException("API endpoint /api/pdf/attachments/add is not implemented yet"); + } + + // ????????????????????????????????????????????????????????????????????????? + // Private helpers + // ????????????????????????????????????????????????????????????????????????? + + /// + /// Reads a ZIP stream and returns a dictionary mapping each entry's full name + /// to an in-memory containing the decompressed bytes. + /// The caller owns the returned streams and is responsible for disposing them. + /// + private static Dictionary UnzipToStreams(Stream zipStream) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read, leaveOpen: false); + + foreach (var entry in archive.Entries) + { + // Skip directory entries (name ends with '/') + if (string.IsNullOrEmpty(entry.Name)) + continue; + + var ms = new MemoryStream((int)entry.Length); + + using (var entryStream = entry.Open()) + { + entryStream.CopyTo(ms); + } + + ms.Position = 0; + result[entry.FullName] = ms; + } + + return result; + } +} + diff --git a/DocumentService.Client/Clients/PdfConversionClient.cs b/DocumentService.Client/Clients/PdfConversionClient.cs new file mode 100644 index 0000000..0ecb0ed --- /dev/null +++ b/DocumentService.Client/Clients/PdfConversionClient.cs @@ -0,0 +1,48 @@ +using DocumentService.Client.Interfaces; +using DocumentService.Client.Models.Requests; +using Microsoft.Extensions.Logging; +using System.Net.Http; + +namespace DocumentService.Client.Clients; + +/// +/// Implementation of PDF conversion client (PDF ? PDF/A). +/// +/// +/// All methods throw because the corresponding +/// API endpoints are not yet implemented on the server side. +/// +public class PdfConversionClient(HttpClient httpClient, ILogger logger) : BaseDocumentClient(httpClient, logger), IPdfConversionClient +{ + /// + [Obsolete("API endpoint not implemented yet")] + public Task ConvertToPdfAAsync(Stream pdfStream, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default) + { + Logger.LogWarning("ConvertToPdfA endpoint is not implemented yet in API"); + throw new NotImplementedException("API endpoint POST /api/pdf/conversion/to-pdfa is not implemented yet"); + } + + /// + [Obsolete("API endpoint not implemented yet")] + public Task ConvertToPdfAAsync(byte[] pdfBytes, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default) + { + Logger.LogWarning("ConvertToPdfA endpoint is not implemented yet in API"); + throw new NotImplementedException("API endpoint POST /api/pdf/conversion/to-pdfa is not implemented yet"); + } + + /// + [Obsolete("API endpoint not implemented yet")] + public Task ConvertFromPdfAAsync(Stream pdfStream, CancellationToken cancellationToken = default) + { + Logger.LogWarning("ConvertFromPdfA endpoint is not implemented yet in API"); + throw new NotImplementedException("API endpoint POST /api/pdf/conversion/from-pdfa is not implemented yet"); + } + + /// + [Obsolete("API endpoint not implemented yet")] + public Task ConvertFromPdfAAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) + { + Logger.LogWarning("ConvertFromPdfA endpoint is not implemented yet in API"); + throw new NotImplementedException("API endpoint POST /api/pdf/conversion/from-pdfa is not implemented yet"); + } +} diff --git a/DocumentService.Client/Clients/PdfOperationsClient.cs b/DocumentService.Client/Clients/PdfOperationsClient.cs new file mode 100644 index 0000000..3ba4919 --- /dev/null +++ b/DocumentService.Client/Clients/PdfOperationsClient.cs @@ -0,0 +1,161 @@ +using DocumentService.Client.Interfaces; +using DocumentService.Client.Models.Requests; +using Microsoft.Extensions.Logging; +using System.Net.Http; +using System.Net.Http.Headers; + +namespace DocumentService.Client.Clients; + +/// +/// Implementation of PDF operations client (merge, annotate, stamp). +/// +public class PdfOperationsClient(HttpClient httpClient, ILogger logger) : BaseDocumentClient(httpClient, logger), IPdfOperationsClient +{ + + // ==================== MERGE OPERATIONS ==================== + + /// + public async Task MergeAsync(IEnumerable pdfStreams, List? pageRanges = null, CancellationToken cancellationToken = default) + { + using var content = new MultipartFormDataContent(); + + foreach (var stream in pdfStreams) + { + var streamContent = new StreamContent(stream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); + content.Add(streamContent, "files", $"file_{Guid.NewGuid()}.pdf"); + } + + return await SendMultipartContentForStreamAsync("/api/pdf/operations/merge", content, cancellationToken); + } + + /// + public async Task MergeAsync(IEnumerable pdfByteArrays, List? pageRanges = null, CancellationToken cancellationToken = default) + { + var request = new MergePdfsBase64Request + { + Base64Pdfs = [.. pdfByteArrays.Select(ToBase64)], + PageRanges = pageRanges + }; + + return await SendJsonForStreamAsync( + "/api/pdf/operations/merge", + request, + cancellationToken); + } + + // ==================== ANNOTATION OPERATIONS ==================== + + /// + public async Task AnnotateAsync(Stream pdfStream, AddAnnotationBase64Request request, CancellationToken cancellationToken = default) + { + // For multipart, we need to send form data with all annotation parameters + using var content = new MultipartFormDataContent(); + + var streamContent = new StreamContent(pdfStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); + content.Add(streamContent, "File", "document.pdf"); + + // Add annotation parameters as form fields + content.Add(new StringContent(request.AnnotationType.ToString()), "AnnotationType"); + content.Add(new StringContent(request.PageNumber.ToString()), "PageNumber"); + content.Add(new StringContent(request.X1.ToString()), "X1"); + content.Add(new StringContent(request.Y1.ToString()), "Y1"); + + if (request.X2.HasValue) + content.Add(new StringContent(request.X2.Value.ToString()), "X2"); + if (request.Y2.HasValue) + content.Add(new StringContent(request.Y2.Value.ToString()), "Y2"); + if (request.Width.HasValue) + content.Add(new StringContent(request.Width.Value.ToString()), "Width"); + if (request.Height.HasValue) + content.Add(new StringContent(request.Height.Value.ToString()), "Height"); + if (!string.IsNullOrEmpty(request.Content)) + content.Add(new StringContent(request.Content), "Content"); + if (!string.IsNullOrEmpty(request.Author)) + content.Add(new StringContent(request.Author), "Author"); + if (!string.IsNullOrEmpty(request.Color)) + content.Add(new StringContent(request.Color), "Color"); + if (request.TextMarkupStyle.HasValue) + content.Add(new StringContent(request.TextMarkupStyle.Value.ToString()), "TextMarkupStyle"); + + content.Add(new StringContent(request.Origin.ToString()), "Origin"); + + return await SendMultipartContentForStreamAsync("/api/pdf/operations/annotate", content, cancellationToken); + } + + /// + public async Task AnnotateAsync(byte[] pdfBytes, AddAnnotationBase64Request request, CancellationToken cancellationToken = default) + { + var requestWithPdf = request with { Base64Pdf = ToBase64(pdfBytes) }; + + return await SendJsonForStreamAsync( + "/api/pdf/operations/annotate", + requestWithPdf, + cancellationToken); + } + + // ==================== STAMP OPERATIONS ==================== + + /// + public async Task StampAsync(Stream pdfStream, AddStampBase64Request request, CancellationToken cancellationToken = default) + { + // For multipart, we need to send form data with all stamp parameters + using var content = new MultipartFormDataContent(); + + var streamContent = new StreamContent(pdfStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); + content.Add(streamContent, "File", "document.pdf"); + + // Add stamp parameters as form fields + content.Add(new StringContent(request.StampType.ToString()), "StampType"); + content.Add(new StringContent(request.X.ToString()), "X"); + content.Add(new StringContent(request.Y.ToString()), "Y"); + + if (request.PageNumbers != null && request.PageNumbers.Length > 0) + { + foreach (var pageNum in request.PageNumbers) + { + content.Add(new StringContent(pageNum.ToString()), "PageNumbers"); + } + } + + if (request.Width.HasValue) + content.Add(new StringContent(request.Width.Value.ToString()), "Width"); + if (request.Height.HasValue) + content.Add(new StringContent(request.Height.Value.ToString()), "Height"); + + content.Add(new StringContent(request.Origin.ToString()), "Origin"); + + if (!string.IsNullOrEmpty(request.Text)) + content.Add(new StringContent(request.Text), "Text"); + if (!string.IsNullOrEmpty(request.FontName)) + content.Add(new StringContent(request.FontName), "FontName"); + if (request.FontSize.HasValue) + content.Add(new StringContent(request.FontSize.Value.ToString()), "FontSize"); + if (!string.IsNullOrEmpty(request.Color)) + content.Add(new StringContent(request.Color), "Color"); + if (request.Opacity.HasValue) + content.Add(new StringContent(request.Opacity.Value.ToString()), "Opacity"); + if (request.Rotation.HasValue) + content.Add(new StringContent(request.Rotation.Value.ToString()), "Rotation"); + + content.Add(new StringContent(request.Placement.ToString()), "Placement"); + + if (request.PredefinedType.HasValue) + content.Add(new StringContent(request.PredefinedType.Value.ToString()), "PredefinedType"); + + return await SendMultipartContentForStreamAsync("/api/pdf/operations/stamp", content, cancellationToken); + } + + /// + public async Task StampAsync(byte[] pdfBytes, AddStampBase64Request request, CancellationToken cancellationToken = default) + { + var requestWithPdf = request with { Base64Pdf = ToBase64(pdfBytes) }; + + return await SendJsonForStreamAsync( + "/api/pdf/operations/stamp", + requestWithPdf, + cancellationToken); + } +} diff --git a/DocumentService.Client/Clients/PdfValidationClient.cs b/DocumentService.Client/Clients/PdfValidationClient.cs new file mode 100644 index 0000000..bb2a179 --- /dev/null +++ b/DocumentService.Client/Clients/PdfValidationClient.cs @@ -0,0 +1,68 @@ +using DocumentService.Client.Interfaces; +using DocumentService.Client.Models.Requests; +using Microsoft.Extensions.Logging; +using System.Net.Http; + +namespace DocumentService.Client.Clients; + +/// +/// Implementation of PDF validation client. +/// +public class PdfValidationClient(HttpClient httpClient, ILogger logger) : BaseDocumentClient(httpClient, logger), IPdfValidationClient +{ + /// + public async Task ValidatePdfAsync(Stream pdfStream, CancellationToken cancellationToken = default) + { + var result = await SendMultipartAsync( + "/api/pdf/validation/validate", + pdfStream, + "document.pdf", + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } + + /// + public async Task ValidatePdfAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) + { + var request = new ValidatePdfBase64Request + { + Base64Pdf = ToBase64(pdfBytes) + }; + + var result = await SendJsonAsync( + "/api/pdf/validation/validate", + request, + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } + + /// + public async Task ValidatePdfAAsync(Stream pdfStream, CancellationToken cancellationToken = default) + { + var result = await SendMultipartAsync( + "/api/pdf/validation/validate-pdfa", + pdfStream, + "document.pdf", + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } + + /// + public async Task ValidatePdfAAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) + { + var request = new ValidatePdfABase64Request + { + Base64Pdf = ToBase64(pdfBytes) + }; + + var result = await SendJsonAsync( + "/api/pdf/validation/validate-pdfa", + request, + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } +} diff --git a/DocumentService.Client/Clients/SwissQrCodeClient.cs b/DocumentService.Client/Clients/SwissQrCodeClient.cs new file mode 100644 index 0000000..ada933d --- /dev/null +++ b/DocumentService.Client/Clients/SwissQrCodeClient.cs @@ -0,0 +1,44 @@ +using DocumentService.Client.Interfaces; +using DocumentService.Client.Models.Requests; +using Microsoft.Extensions.Logging; +using System.Net.Http; + +namespace DocumentService.Client.Clients; + +/// +/// Implementation of Swiss QR Code extraction client. +/// +public class SwissQrCodeClient(HttpClient httpClient, ILogger logger) : BaseDocumentClient(httpClient, logger), ISwissQrCodeClient +{ + /// + public async Task ExtractSwissQrCodeAsync(Stream pdfStream, bool raw = false, CancellationToken cancellationToken = default) + { + var endpoint = $"/api/pdf/qr-code/extract-swiss?raw={raw}"; + + var result = await SendMultipartAsync( + endpoint, + pdfStream, + "document.pdf", + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } + + /// + public async Task ExtractSwissQrCodeAsync(byte[] pdfBytes, bool raw = false, CancellationToken cancellationToken = default) + { + var endpoint = $"/api/pdf/qr-code/extract-swiss?raw={raw}"; + + var request = new ExtractSwissQrCodeBase64Request + { + Base64Pdf = ToBase64(pdfBytes) + }; + + var result = await SendJsonAsync( + endpoint, + request, + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } +} diff --git a/DocumentService.Client/Clients/ZugferdClient.cs b/DocumentService.Client/Clients/ZugferdClient.cs new file mode 100644 index 0000000..b7f1589 --- /dev/null +++ b/DocumentService.Client/Clients/ZugferdClient.cs @@ -0,0 +1,87 @@ +using DocumentService.Client.Interfaces; +using DocumentService.Client.Models.Requests; +using Microsoft.Extensions.Logging; +using System.Net.Http; + +namespace DocumentService.Client.Clients; + +/// +/// Implementation of ZUGFeRD client (detection and extraction). +/// +public class ZugferdClient(HttpClient httpClient, ILogger logger) : BaseDocumentClient(httpClient, logger), IZugferdClient +{ + /// + public async Task HasZugferdAsync(Stream pdfStream, CancellationToken cancellationToken = default) + { + var result = await SendMultipartAsync( + "/api/pdf/zugferd/has-zugferd", + pdfStream, + "document.pdf", + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } + + /// + public async Task HasZugferdAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) + { + var request = new HasZugferdRequest { Base64Pdf = ToBase64(pdfBytes) }; + + var result = await SendJsonAsync( + "/api/pdf/zugferd/has-zugferd", + request, + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } + + /// + public async Task ExtractZugferdAsync(Stream pdfStream, CancellationToken cancellationToken = default) + { + // asFile=true returns the XML file directly (application/xml stream) + return await SendMultipartForStreamAsync( + "/api/pdf/zugferd/extract?asFile=true", + pdfStream, + "document.pdf", + cancellationToken); + } + + /// + public async Task ExtractZugferdAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) + { + var request = new ExtractZugferdRequest { Base64Pdf = ToBase64(pdfBytes) }; + + // format=file returns the XML file directly (application/xml stream) + return await SendJsonForStreamAsync( + "/api/pdf/zugferd/extract?format=file", + request, + cancellationToken); + } + + /// + public async Task ExtractZugferdAsResultAsync(Stream pdfStream, CancellationToken cancellationToken = default) + { + // asFile=false returns JSON with metadata + XML content + var result = await SendMultipartAsync( + "/api/pdf/zugferd/extract?asFile=false", + pdfStream, + "document.pdf", + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } + + /// + public async Task ExtractZugferdAsResultAsync(byte[] pdfBytes, CancellationToken cancellationToken = default) + { + var request = new ExtractZugferdRequest { Base64Pdf = ToBase64(pdfBytes) }; + + // format=json returns JSON with metadata + XML content + var result = await SendJsonAsync( + "/api/pdf/zugferd/extract?format=json", + request, + cancellationToken); + + return result ?? throw new InvalidOperationException("API returned null response"); + } +}