Render invoice-editor images (mfr portal only) into the PDF
The invoice line editor already shows <img> pictures pulled from the mfr portal, but the PDF pipeline's HTML-to-MigraDoc converter had no <img> handler, so they never appeared on the actual invoice. InvoiceService now fetches only https://portal.mobilefieldreport.com/mfr/* image sources before rendering (size/type/count capped, failures logged and skipped) and hands the resolved images down through ApplyInvoice into the HTML converter, which renders them inline at their original HTML position and size (px converted to cm, capped against layout overflow). See ADR 0018. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -709,7 +709,8 @@ public static class FuchsPdf
|
||||
}
|
||||
|
||||
// ── ApplyInvoice ──────────────────────────────────────────────────────────
|
||||
public static void ApplyInvoice(Document doc, FdsTextBlocks tb, FdsInvoiceData inv, bool draft = false)
|
||||
public static void ApplyInvoice(Document doc, FdsTextBlocks tb, FdsInvoiceData inv, bool draft = false,
|
||||
IReadOnlyDictionary<string, string>? inlineImages = null)
|
||||
{
|
||||
string invoiceType = inv.InvoiceType;
|
||||
bool p13b = inv.InvoiceRegistration?.getString("InvoiceOptions")
|
||||
@@ -813,7 +814,7 @@ public static class FuchsPdf
|
||||
if (line.IsSetHeader) titleCell.AddFormattedText(line.Title, TextFormat.Bold);
|
||||
else titleCell.AddText(line.Title);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(line.Desc)) row.Cells[1].AddHtml($"<div>{line.Desc}</div>");
|
||||
if (!string.IsNullOrEmpty(line.Desc)) row.Cells[1].AddHtml($"<div>{line.Desc}</div>", inlineImages);
|
||||
row.Cells[2].AddParagraph(line.Qty).Style = "TblCell_Base";
|
||||
row.Cells[3].AddParagraph(line.ShowPrice ? Currency(line.PriceNet) : "").Style = "TblCell_Base";
|
||||
row.Cells[4].AddParagraph(line.ShowPrice ? Currency(line.TotalNet) : "").Style = "TblCell_RSum";
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
using MigraDoc.DocumentObjectModel;
|
||||
using MigraDoc.DocumentObjectModel.Shapes;
|
||||
using MigraDoc.DocumentObjectModel.Tables;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
// Ported from migradoc_Extensions.vb + migradoc_HtmlConverter.vb
|
||||
|
||||
@@ -24,8 +26,13 @@ namespace MigraDoc.Extensions
|
||||
|
||||
public static Section AddHtml(this Section section, string html)
|
||||
=> section.Add(html, new MigraDoc.Extensions.Html.HtmlConverter());
|
||||
public static Cell AddHtml(this Cell cell, string html)
|
||||
=> cell.Add(html, new MigraDoc.Extensions.Html.HtmlConverter());
|
||||
/// <param name="inlineImages">Maps an <img> element's original <c>src</c> to a MigraDoc
|
||||
/// image source ("base64:...") for images already fetched/vetted by the caller (see
|
||||
/// InvoiceService.ResolveInlineImagesAsync / ADR 0018). Src values not present here are
|
||||
/// dropped, matching the converter's long-standing behaviour for unsupported elements.</param>
|
||||
public static Cell AddHtml(this Cell cell, string html,
|
||||
IReadOnlyDictionary<string, string>? inlineImages = null)
|
||||
=> cell.Add(html, new MigraDoc.Extensions.Html.HtmlConverter(inlineImages));
|
||||
public static TextFrame AddHtml(this TextFrame frame, string html)
|
||||
=> frame.Add(html, new MigraDoc.Extensions.Html.HtmlConverter());
|
||||
|
||||
@@ -59,9 +66,11 @@ namespace MigraDoc.Extensions.Html
|
||||
public class HtmlConverter : MigraDoc.Extensions.IConverter
|
||||
{
|
||||
private readonly IDictionary<string, Func<HtmlNode, DocumentObject, DocumentObject>> _handlers;
|
||||
private readonly IReadOnlyDictionary<string, string>? _inlineImages;
|
||||
|
||||
public HtmlConverter()
|
||||
public HtmlConverter(IReadOnlyDictionary<string, string>? inlineImages = null)
|
||||
{
|
||||
_inlineImages = inlineImages;
|
||||
_handlers = new Dictionary<string, Func<HtmlNode, DocumentObject, DocumentObject>>();
|
||||
AddDefaultHandlers();
|
||||
}
|
||||
@@ -103,6 +112,32 @@ namespace MigraDoc.Extensions.Html
|
||||
};
|
||||
_handlers["hr"] = (n, p) => { var para = GetPara(p); if (para != null) para.Style = "HorizontalRule"; return (DocumentObject?)para ?? p; };
|
||||
|
||||
// Image — only src values pre-resolved by the caller (see ctor) are rendered; anything
|
||||
// else (including any remote URL the caller didn't fetch/vet) is silently dropped, same
|
||||
// as every other unsupported element. Size mirrors what the editor shows (the width/height
|
||||
// TinyMCE writes onto the <img> from its resize handles), capped so a stray huge size from
|
||||
// hand-edited HTML can't blow out the invoice table layout.
|
||||
_handlers["img"] = (n, p) =>
|
||||
{
|
||||
string src = n.GetAttributeValue("src", "");
|
||||
if (src.Length == 0 || _inlineImages == null || !_inlineImages.TryGetValue(src, out var source))
|
||||
return p;
|
||||
var para = GetPara(p);
|
||||
if (para == null) return p;
|
||||
var img = para.AddImage(source);
|
||||
img.LockAspectRatio = true;
|
||||
|
||||
double? wPx = ReadPxDimension(n, "width");
|
||||
double? hPx = ReadPxDimension(n, "height");
|
||||
if (wPx is > 0 && hPx is > 0) { img.LockAspectRatio = false; img.Width = PxToUnit(wPx.Value); img.Height = PxToUnit(hPx.Value); }
|
||||
else if (wPx is > 0) img.Width = PxToUnit(wPx.Value);
|
||||
else if (hPx is > 0) img.Height = PxToUnit(hPx.Value);
|
||||
|
||||
if (img.Width > MaxInlineImageWidth) { img.LockAspectRatio = true; img.Width = MaxInlineImageWidth; }
|
||||
if (img.Height > MaxInlineImageHeight) { img.LockAspectRatio = true; img.Height = MaxInlineImageHeight; }
|
||||
return para;
|
||||
};
|
||||
|
||||
// Headings
|
||||
foreach (int i in Enumerable.Range(1, 6))
|
||||
{
|
||||
@@ -148,6 +183,31 @@ namespace MigraDoc.Extensions.Html
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
private const double CssPxPerCm = 96 / 2.54; // CSS px (96dpi) → cm, matches browser rendering
|
||||
private static readonly Unit MaxInlineImageWidth = Unit.FromCentimeter(7);
|
||||
private static readonly Unit MaxInlineImageHeight = Unit.FromCentimeter(10);
|
||||
private static readonly Regex StyleSizeRegex =
|
||||
new(@"(?<prop>width|height)\s*:\s*(?<val>[\d.]+)px", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>Reads a pixel width/height from the HTML <c>width</c>/<c>height</c> attribute
|
||||
/// (as TinyMCE's image resize handles write it), falling back to an inline <c>style</c>
|
||||
/// declaration. Percentage/unit-less-other sizes and missing values return null.</summary>
|
||||
private static double? ReadPxDimension(HtmlNode n, string attr)
|
||||
{
|
||||
string raw = n.GetAttributeValue(attr, "");
|
||||
if (double.TryParse(raw, NumberStyles.Any, CultureInfo.InvariantCulture, out var px) && px > 0)
|
||||
return px;
|
||||
string style = n.GetAttributeValue("style", "");
|
||||
foreach (Match m in StyleSizeRegex.Matches(style))
|
||||
if (string.Equals(m.Groups["prop"].Value, attr, StringComparison.OrdinalIgnoreCase)
|
||||
&& double.TryParse(m.Groups["val"].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var spx)
|
||||
&& spx > 0)
|
||||
return spx;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Unit PxToUnit(double px) => Unit.FromCentimeter(px / CssPxPerCm);
|
||||
|
||||
private static DocumentObject Fmt(DocumentObject parent, TextFormat fmt)
|
||||
{
|
||||
if (parent is FormattedText ft) return ft.AddFormattedText(fmt);
|
||||
|
||||
Reference in New Issue
Block a user