Skip to content

Commit bc697cd

Browse files
committed
Update comparison reports and images for MiniPdf vs Reference PDF
- Updated the generated timestamps in comparison reports for both DOCX and XLSX formats. - Improved scores and visual comparisons for the "SA8000 ch sample" and "Expense report basic1" entries. - Added new images for "SA8000 ch sample" and "Weekly schedule planner1" comparisons. - Enhanced the pixel comparison algorithm in `compare_pdfs.py` to resize images for better accuracy. - Added a new entry for "Weekly schedule planner1" in the XLSX report with detailed results. - Adjusted overall average scores in both reports based on updated visual scores.
1 parent 7803f0c commit bc697cd

18 files changed

Lines changed: 620 additions & 144 deletions

MiniPdf.Api/Program.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
foreach (var dir in new[] { "/home/fonts", Path.Combine(AppContext.BaseDirectory, "Fonts") })
66
{
77
if (!Directory.Exists(dir)) continue;
8-
foreach (var file in Directory.GetFiles(dir, "*.ttf"))
9-
{
10-
var name = Path.GetFileNameWithoutExtension(file);
11-
MiniPdf.RegisterFont(name, File.ReadAllBytes(file));
12-
}
8+
foreach (var ext in new[] { "*.ttf", "*.ttc", "*.otf" })
9+
foreach (var file in Directory.GetFiles(dir, ext))
10+
{
11+
try
12+
{
13+
var name = Path.GetFileNameWithoutExtension(file);
14+
MiniPdf.RegisterFont(name, File.ReadAllBytes(file));
15+
}
16+
catch { /* skip fonts that fail to parse */ }
17+
}
1318
}
1419

1520
var builder = WebApplication.CreateBuilder(args);

MiniPdf.Api/startup.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
# /home/ is persistent storage on Azure App Service Linux.
55

66
FONT_DIR="/home/fonts"
7+
FONT_COUNT=$(find "$FONT_DIR" -maxdepth 1 -name '*.ttf' -o -name '*.ttc' 2>/dev/null | wc -l)
78

8-
if [ ! -f "$FONT_DIR/.done" ]; then
9+
if [ "$FONT_COUNT" -ge 10 ]; then
10+
echo "[startup] Found $FONT_COUNT fonts in $FONT_DIR (uploaded), skipping download."
11+
elif [ ! -f "$FONT_DIR/.done" ]; then
912
echo "[startup] Downloading fonts to $FONT_DIR ..."
1013
mkdir -p "$FONT_DIR"
1114

src/MiniPdf/ExcelReader.cs

Lines changed: 260 additions & 32 deletions
Large diffs are not rendered by default.

src/MiniPdf/ExcelToPdfConverter.cs

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ private static void RenderSheet(PdfDocument doc, ExcelSheet sheet, ConversionOpt
314314
columnWidths: sheet.ColumnWidths,
315315
defaultColumnWidth: sheet.DefaultColumnWidth,
316316
charts: sheet.Charts.Count > 0 ? sheet.Charts : null,
317+
shapes: sheet.Shapes.Count > 0 ? sheet.Shapes : null,
317318
mergedCells: sheet.MergedCells,
318319
rowHeights: sheet.RowHeights,
319320
defaultRowHeight: sheet.DefaultRowHeight,
@@ -372,6 +373,7 @@ private static void RenderSheet(PdfDocument doc, ExcelSheet sheet, ConversionOpt
372373
images: sheet.Images.Count > 0 ? sheet.Images : null,
373374
columnWidths: sheet.ColumnWidths, defaultColumnWidth: sheet.DefaultColumnWidth,
374375
charts: sheet.Charts.Count > 0 ? sheet.Charts : null,
376+
shapes: sheet.Shapes.Count > 0 ? sheet.Shapes : null,
375377
mergedCells: sheet.MergedCells, rowHeights: sheet.RowHeights,
376378
defaultRowHeight: sheet.DefaultRowHeight, customHeightRows: sheet.CustomHeightRows,
377379
isLandscape: sheet.IsLandscape, printScale: combined2, paperSize: sheet.PaperSize,
@@ -566,6 +568,7 @@ private static void RenderSheet(PdfDocument doc, ExcelSheet sheet, ConversionOpt
566568
columnWidths: trimmedColWidths,
567569
defaultColumnWidth: sheet.DefaultColumnWidth,
568570
charts: trimmedCharts.Count > 0 ? trimmedCharts : null,
571+
shapes: sheet.Shapes.Count > 0 ? sheet.Shapes : null,
569572
mergedCells: trimmedMerged,
570573
rowHeights: trimmedRowHeights,
571574
defaultRowHeight: sheet.DefaultRowHeight,
@@ -1688,25 +1691,25 @@ float RenderPrintTitleRows()
16881691
{
16891692
var bc = border.Left.Color ?? borderColor;
16901693
var bw = Math.Max(0.08f, BorderStyleWidth(border.Left.Style) * borderScaleFactor);
1691-
currentPage!.AddLine(bx, byTop, bx, byBottom, bc, bw);
1694+
currentPage!.AddLine(bx, byTop, bx, byBottom, bc, bw, BorderDashPattern(border.Left.Style, bw));
16921695
}
16931696
if (border.Right is { Style: not "none" and not "" })
16941697
{
16951698
var bc = border.Right.Color ?? borderColor;
16961699
var bw = Math.Max(0.08f, BorderStyleWidth(border.Right.Style) * borderScaleFactor);
1697-
currentPage!.AddLine(bxRight, byTop, bxRight, byBottom, bc, bw);
1700+
currentPage!.AddLine(bxRight, byTop, bxRight, byBottom, bc, bw, BorderDashPattern(border.Right.Style, bw));
16981701
}
16991702
if (border.Top is { Style: not "none" and not "" })
17001703
{
17011704
var bc = border.Top.Color ?? borderColor;
17021705
var bw = Math.Max(0.08f, BorderStyleWidth(border.Top.Style) * borderScaleFactor);
1703-
currentPage!.AddLine(bx, byTop, bxRight, byTop, bc, bw);
1706+
currentPage!.AddLine(bx, byTop, bxRight, byTop, bc, bw, BorderDashPattern(border.Top.Style, bw));
17041707
}
17051708
if (border.Bottom is { Style: not "none" and not "" })
17061709
{
17071710
var bc = border.Bottom.Color ?? borderColor;
17081711
var bw = Math.Max(0.08f, BorderStyleWidth(border.Bottom.Style) * borderScaleFactor);
1709-
currentPage!.AddLine(bx, byBottom, bxRight, byBottom, bc, bw);
1712+
currentPage!.AddLine(bx, byBottom, bxRight, byBottom, bc, bw, BorderDashPattern(border.Bottom.Style, bw));
17101713
}
17111714
}
17121715

@@ -1796,6 +1799,55 @@ float RenderPrintTitleRows()
17961799

17971800
// (Trailing empty page logic moved to RenderSheet for proper per-sheet page tracking)
17981801

1802+
// Place drawing shapes (rectangles used as decorative frames)
1803+
const float ShapeEmuToPt = 1f / 12700f;
1804+
foreach (var shape in sheet.Shapes)
1805+
{
1806+
// Resolve anchor positions using rowTopY and colXStarts
1807+
if (!rowTopY.TryGetValue(shape.FromRow, out var shapeTop))
1808+
continue;
1809+
if (!rowTopY.TryGetValue(shape.ToRow, out var shapeBot))
1810+
{
1811+
// If toRow is beyond rendered rows, use bottom margin
1812+
shapeBot = options.MarginBottom;
1813+
}
1814+
1815+
// Determine which column group the shape's from-column falls into
1816+
var fromIdx = Array.IndexOf(columns, shape.FromCol);
1817+
if (fromIdx < 0) fromIdx = 0;
1818+
var toIdx = Array.IndexOf(columns, shape.ToCol);
1819+
if (toIdx < 0) toIdx = columns.Length - 1;
1820+
1821+
var shapeX = colXStarts[fromIdx] + shape.FromColOffEmu * ShapeEmuToPt;
1822+
var shapeXRight = toIdx < colXStarts.Length ?
1823+
colXStarts[toIdx] + shape.ToColOffEmu * ShapeEmuToPt :
1824+
colXStarts[^1] + colWidths[^1];
1825+
1826+
// Adjust for row offsets
1827+
shapeTop -= shape.FromRowOffEmu * ShapeEmuToPt;
1828+
shapeBot -= shape.ToRowOffEmu * ShapeEmuToPt;
1829+
1830+
var shapeW = shapeXRight - shapeX;
1831+
var shapeH = shapeTop - shapeBot;
1832+
if (shapeW <= 0 || shapeH <= 0) continue;
1833+
1834+
var shapePage = rowPage.TryGetValue(shape.FromRow, out var sp) ? sp : currentPage!;
1835+
1836+
// Render fill
1837+
if (shape.FillColor is { } fc)
1838+
shapePage.AddRectangle(shapeX, shapeBot, shapeW, shapeH, fc);
1839+
1840+
// Render border
1841+
if (shape.BorderColor is { } bc && shape.BorderWidthPt > 0)
1842+
{
1843+
var bw = shape.BorderWidthPt;
1844+
shapePage.AddLine(shapeX, shapeTop, shapeXRight, shapeTop, bc, bw); // top
1845+
shapePage.AddLine(shapeX, shapeBot, shapeXRight, shapeBot, bc, bw); // bottom
1846+
shapePage.AddLine(shapeX, shapeTop, shapeX, shapeBot, bc, bw); // left
1847+
shapePage.AddLine(shapeXRight, shapeTop, shapeXRight, shapeBot, bc, bw); // right
1848+
}
1849+
}
1850+
17991851
// Place embedded images and chart placeholders
18001852
if (sheet.Images.Count == 0 && sheet.Charts.Count == 0) return;
18011853

@@ -3291,8 +3343,22 @@ private static bool ShouldUsePdfBold(bool requestedBold, float fontSize, string?
32913343
{
32923344
"thick" => 1.5f,
32933345
"medium" or "mediumDashed" or "mediumDashDot" or "mediumDashDotDot" => 1f,
3294-
"hair" => 0.1f,
3295-
_ => 0.3f // thin, dashed, dotted, dashDot, dashDotDot, double, slantDashDot
3346+
"dotted" or "hair" => 0.1f,
3347+
_ => 0.3f // thin, dashed, dashDot, dashDotDot, double, slantDashDot
3348+
};
3349+
3350+
/// <summary>
3351+
/// Maps OOXML border style names to PDF dash patterns.
3352+
/// Returns null for solid lines.
3353+
/// </summary>
3354+
private static float[]? BorderDashPattern(string style, float lineWidth) => style switch
3355+
{
3356+
"dotted" => new[] { 0.2f, 0.8f },
3357+
"dashed" or "mediumDashed" => new[] { 4f, 2f },
3358+
"dashDot" or "mediumDashDot" or "slantDashDot" => new[] { 4f, 1.5f, Math.Max(0.5f, lineWidth), 1.5f },
3359+
"dashDotDot" or "mediumDashDotDot" => new[] { 4f, 1f, Math.Max(0.5f, lineWidth), 1f, Math.Max(0.5f, lineWidth), 1f },
3360+
"hair" => new[] { 0.5f, 0.5f },
3361+
_ => null // solid: thin, medium, thick
32963362
};
32973363

32983364
/// <summary>

src/MiniPdf/PdfPage.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ internal sealed record PdfLineBlock(
6161
float X2, // end X in points
6262
float Y2, // end Y in points
6363
PdfColor Color, // stroke color
64-
float LineWidth = 1f // stroke width in points
64+
float LineWidth = 1f, // stroke width in points
65+
float[]? DashPattern = null // PDF dash array (e.g. [1,1] for dotted, [3,3] for dashed)
6566
);
6667

6768
/// <summary>
@@ -209,9 +210,9 @@ public PdfPage AddCompoundPolygon(List<List<PdfPoint>> subpaths, PdfColor? fillC
209210
/// <summary>
210211
/// Adds a line segment at the specified coordinates.
211212
/// </summary>
212-
public PdfPage AddLine(float x1, float y1, float x2, float y2, PdfColor? color = null, float lineWidth = 1f)
213+
public PdfPage AddLine(float x1, float y1, float x2, float y2, PdfColor? color = null, float lineWidth = 1f, float[]? dashPattern = null)
213214
{
214-
_lineBlocks.Add(new PdfLineBlock(x1, y1, x2, y2, color ?? new PdfColor(0, 0, 0), lineWidth));
215+
_lineBlocks.Add(new PdfLineBlock(x1, y1, x2, y2, color ?? new PdfColor(0, 0, 0), lineWidth, dashPattern));
215216
return this;
216217
}
217218

src/MiniPdf/PdfWriter.cs

Lines changed: 106 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,20 @@ private static string BuildContentStream(PdfPage page, bool hasUnicodeFont, Dict
758758
var ly2 = line.Y2.ToString("F3", CultureInfo.InvariantCulture);
759759
sb.Append($"{lr} {lg} {lb} RG\n");
760760
sb.Append($"{lw} w\n");
761+
if (line.DashPattern is { Length: > 0 })
762+
{
763+
sb.Append('[');
764+
for (var di = 0; di < line.DashPattern.Length; di++)
765+
{
766+
if (di > 0) sb.Append(' ');
767+
sb.Append(line.DashPattern[di].ToString("F1", CultureInfo.InvariantCulture));
768+
}
769+
sb.Append("] 0 d\n");
770+
}
771+
else
772+
{
773+
sb.Append("[] 0 d\n");
774+
}
761775
sb.Append($"{lx1} {ly1} m\n");
762776
sb.Append($"{lx2} {ly2} l\n");
763777
sb.Append("S\n");
@@ -1269,7 +1283,21 @@ private static bool IsRgbaPng(byte[] data)
12691283
{
12701284
if (data.Length < 26) return false;
12711285
if (!data.AsSpan(0, 8).SequenceEqual(PngSignature.AsSpan())) return false;
1272-
return data[24] == 8 && data[25] == 6; // 8-bit RGBA
1286+
if (data[24] == 8 && data[25] == 6) return true; // 8-bit RGBA
1287+
// Palette PNG with tRNS chunk also has alpha
1288+
if (data[25] == 3)
1289+
{
1290+
var pos = 8;
1291+
while (pos + 12 <= data.Length)
1292+
{
1293+
var chunkLen = (data[pos] << 24) | (data[pos + 1] << 16) | (data[pos + 2] << 8) | data[pos + 3];
1294+
var chunkType = Encoding.ASCII.GetString(data, pos + 4, 4);
1295+
if (chunkType == "tRNS") return true;
1296+
if (chunkType == "IEND") break;
1297+
pos += 12 + chunkLen;
1298+
}
1299+
}
1300+
return false;
12731301
}
12741302

12751303
private static bool TryDecodePngToRgb(byte[] data, out int width, out int height, out byte[] rgb, out byte[]? alpha)
@@ -1285,26 +1313,46 @@ private static bool TryDecodePngToRgb(byte[] data, out int width, out int height
12851313
height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23];
12861314
var bitDepth = data[24];
12871315
var colorType = data[25];
1288-
// Supported: RGB (2) with 8-bit depth, RGBA (6) with 8-bit depth
1289-
if (bitDepth != 8 || colorType is not (2 or 6))
1290-
return false;
1316+
// Supported: RGB (2) 8-bit, RGBA (6) 8-bit, Indexed (3) 1/2/4/8-bit
1317+
if (colorType == 3)
1318+
{
1319+
if (bitDepth is not (1 or 2 or 4 or 8)) return false;
1320+
}
1321+
else
1322+
{
1323+
if (bitDepth != 8 || colorType is not (2 or 6)) return false;
1324+
}
12911325

1292-
int channels = colorType == 6 ? 4 : 3;
1326+
int channels = colorType == 6 ? 4 : colorType == 2 ? 3 : 1; // palette uses 1 byte per pixel (at 8-bit)
12931327

1294-
// Collect all IDAT chunks and concatenate their compressed data
1328+
// Collect IDAT data and palette/transparency chunks
12951329
using var idatStream = new System.IO.MemoryStream();
1330+
byte[]? palette = null;
1331+
byte[]? trns = null;
12961332
var pos = 8;
12971333
while (pos + 12 <= data.Length)
12981334
{
12991335
var chunkLen = (data[pos] << 24) | (data[pos + 1] << 16) | (data[pos + 2] << 8) | data[pos + 3];
13001336
var chunkType = Encoding.ASCII.GetString(data, pos + 4, 4);
13011337
if (chunkType == "IDAT")
13021338
idatStream.Write(data, pos + 8, chunkLen);
1339+
else if (chunkType == "PLTE")
1340+
{
1341+
palette = new byte[chunkLen];
1342+
Array.Copy(data, pos + 8, palette, 0, chunkLen);
1343+
}
1344+
else if (chunkType == "tRNS")
1345+
{
1346+
trns = new byte[chunkLen];
1347+
Array.Copy(data, pos + 8, trns, 0, chunkLen);
1348+
}
13031349
else if (chunkType == "IEND")
13041350
break;
13051351
pos += 12 + chunkLen;
13061352
}
13071353

1354+
if (colorType == 3 && palette == null) return false; // palette PNG requires PLTE
1355+
13081356
// zlib-compressed data: skip 2-byte zlib header, decompress raw deflate
13091357
var compressed = idatStream.ToArray();
13101358
if (compressed.Length < 3) return false;
@@ -1323,11 +1371,18 @@ private static bool TryDecodePngToRgb(byte[] data, out int width, out int height
13231371
return false;
13241372
}
13251373

1326-
// Apply PNG row filters to get raw RGB data
1327-
var stride = width * channels;
1374+
// Apply PNG row filters to get raw pixel data
1375+
// For palette images, stride is bytes per row of indexed data (may pack multiple pixels per byte)
1376+
int stride;
1377+
if (colorType == 3)
1378+
stride = (width * bitDepth + 7) / 8; // bits per pixel, packed
1379+
else
1380+
stride = width * channels;
13281381
var outputRgb = new byte[width * height * 3];
1329-
byte[]? outputAlpha = channels == 4 ? new byte[width * height] : null;
1382+
byte[]? outputAlpha = (channels == 4 || (colorType == 3 && trns != null)) ? new byte[width * height] : null;
13301383
var prevRow = new byte[stride];
1384+
// For palette filtering, the filter unit is 1 byte (regardless of sub-byte packing)
1385+
var filterUnit = colorType == 3 ? 1 : channels;
13311386

13321387
for (var row = 0; row < height; row++)
13331388
{
@@ -1343,7 +1398,7 @@ private static bool TryDecodePngToRgb(byte[] data, out int width, out int height
13431398
break;
13441399
case 1: // Sub
13451400
for (var x = 0; x < stride; x++)
1346-
cur[x] = (byte)(raw[x] + (x >= channels ? cur[x - channels] : 0));
1401+
cur[x] = (byte)(raw[x] + (x >= filterUnit ? cur[x - filterUnit] : 0));
13471402
break;
13481403
case 2: // Up
13491404
for (var x = 0; x < stride; x++)
@@ -1352,16 +1407,16 @@ private static bool TryDecodePngToRgb(byte[] data, out int width, out int height
13521407
case 3: // Average
13531408
for (var x = 0; x < stride; x++)
13541409
{
1355-
var a = x >= channels ? cur[x - channels] : 0;
1410+
var a = x >= filterUnit ? cur[x - filterUnit] : 0;
13561411
cur[x] = (byte)(raw[x] + (a + prevRow[x]) / 2);
13571412
}
13581413
break;
13591414
case 4: // Paeth
13601415
for (var x = 0; x < stride; x++)
13611416
{
1362-
var a = x >= channels ? cur[x - channels] : 0;
1417+
var a = x >= filterUnit ? cur[x - filterUnit] : 0;
13631418
var b = prevRow[x];
1364-
var c = x >= channels ? prevRow[x - channels] : 0;
1419+
var c = x >= filterUnit ? prevRow[x - filterUnit] : 0;
13651420
cur[x] = (byte)(raw[x] + PaethPredictor(a, b, c));
13661421
}
13671422
break;
@@ -1370,15 +1425,46 @@ private static bool TryDecodePngToRgb(byte[] data, out int width, out int height
13701425
break;
13711426
}
13721427

1373-
// Convert to RGB; extract alpha channel separately if RGBA
1428+
// Convert to RGB; handle palette mapping for indexed color
13741429
var outBase = row * width * 3;
1375-
for (var px = 0; px < width; px++)
1430+
if (colorType == 3)
1431+
{
1432+
// Palette: each pixel is an index into the PLTE table
1433+
for (var px = 0; px < width; px++)
1434+
{
1435+
int idx;
1436+
if (bitDepth == 8)
1437+
idx = cur[px];
1438+
else
1439+
{
1440+
// Sub-byte packing: extract the pixel index from packed bytes
1441+
var pixelsPerByte = 8 / bitDepth;
1442+
var byteIdx = px / pixelsPerByte;
1443+
var bitShift = (pixelsPerByte - 1 - px % pixelsPerByte) * bitDepth;
1444+
var mask = (1 << bitDepth) - 1;
1445+
idx = (cur[byteIdx] >> bitShift) & mask;
1446+
}
1447+
var palBase = idx * 3;
1448+
if (palBase + 2 < palette!.Length)
1449+
{
1450+
outputRgb[outBase + px * 3] = palette[palBase];
1451+
outputRgb[outBase + px * 3 + 1] = palette[palBase + 1];
1452+
outputRgb[outBase + px * 3 + 2] = palette[palBase + 2];
1453+
}
1454+
if (outputAlpha != null)
1455+
outputAlpha[row * width + px] = (trns != null && idx < trns.Length) ? trns[idx] : (byte)255;
1456+
}
1457+
}
1458+
else
13761459
{
1377-
outputRgb[outBase + px * 3] = cur[px * channels];
1378-
outputRgb[outBase + px * 3 + 1] = cur[px * channels + 1];
1379-
outputRgb[outBase + px * 3 + 2] = cur[px * channels + 2];
1380-
if (channels == 4)
1381-
outputAlpha![row * width + px] = cur[px * channels + 3];
1460+
for (var px = 0; px < width; px++)
1461+
{
1462+
outputRgb[outBase + px * 3] = cur[px * channels];
1463+
outputRgb[outBase + px * 3 + 1] = cur[px * channels + 1];
1464+
outputRgb[outBase + px * 3 + 2] = cur[px * channels + 2];
1465+
if (channels == 4)
1466+
outputAlpha![row * width + px] = cur[px * channels + 3];
1467+
}
13821468
}
13831469

13841470
cur.CopyTo(prevRow, 0);

0 commit comments

Comments
 (0)