diff --git a/src/MiniPdf/ExcelReader.cs b/src/MiniPdf/ExcelReader.cs index bb4e1ea8..fc28e32c 100644 --- a/src/MiniPdf/ExcelReader.cs +++ b/src/MiniPdf/ExcelReader.cs @@ -4214,6 +4214,9 @@ private static List ReadSheetCharts(ZipArchive archive, string w XElement? overlayChartTypeEl = null; var overlayChartType = ""; var overlaySeries = new List(); + var legendPosition = ""; + var chartStyle = 0; + int? gapWidthPercent = null; if (chartEntry != null) { @@ -4221,6 +4224,10 @@ private static List ReadSheetCharts(ZipArchive archive, string w var cDoc = XDocument.Load(cStream); var cns = XNamespace.Get("http://schemas.openxmlformats.org/drawingml/2006/chart"); + int.TryParse(cDoc.Root?.Element(cns + "style")?.Attribute("val")?.Value, out chartStyle); + var legend = cDoc.Descendants(cns + "legend").FirstOrDefault(); + legendPosition = legend?.Element(cns + "legendPos")?.Attribute("val")?.Value ?? ""; + // Extract chart title from var titleEl = cDoc.Descendants(cns + "title").FirstOrDefault(); if (titleEl != null) @@ -4261,6 +4268,10 @@ private static List ReadSheetCharts(ZipArchive archive, string w chartType = groupingVal.Contains("percent", StringComparison.OrdinalIgnoreCase) ? "percentStacked_" + chartType : "stacked_" + chartType; + + if (int.TryParse(chartTypeEl.Element(cns + "gapWidth")?.Attribute("val")?.Value, + out var parsedGapWidth)) + gapWidthPercent = Math.Max(0, parsedGapWidth); } // Extract axis titles @@ -4456,7 +4467,10 @@ private static List ReadSheetCharts(ZipArchive archive, string w showDataLabelVal, dataLabelFmtCode, valAxisFmtCode, isTwoCellAnchor) { OverlaySeries = overlaySeries, - OverlayChartType = overlayChartType + OverlayChartType = overlayChartType, + LegendPosition = legendPosition, + ChartStyle = chartStyle, + GapWidthPercent = gapWidthPercent }; charts.Add(chartInfo); } @@ -4716,6 +4730,12 @@ internal sealed record ExcelChartInfo( public List OverlaySeries { get; init; } = new(); /// Chart type for overlay series (e.g., "lineChart" when primary is "barChart"). public string OverlayChartType { get; init; } = ""; + /// Legend position from chart XML (r, l, t, b, tr); empty when hidden. + public string LegendPosition { get; init; } = ""; + /// Built-in chart style identifier; zero when unspecified. + public int ChartStyle { get; init; } + /// Gap between bar clusters as a percentage of one bar width. + public int? GapWidthPercent { get; init; } }; /// diff --git a/src/MiniPdf/ExcelToPdfConverter.cs b/src/MiniPdf/ExcelToPdfConverter.cs index 4a1f033c..1cb1b142 100644 --- a/src/MiniPdf/ExcelToPdfConverter.cs +++ b/src/MiniPdf/ExcelToPdfConverter.cs @@ -2765,6 +2765,8 @@ float RenderPrintTitleRows() private static void RenderChart(PdfPage page, ExcelChartInfo chart, float x, float top, float width, float height, float baseFontSize) { + var chartFitsPage = x >= 0 && x + width <= page.Width; + var useStyle10Layout = chart.ChartStyle == 10 && chartFitsPage; var titleFontSize = baseFontSize + 2; var labelFontSize = baseFontSize - 1; var axisFontSize = baseFontSize - 2; @@ -2772,8 +2774,17 @@ private static void RenderChart(PdfPage page, ExcelChartInfo chart, var chartColor = chart.ChartTextColor; var chartFont = chart.ChartFontName; + if (useStyle10Layout) + { + var borderColor = new PdfColor(0.82f, 0.82f, 0.82f); + page.AddLine(x, top, x + width, top, borderColor, 0.5f); + page.AddLine(x + width, top, x + width, top - height, borderColor, 0.5f); + page.AddLine(x + width, top - height, x, top - height, borderColor, 0.5f); + page.AddLine(x, top - height, x, top, borderColor, 0.5f); + } + // Draw chart title (clipped to chart width) - var titleY = top; + var titleY = top - (useStyle10Layout ? 12f : 0f); if (!string.IsNullOrEmpty(chart.Title)) { var titleAvailWidth = width - padding * 2; // use nearly full chart width @@ -2782,15 +2793,19 @@ private static void RenderChart(PdfPage page, ExcelChartInfo chart, // Center the title horizontally var titleTextWidth = (float)MeasureHelveticaWidth(clippedTitle, titleFontSize); var titleX = x + (width - titleTextWidth) / 2f; - page.AddText(clippedTitle, titleX, titleY - titleFontSize, titleFontSize, chartColor, preferredFontName: chartFont); + page.AddText(clippedTitle, titleX, titleY - titleFontSize, titleFontSize, chartColor, bold: useStyle10Layout, preferredFontName: chartFont); titleY -= titleFontSize * 2.2f; } // Plot area bounds - var plotLeft = x + padding + 40f; // leave room for Y-axis labels - var plotRight = x + width - padding - 10f; + var showRightLegend = chart.LegendPosition is "r" or "tr" + && chart.Series.Any(series => !string.IsNullOrEmpty(series.Name)) + && chartFitsPage; + var legendWidth = showRightLegend ? Math.Min(32f, width * 0.1f) : 0f; + var plotLeft = x + padding + (useStyle10Layout ? 47f : 40f); + var plotRight = x + width - padding - 10f - legendWidth; var plotTop = titleY - padding; - var plotBottom = top - height + padding + 30f; // leave room for X-axis labels + var plotBottom = top - height + padding + (useStyle10Layout ? 37f : 30f); var plotWidth = plotRight - plotLeft; var plotHeight = plotTop - plotBottom; @@ -2866,6 +2881,20 @@ private static void RenderChart(PdfPage page, ExcelChartInfo chart, { page.AddText(chart.CategoryAxisTitle, plotLeft + plotWidth * 0.35f, plotBottom - 22f, axisFontSize, chartColor, preferredFontName: chartFont); } + + if (showRightLegend) + { + var legendX = plotRight + 10f; + var legendY = plotBottom + plotHeight * 0.5f; + for (var index = 0; index < chart.Series.Count; index++) + { + var seriesName = chart.Series[index].Name; + if (string.IsNullOrEmpty(seriesName)) continue; + var entryY = legendY - index * (labelFontSize + 6f); + page.AddRectangle(legendX, entryY, 7f, 7f, ChartColors[index % ChartColors.Length]); + page.AddText(seriesName, legendX + 11f, entryY - 1f, labelFontSize, chartColor, preferredFontName: chartFont); + } + } } /// Renders a bar/column chart. @@ -2917,11 +2946,11 @@ private static void RenderBarChart(PdfPage page, ExcelChartInfo chart, dataMax = allValues.Max(); var rawMin = allValues.Min(); // Zero-base the axis for typical positive data, but when values are - // tightly clustered far above zero (e.g. stock prices around 150), - // LibreOffice's auto-scale keeps the range close to the data instead - // of compressing it against a distant zero baseline. - dataMin = rawMin > 0 && (dataMax - rawMin) < rawMin * 0.5 - ? rawMin + // tightly clustered far above zero, LibreOffice expands the lower + // bound by half the data range before rounding to a nice interval. + var dataRange = dataMax - rawMin; + dataMin = rawMin > 0 && rawMin / dataMax > 5.0 / 6.0 + ? rawMin - dataRange / 2.0 : Math.Min(0, rawMin); } @@ -2934,8 +2963,11 @@ private static void RenderBarChart(PdfPage page, ExcelChartInfo chart, var numSeries = series.Count; var groupWidth = plotWidth / numCats; - var barWidth = isStacked ? groupWidth * 0.7f : groupWidth * 0.7f / numSeries; - var groupPadding = groupWidth * 0.15f; + var barsPerGroup = isStacked ? 1 : numSeries; + var barWidth = chart.GapWidthPercent.HasValue + ? groupWidth / (barsPerGroup + chart.GapWidthPercent.Value / 100f) + : groupWidth * 0.7f / barsPerGroup; + var groupPadding = (groupWidth - barWidth * barsPerGroup) / 2f; // Y-axis baseline: where value=0 sits, clamped into the visible axis // range. When the axis is auto-scaled away from zero (data clustered @@ -3120,8 +3152,11 @@ private static void RenderHorizontalBarChart(PdfPage page, ExcelChartInfo chart, if (range <= 0) range = 1; var groupHeight = plotHeight / numCats; - var barHeight = isStacked ? groupHeight * 0.7f : groupHeight * 0.7f / numSeries; - var groupPadding = groupHeight * 0.15f; + var barsPerGroup = isStacked ? 1 : numSeries; + var barHeight = chart.GapWidthPercent.HasValue + ? groupHeight / (barsPerGroup + chart.GapWidthPercent.Value / 100f) + : groupHeight * 0.7f / barsPerGroup; + var groupPadding = (groupHeight - barHeight * barsPerGroup) / 2f; var baselineX = plotLeft + (float)((0 - niceMin) / range) * plotWidth; diff --git a/tests/Issue_Files/reference_xlsx/XlsxIssue152_ClusteredNonZeroBarChart.pdf b/tests/Issue_Files/reference_xlsx/XlsxIssue152_ClusteredNonZeroBarChart.pdf new file mode 100644 index 00000000..10ccbc74 Binary files /dev/null and b/tests/Issue_Files/reference_xlsx/XlsxIssue152_ClusteredNonZeroBarChart.pdf differ diff --git a/tests/Issue_Files/reports_xlsx/comparison_manifest.json b/tests/Issue_Files/reports_xlsx/comparison_manifest.json index 931d8bec..2cf2223f 100644 --- a/tests/Issue_Files/reports_xlsx/comparison_manifest.json +++ b/tests/Issue_Files/reports_xlsx/comparison_manifest.json @@ -1,119 +1,124 @@ -{ - "cases": [ - { - "name": "Academic Achievement Summary Table", - "case_id": "Academic Achievement Summary Table", - "format": "xlsx" - }, - { - "name": "AcademicAchievement_temp", - "case_id": "AcademicAchievement_temp", - "format": "xlsx" - }, - { - "name": "Business expense budget1", - "case_id": "Business expense budget1", - "format": "xlsx" - }, - { - "name": "Business expenses budget2", - "case_id": "Business expenses budget2", - "format": "xlsx" - }, - { - "name": "Business plan checklist with SWOT analysis1", - "case_id": "Business plan checklist with SWOT analysis1", - "format": "xlsx" - }, - { - "name": "Event budget1", - "case_id": "Event budget1", - "format": "xlsx" - }, - { - "name": "Expense report basic1", - "case_id": "Expense report basic1", - "format": "xlsx" - }, - { - "name": "Grocery list1", - "case_id": "Grocery list1", - "format": "xlsx" - }, - { - "name": "Issue202609031340", - "case_id": "Issue202609031340", - "format": "xlsx" - }, - { - "name": "payroll-calculator_f", - "case_id": "payroll-calculator_f", - "format": "xlsx" - }, - { - "name": "PO_anonymized", - "case_id": "PO_anonymized", - "format": "xlsx" - }, - { - "name": "Simple invoice1", - "case_id": "Simple invoice1", - "format": "xlsx" - }, - { - "name": "Small business cash flow forecast1", - "case_id": "Small business cash flow forecast1", - "format": "xlsx" - }, - { - "name": "Wedding_timeline_planner1_copy", - "case_id": "Wedding_timeline_planner1_copy", - "format": "xlsx" - }, - { - "name": "Weekly schedule planner1", - "case_id": "Weekly schedule planner1", - "format": "xlsx" - }, - { - "name": "XlsxIssue75", - "case_id": "XlsxIssue75", - "format": "xlsx" - }, - { - "name": "XlsxIssue77_MergedCellAlignment", - "case_id": "XlsxIssue77_MergedCellAlignment", - "format": "xlsx" - }, - { - "name": "XlsxIssue77_Template1", - "case_id": "XlsxIssue77_Template1", - "format": "xlsx" - }, - { - "name": "XlsxIssue77_Template2_Workaround", - "case_id": "XlsxIssue77_Template2_Workaround", - "format": "xlsx" - }, - { - "name": "XlsxIssue81_LayoutOptions", - "case_id": "XlsxIssue81_LayoutOptions", - "format": "xlsx" - }, - { - "name": "XlsxIssue82_5mb", - "case_id": "XlsxIssue82_5mb", - "format": "xlsx" - }, - { - "name": "XlsxIssue82_SampleTestData5mb", - "case_id": "XlsxIssue82_SampleTestData5mb", - "format": "xlsx" - }, - { - "name": "XlsxIssue82_WideTable", - "case_id": "XlsxIssue82_WideTable", - "format": "xlsx" - } - ] +{ + "cases": [ + { + "name": "Academic Achievement Summary Table", + "case_id": "Academic Achievement Summary Table", + "format": "xlsx" + }, + { + "name": "AcademicAchievement_temp", + "case_id": "AcademicAchievement_temp", + "format": "xlsx" + }, + { + "name": "Business expense budget1", + "case_id": "Business expense budget1", + "format": "xlsx" + }, + { + "name": "Business expenses budget2", + "case_id": "Business expenses budget2", + "format": "xlsx" + }, + { + "name": "Business plan checklist with SWOT analysis1", + "case_id": "Business plan checklist with SWOT analysis1", + "format": "xlsx" + }, + { + "name": "Event budget1", + "case_id": "Event budget1", + "format": "xlsx" + }, + { + "name": "Expense report basic1", + "case_id": "Expense report basic1", + "format": "xlsx" + }, + { + "name": "Grocery list1", + "case_id": "Grocery list1", + "format": "xlsx" + }, + { + "name": "Issue202609031340", + "case_id": "Issue202609031340", + "format": "xlsx" + }, + { + "name": "payroll-calculator_f", + "case_id": "payroll-calculator_f", + "format": "xlsx" + }, + { + "name": "PO_anonymized", + "case_id": "PO_anonymized", + "format": "xlsx" + }, + { + "name": "Simple invoice1", + "case_id": "Simple invoice1", + "format": "xlsx" + }, + { + "name": "Small business cash flow forecast1", + "case_id": "Small business cash flow forecast1", + "format": "xlsx" + }, + { + "name": "Wedding_timeline_planner1_copy", + "case_id": "Wedding_timeline_planner1_copy", + "format": "xlsx" + }, + { + "name": "Weekly schedule planner1", + "case_id": "Weekly schedule planner1", + "format": "xlsx" + }, + { + "name": "XlsxIssue152_ClusteredNonZeroBarChart", + "case_id": "XlsxIssue152_ClusteredNonZeroBarChart", + "format": "xlsx" + }, + { + "name": "XlsxIssue75", + "case_id": "XlsxIssue75", + "format": "xlsx" + }, + { + "name": "XlsxIssue77_MergedCellAlignment", + "case_id": "XlsxIssue77_MergedCellAlignment", + "format": "xlsx" + }, + { + "name": "XlsxIssue77_Template1", + "case_id": "XlsxIssue77_Template1", + "format": "xlsx" + }, + { + "name": "XlsxIssue77_Template2_Workaround", + "case_id": "XlsxIssue77_Template2_Workaround", + "format": "xlsx" + }, + { + "name": "XlsxIssue81_LayoutOptions", + "case_id": "XlsxIssue81_LayoutOptions", + "format": "xlsx" + }, + { + "name": "XlsxIssue82_5mb", + "case_id": "XlsxIssue82_5mb", + "format": "xlsx" + }, + { + "name": "XlsxIssue82_SampleTestData5mb", + "case_id": "XlsxIssue82_SampleTestData5mb", + "format": "xlsx" + }, + { + "name": "XlsxIssue82_WideTable", + "case_id": "XlsxIssue82_WideTable", + "format": "xlsx" + } + ] } diff --git a/tests/Issue_Files/reports_xlsx/comparison_report.json b/tests/Issue_Files/reports_xlsx/comparison_report.json index c6b415ea..dfb1fba6 100644 --- a/tests/Issue_Files/reports_xlsx/comparison_report.json +++ b/tests/Issue_Files/reports_xlsx/comparison_report.json @@ -972,5 +972,53 @@ } ], "overall_score": 0.9341 + }, + { + "name": "XlsxIssue152_ClusteredNonZeroBarChart", + "minipdf_exists": true, + "reference_exists": true, + "report_scope": "issue-xlsx", + "case_id": "XlsxIssue152_ClusteredNonZeroBarChart", + "format": "xlsx", + "minipdf_size": 4416, + "reference_size": 51953, + "pdf_valid": true, + "minipdf_pages": 1, + "reference_pages": 1, + "text_similarity": 0.9771, + "flat_text_similarity": 0.9771, + "word_text_similarity": 0.9762, + "text_diff": "--- minipdf/XlsxIssue152_ClusteredNonZeroBarChart.pdf\n+++ reference/XlsxIssue152_ClusteredNonZeroBarChart.pdf\n@@ -8,8 +8,7 @@\n 160\n\n 155\n\n 150\n\n-Price\n\n-145\n\n+145 Price\n\n Price\n\n 140\n\n 135\n", + "visual_scores": [ + 0.9652 + ], + "visual_avg": 0.9652, + "diff_images": [ + { + "page": 1, + "minipdf_img": "XlsxIssue152_ClusteredNonZeroBarChart_p1_minipdf.png", + "reference_img": "XlsxIssue152_ClusteredNonZeroBarChart_p1_reference.png", + "heatmap_img": "XlsxIssue152_ClusteredNonZeroBarChart_p1_heatmap.png", + "heatmap_metrics": { + "size_px": [ + 1241, + 1650 + ], + "difference_bbox_px": [ + 109, + 141, + 881, + 782 + ], + "changed_pixels": 82552, + "changed_fraction": 0.040315, + "mean_abs_rgb": 4.6913, + "rmse_rgb": 27.7385, + "threshold": 12, + "gain": 5.0 + } + } + ], + "overall_score": 0.9769 } ] \ No newline at end of file diff --git a/tests/Issue_Files/reports_xlsx/comparison_report.md b/tests/Issue_Files/reports_xlsx/comparison_report.md index 4b56da7d..3d7587c0 100644 --- a/tests/Issue_Files/reports_xlsx/comparison_report.md +++ b/tests/Issue_Files/reports_xlsx/comparison_report.md @@ -1,6 +1,6 @@ -# MiniPdf vs Microsoft 365 Reference PDF Comparison Report +# MiniPdf vs LibreOffice Reference PDF Comparison Report -Generated: 2026-09-04T20:39:22.365558 +Generated: 2026-09-08T10:15:48.738138 ## Summary @@ -29,8 +29,9 @@ Generated: 2026-09-04T20:39:22.365558 | 21 | 🔴 XlsxIssue82_SampleTestData5mb | ✅ | 0.4255 | 0.9146 | 834/1668 | **0.636** | | 22 | 🟢 XlsxIssue82_WideTable | ✅ | 1.0 | 0.8648 | 13/13 | **0.9459** | | 23 | 🟢 Issue202609031340 | ✅ | 0.8828 | 0.9524 | 4/4 | **0.9341** | +| 24 | 🟢 XlsxIssue152_ClusteredNonZeroBarChart | ✅ | 0.9771 | 0.9652 | 1/1 | **0.9769** | -**Average Overall Score: 0.8401** +**Average Overall Score: 0.8458** ## Difference Heatmaps @@ -58,23 +59,28 @@ Blue areas are below the configured difference threshold; red areas have stronge Issue202609031340 page 4 difference heatmap changed: 18149 px (0.83%)
bbox: [42, 743, 1196, 1009]
mean abs RGB: 1.166
RMSE RGB: 14.6026
threshold: 12, gain: 5.0 + + XlsxIssue152_ClusteredNonZeroBarChart
Page 1 + XlsxIssue152_ClusteredNonZeroBarChart page 1 difference heatmap + changed: 82552 px (4.03%)
bbox: [109, 141, 881, 782]
mean abs RGB: 4.6913
RMSE RGB: 27.7385
threshold: 12, gain: 5.0 + ## Visual Comparison - + - + - + @@ -82,11 +88,11 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + @@ -94,15 +100,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -110,19 +116,19 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + - + @@ -130,7 +136,7 @@ Blue areas are below the configured difference threshold; red areas have stronge - + @@ -138,15 +144,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -154,7 +160,7 @@ Blue areas are below the configured difference threshold; red areas have stronge - + @@ -162,7 +168,7 @@ Blue areas are below the configured difference threshold; red areas have stronge - + @@ -170,15 +176,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -186,15 +192,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -202,7 +208,7 @@ Blue areas are below the configured difference threshold; red areas have stronge - + @@ -210,15 +216,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -226,15 +232,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -242,7 +248,7 @@ Blue areas are below the configured difference threshold; red areas have stronge - + @@ -250,15 +256,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -266,11 +272,11 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + @@ -278,15 +284,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -294,15 +300,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -310,15 +316,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -326,15 +332,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -342,15 +348,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -358,15 +364,15 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + @@ -374,19 +380,27 @@ Blue areas are below the configured difference threshold; red areas have stronge - + - + - + - + + + + + + + + +
MiniPdfMicrosoft 365 Reference
MiniPdfLibreOffice Reference
Academic Achievement Summary Table
format: xlsx | case: Academic Achievement Summary Table | scope: issue-xlsx
Academic Achievement Summary Table ⬤ 97.4%
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
AcademicAchievement_temp
format: xlsx | case: AcademicAchievement_temp | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
Business expense budget1
format: xlsx | case: Business expense budget1 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
Business expenses budget2
format: xlsx | case: Business expenses budget2 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
Business plan checklist with SWOT analysis1
format: xlsx | case: Business plan checklist with SWOT analysis1 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
Event budget1
format: xlsx | case: Event budget1 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
Expense report basic1
format: xlsx | case: Expense report basic1 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
Grocery list1
format: xlsx | case: Grocery list1 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
payroll-calculator_f
format: xlsx | case: payroll-calculator_f | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
PO_anonymized
format: xlsx | case: PO_anonymized | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
Simple invoice1
format: xlsx | case: Simple invoice1 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
Small business cash flow forecast1
format: xlsx | case: Small business cash flow forecast1 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
missingMicrosoft 365 ReferenceLibreOffice Reference
Wedding_timeline_planner1_copy
format: xlsx | case: Wedding_timeline_planner1_copy | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
Weekly schedule planner1
format: xlsx | case: Weekly schedule planner1 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
XlsxIssue75
format: xlsx | case: XlsxIssue75 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
XlsxIssue77_MergedCellAlignment
format: xlsx | case: XlsxIssue77_MergedCellAlignment | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
XlsxIssue77_Template1
format: xlsx | case: XlsxIssue77_Template1 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
XlsxIssue77_Template2_Workaround
format: xlsx | case: XlsxIssue77_Template2_Workaround | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
XlsxIssue81_LayoutOptions
format: xlsx | case: XlsxIssue81_LayoutOptions | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
XlsxIssue82_5mb
format: xlsx | case: XlsxIssue82_5mb | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
XlsxIssue82_SampleTestData5mb
format: xlsx | case: XlsxIssue82_SampleTestData5mb | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
XlsxIssue82_WideTable
format: xlsx | case: XlsxIssue82_WideTable | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
Issue202609031340
format: xlsx | case: Issue202609031340 | scope: issue-xlsx
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
MiniPdfMicrosoft 365 ReferenceLibreOffice Reference
XlsxIssue152_ClusteredNonZeroBarChart
format: xlsx | case: XlsxIssue152_ClusteredNonZeroBarChart | scope: issue-xlsx
XlsxIssue152_ClusteredNonZeroBarChart ⬤ 97.7%
MiniPdfLibreOffice Reference
@@ -2642,6 +2656,41 @@ Text content: ✅ Identical ``` +### XlsxIssue152_ClusteredNonZeroBarChart + +- **Case Metadata:** format: xlsx | case: XlsxIssue152_ClusteredNonZeroBarChart | scope: issue-xlsx +- **Text Similarity:** 0.9771 +- **Visual Average:** 0.9652 +- **Overall Score:** 0.9769 +- **Pages:** MiniPdf=1, Reference=1 +- **File Size:** MiniPdf=4416 bytes, Reference=51953 bytes + +
Text Diff + +```diff +--- minipdf/XlsxIssue152_ClusteredNonZeroBarChart.pdf ++++ reference/XlsxIssue152_ClusteredNonZeroBarChart.pdf +@@ -8,8 +8,7 @@ + 160 + + 155 + + 150 + +-Price + +-145 + ++145 Price + + Price + + 140 + + 135 +``` +
+ ## Improvement Suggestions ### ⚠ Low-Score Test Cases (below 0.8) diff --git a/tests/Issue_Files/reports_xlsx/images/XlsxIssue152_ClusteredNonZeroBarChart_p1_heatmap.png b/tests/Issue_Files/reports_xlsx/images/XlsxIssue152_ClusteredNonZeroBarChart_p1_heatmap.png new file mode 100644 index 00000000..32e8a51f Binary files /dev/null and b/tests/Issue_Files/reports_xlsx/images/XlsxIssue152_ClusteredNonZeroBarChart_p1_heatmap.png differ diff --git a/tests/Issue_Files/reports_xlsx/images/XlsxIssue152_ClusteredNonZeroBarChart_p1_minipdf.png b/tests/Issue_Files/reports_xlsx/images/XlsxIssue152_ClusteredNonZeroBarChart_p1_minipdf.png new file mode 100644 index 00000000..bf8e8b70 Binary files /dev/null and b/tests/Issue_Files/reports_xlsx/images/XlsxIssue152_ClusteredNonZeroBarChart_p1_minipdf.png differ diff --git a/tests/Issue_Files/reports_xlsx/images/XlsxIssue152_ClusteredNonZeroBarChart_p1_reference.png b/tests/Issue_Files/reports_xlsx/images/XlsxIssue152_ClusteredNonZeroBarChart_p1_reference.png new file mode 100644 index 00000000..02ceff34 Binary files /dev/null and b/tests/Issue_Files/reports_xlsx/images/XlsxIssue152_ClusteredNonZeroBarChart_p1_reference.png differ diff --git a/tests/Issue_Files/xlsx/XlsxIssue152_ClusteredNonZeroBarChart.xlsx b/tests/Issue_Files/xlsx/XlsxIssue152_ClusteredNonZeroBarChart.xlsx new file mode 100644 index 00000000..07048fac Binary files /dev/null and b/tests/Issue_Files/xlsx/XlsxIssue152_ClusteredNonZeroBarChart.xlsx differ diff --git a/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs b/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs index f7fdc5e5..728ce9c0 100644 --- a/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs +++ b/tests/MiniPdf.Tests/ExcelToPdfConverterTests.cs @@ -81,7 +81,9 @@ public void Convert_BarChartWithClusteredNonZeroData_KeepsBarsAndAxisWithinPlot( Math.Abs(line.X1 - line.X2) < 0.01f && line.Color == PdfColor.Black); var plotBottom = Math.Min(yAxis.Y1, yAxis.Y2); var plotTop = Math.Max(yAxis.Y1, yAxis.Y2); - var bars = page.RectBlocks.ToArray(); + var bars = page.RectBlocks.Where(rectangle => rectangle.Height > 7f) + .OrderBy(rectangle => rectangle.X) + .ToArray(); Assert.Equal(5, bars.Length); Assert.All(bars, bar => @@ -89,9 +91,15 @@ public void Convert_BarChartWithClusteredNonZeroData_KeepsBarsAndAxisWithinPlot( Assert.Equal(plotBottom, bar.Y, 3); Assert.InRange(bar.Y + bar.Height, plotBottom, plotTop + 0.01f); }); + var firstGap = bars[1].X - bars[0].X - bars[0].Width; + Assert.True(firstGap > bars[0].Width, "A 150% gapWidth should leave more gap than bar width."); var axisLabels = page.TextBlocks.Select(block => block.Text); Assert.DoesNotContain("0", axisLabels); + Assert.Contains("130", axisLabels); + Assert.Contains("160", axisLabels); + Assert.Contains("Price", axisLabels); + Assert.Contains(page.TextBlocks, block => block.Text == "Clustered Non-Zero Prices" && block.Bold); } [Fact] @@ -1299,7 +1307,9 @@ private static MemoryStream CreateExcelWithBarChart(string[] categories, double[ + + Clustered Non-Zero Prices @@ -1307,6 +1317,7 @@ private static MemoryStream CreateExcelWithBarChart(string[] categories, double[ + Price Sheet1!$A$2:$A${{categories.Length + 1}} @@ -1320,8 +1331,10 @@ private static MemoryStream CreateExcelWithBarChart(string[] categories, double[ + + """);