diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index a566064de..eae38b74a 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -15,6 +15,9 @@ Date Author Change using EPPlus.Export.Pdf.Settings.PdfPageSizes; using OfficeOpenXml; using OfficeOpenXml.Export.PdfExport; +using OfficeOpenXml.Export.PdfExport.Data; +using OfficeOpenXml.Export.PdfExport.Layout; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; using OfficeOpenXml.Export.PdfExport.Settings; using OfficeOpenXml.Style; using System.Diagnostics; @@ -761,5 +764,39 @@ public void EachWorksheetUsesItsOwnPaperSize() } } + [TestMethod] + + public void GetOriginX_CenteringOff_ReturnsContentBoundsLeft() + { + var s = new PdfPageSettings(null); + var p = new Page() + { + FromRow = 1, ToRow = 10, FromColumn = 1, ToColumn = 5, + UsedWidth = 100, + UsedHeight = 100, + RowHeights = new double[10] + }; + + Assert.AreEqual(s.ContentBounds.Left, PdfLayout.GetOriginX(s, p), 0.0001); + } + + [TestMethod] + public void GetOrigin_FlagsAreIndependent() + { + var s = new PdfPageSettings(null); + s.CenterOnPageHorizontally = true; + var p = new Page() + { + FromRow = 1, + ToRow = 10, + FromColumn = 1, + ToColumn = 5, + UsedWidth = s.ContentBounds.Width - 100d, + UsedHeight = s.ContentBounds.Height - 200d, + RowHeights = new double[10] + }; + Assert.AreEqual(s.ContentBounds.Left + 50d, PdfLayout.GetOriginX(s, p), 0.0001); + Assert.AreEqual(s.ContentBounds.Top, PdfLayout.GetOriginY(s, p), 0.0001); + } } } diff --git a/src/EPPlus/Export/PdfExport/Data/PageData.cs b/src/EPPlus/Export/PdfExport/Data/PageData.cs index 458d1bb62..d5a0bc0a5 100644 --- a/src/EPPlus/Export/PdfExport/Data/PageData.cs +++ b/src/EPPlus/Export/PdfExport/Data/PageData.cs @@ -40,6 +40,8 @@ internal struct Page public double[] RowHeights; public double HeadingWidth; public double HeadingHeight; + public double UsedWidth; + public double UsedHeight; } internal struct Pages diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfGridlinesLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfGridlinesLayout.cs index d2f9f50e7..ca4e94313 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfGridlinesLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfGridlinesLayout.cs @@ -50,7 +50,7 @@ public static void AddGridLines(PdfPageSettings pageSettings, Page page, PdfPage // colX[ci] = X of left edge of column ci (0-based within page). // colX[colCount] = X of right edge of last column. var colX = new double[colCount + 1]; - colX[0] = pageSettings.ContentBounds.Left + page.HeadingWidth + page.PrintTitleWidth; + colX[0] = PdfLayout.GetOriginX(pageSettings, page) + page.HeadingWidth + page.PrintTitleWidth; for (int ci = 0; ci < colCount; ci++) { var cell = page.Map[page.FromRow, page.FromColumn + ci]; @@ -61,9 +61,10 @@ public static void AddGridLines(PdfPageSettings pageSettings, Page page, PdfPage // rowY[rowCount] = Y of bottom edge of last row. // Y decreases downward (PDF coordinate system used throughout GetCatalog). var rowY = new double[rowCount + 1]; - rowY[0] = pageSettings.ContentBounds.Top - page.HeadingHeight - page.PrintTitleHeight; + //rowY[0] = pageSettings.ContentBounds.Top - page.HeadingHeight - page.PrintTitleHeight; + rowY[0] = PdfLayout.GetOriginY(pageSettings, page) - page.HeadingHeight - page.PrintTitleHeight; - for (int ri = 0; ri < rowCount; ri++) + for (int ri = 0; ri < rowCount; ri++) { rowY[ri + 1] = rowY[ri] - page.RowHeights[ri]; } @@ -72,9 +73,9 @@ public static void AddGridLines(PdfPageSettings pageSettings, Page page, PdfPage // Always computed so BorderLines is available for margin clipping regardless of // whether ShowGridLines is on. When borderOnly is true we stop here. - double frameLeft = pageSettings.ContentBounds.Left; //colX[0]; + double frameLeft = PdfLayout.GetOriginX(pageSettings, page); double frameRight = colX[colCount]; - double frameTop = pageSettings.ContentBounds.Top; //rowY[0]; + double frameTop = PdfLayout.GetOriginY(pageSettings, page); double frameBottom = rowY[rowCount]; pageLayout.BorderLines.Add(new GridLine(frameLeft, frameTop, frameRight, frameTop)); diff --git a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs index e6fe821c3..854b22c94 100644 --- a/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs +++ b/src/EPPlus/Export/PdfExport/Layout/PdfLayout.cs @@ -93,8 +93,8 @@ internal static Transform GetCatalog(int firstPageNumber, PdfDictionaries dictio pageLayout.PrintTitleWidth = page.PrintTitleWidth; pageLayout.PrintTitleHeight = page.PrintTitleHeight; var drawnMergedCells = new HashSet(); - double contentStartX = pageSettings.ContentBounds.Left + page.HeadingWidth + page.PrintTitleWidth; - double contentStartY = pageSettings.ContentBounds.Top - page.HeadingHeight - page.PrintTitleHeight; + double contentStartX = GetOriginX(pageSettings, page) + page.HeadingWidth + page.PrintTitleWidth; + double contentStartY = GetOriginY(pageSettings, page) - page.HeadingHeight - page.PrintTitleHeight; if (pageSettings.ShowHeadings && !pdfPages[i].IsCommentsPage) { AddHeadingCells(pageSettings, dictionaries, page, pageLayout, contentStartX, contentStartY, page.HeadingWidth, page.HeadingHeight, pdfPages[i].HeadingFontName, pdfPages[i].HeadingFontSize, pdfPages[i].HeadingFill); @@ -463,7 +463,7 @@ private static void AddHeadingCells(PdfPageSettings pageSettings, PdfDictionarie { var headingStyle = new PdfCellStyle(); headingStyle.xfFill = fill; - var cornerFill = new PdfCellLayout(pageSettings.ContentBounds.Left, pageSettings.ContentBounds.Top, headingWidth, headingHeight); + var cornerFill = new PdfCellLayout(GetOriginX(pageSettings, page), GetOriginY(pageSettings, page), headingWidth, headingHeight); SetFill(dictionaries, headingStyle, "", cornerFill); cornerFill.Name = "Heading_Corner"; cornerFill.UpdateShadingPositionMatrix(pageSettings); @@ -475,7 +475,7 @@ private static void AddHeadingCells(PdfPageSettings pageSettings, PdfDictionarie if (colWidth == 0d) { x += colWidth; continue; } string colLetter = ExcelCellBase.GetColumnLetter(col); AddHeadingCell(pageSettings, dictionaries, pageLayout, headingStyle, colLetter, - x, pageSettings.ContentBounds.Top, colWidth, headingHeight, fontName, fontSize, "Heading_Col_" + colLetter); + x, GetOriginY(pageSettings, page), colWidth, headingHeight, fontName, fontSize, "Heading_Col_" + colLetter); x += colWidth; } double y = contentStartY; @@ -485,7 +485,7 @@ private static void AddHeadingCells(PdfPageSettings pageSettings, PdfDictionarie if (rowHeight == 0d) { y -= rowHeight; continue; } string rowNum = row.ToString(); AddHeadingCell(pageSettings, dictionaries, pageLayout, headingStyle, rowNum, - pageSettings.ContentBounds.Left, y, headingWidth, rowHeight, fontName, fontSize, "Heading_Row_" + rowNum); + GetOriginX(pageSettings, page), y, headingWidth, rowHeight, fontName, fontSize, "Heading_Row_" + rowNum); y -= rowHeight; } } @@ -726,7 +726,7 @@ private static Page PrecomputePageMergedCells(PdfPageSettings pageSettings, PdfR } // --- Y --- // Replace the * 15d line with a sum of real row heights - double drawY = pageSettings.ContentBounds.Top - page.HeadingHeight - page.PrintTitleHeight; + double drawY = GetOriginY(pageSettings, page) - page.HeadingHeight - page.PrintTitleHeight; for (int r = page.FromRow; r < row; r++) { drawY -= range.RowHeights[r - range.Range._fromRow].Height; @@ -775,7 +775,7 @@ private static double[] BuildColumnXPositions(PdfPageSettings pageSettings, Page { int colCount = page.ToColumn - page.FromColumn + 1; var colX = new double[colCount]; - double x = pageSettings.ContentBounds.Left + page.HeadingWidth + page.PrintTitleWidth; + double x = GetOriginY(pageSettings, page) + page.HeadingWidth + page.PrintTitleWidth; for (int col = page.FromColumn; col <= page.ToColumn; col++) { colX[col - page.FromColumn] = x; @@ -827,7 +827,7 @@ private static Page PrecomputePagePrintTitleCells(PdfPageSettings pageSettings, // Content-column X: same origin/widths the content loop uses (step-2 origin). var contentColX = new Dictionary(); - double cx = pageSettings.ContentBounds.Left + page.HeadingWidth + page.PrintTitleWidth; + double cx = PdfLayout.GetOriginX(pageSettings, page) + page.HeadingWidth + page.PrintTitleWidth; for (int c = page.FromColumn; c <= page.ToColumn; c++) { contentColX[c] = cx; @@ -835,7 +835,7 @@ private static Page PrecomputePagePrintTitleCells(PdfPageSettings pageSettings, } // Content-row Y. var contentRowY = new Dictionary(); - double cy = pageSettings.ContentBounds.Top - page.HeadingHeight - page.PrintTitleHeight; + double cy = GetOriginY(pageSettings, page) - page.HeadingHeight - page.PrintTitleHeight; for (int r = page.FromRow; r <= page.ToRow; r++) { contentRowY[r] = cy; @@ -845,7 +845,7 @@ private static Page PrecomputePagePrintTitleCells(PdfPageSettings pageSettings, var titleColX = new Dictionary(); if (leftBand) { - double tx = pageSettings.ContentBounds.Left + page.HeadingWidth; + double tx = GetOriginX(pageSettings, page) + page.HeadingWidth; for (int c = pdfSheet.PrintTitleColFrom; c <= pdfSheet.PrintTitleColTo; c++) { titleColX[c] = tx; @@ -856,7 +856,7 @@ private static Page PrecomputePagePrintTitleCells(PdfPageSettings pageSettings, var titleRowY = new Dictionary(); if (topBand) { - double ty = pageSettings.ContentBounds.Top - page.HeadingHeight; + double ty = GetOriginY(pageSettings, page) - page.HeadingHeight; for (int r = pdfSheet.PrintTitleRowFrom; r <= pdfSheet.PrintTitleRowTo; r++) { titleRowY[r] = ty; @@ -899,7 +899,7 @@ private static Page PrecomputePagePrintTitleCells(PdfPageSettings pageSettings, { IsRow = true, Index = r, - X = pageSettings.ContentBounds.Left, + X = GetOriginX(pageSettings, page), Y = titleRowY[r], Width = page.HeadingWidth, Height = h @@ -916,7 +916,7 @@ private static Page PrecomputePagePrintTitleCells(PdfPageSettings pageSettings, IsRow = false, Index = c, X = titleColX[c], - Y = pageSettings.ContentBounds.Top, + Y = GetOriginY(pageSettings, page), Width = w, Height = page.HeadingHeight }); @@ -926,24 +926,24 @@ private static Page PrecomputePagePrintTitleCells(PdfPageSettings pageSettings, // repeated title-row text continues onto the next horizontal page's band if (topBand) AddIncomingSpill(page, range, pdfSheet.PrintTitleRowFrom, pdfSheet.PrintTitleRowTo, page.FromColumn, page.ToColumn, - pageSettings.ContentBounds.Left + page.HeadingWidth + page.PrintTitleWidth, - pageSettings.ContentBounds.Top - page.HeadingHeight, + GetOriginX(pageSettings, page) + page.HeadingWidth + page.PrintTitleWidth, + GetOriginY(pageSettings, page) - page.HeadingHeight, isPrintTitle: true); // left band: a neighbour whose text spills INTO a title column travels with the repeated column if (leftBand) AddIncomingSpill(page, range, page.FromRow, page.ToRow, pdfSheet.PrintTitleColFrom, pdfSheet.PrintTitleColTo, - pageSettings.ContentBounds.Left + page.HeadingWidth, // band origin X (left edge of the title columns) - pageSettings.ContentBounds.Top - page.HeadingHeight - page.PrintTitleHeight, // content-rows origin Y + GetOriginX(pageSettings, page) + page.HeadingWidth, // band origin X (left edge of the title columns) + GetOriginY(pageSettings, page) - page.HeadingHeight - page.PrintTitleHeight, // content-rows origin Y isPrintTitle: true); // corner: same, for the title-rows × title-columns intersection if (topBand && leftBand) AddIncomingSpill(page, range, pdfSheet.PrintTitleRowFrom, pdfSheet.PrintTitleRowTo, pdfSheet.PrintTitleColFrom, pdfSheet.PrintTitleColTo, - pageSettings.ContentBounds.Left + page.HeadingWidth, // band origin X - pageSettings.ContentBounds.Top - page.HeadingHeight, // title-rows origin Y + GetOriginX(pageSettings, page) + page.HeadingWidth, // band origin X + GetOriginY(pageSettings, page) - page.HeadingHeight, // title-rows origin Y isPrintTitle: true); return page; @@ -1420,6 +1420,20 @@ internal static Pages MapPage(PdfRange range, Pages pdfPages) page.Map[row, col] = range.Map[row, col]; } } + double usedWidth = page.HeadingWidth + page.PrintTitleWidth; + for (int col = page.FromColumn; col <= page.ToColumn; col++) + { + usedWidth += page.Map[page.FromRow, col]?.ColumnWidth ?? 0d; + } + page.UsedWidth = usedWidth; + + double usedHeight = page.HeadingHeight + page.PrintTitleHeight; + for (int ri = 0; ri < page.RowHeights.Length; ri++) + { + usedHeight += page.RowHeights[ri]; + } + page.UsedHeight = usedHeight; + pdfPages.Page[i] = page; } pdfPages = pages; @@ -1566,8 +1580,8 @@ internal static Pages PrecomputeSpillCells(PdfPageSettings pageSettings, PdfRang { var page = pdfPages.Page[i]; page.SpillCells = new List(); - double originX = pageSettings.ContentBounds.Left + page.HeadingWidth + page.PrintTitleWidth; - double originY = pageSettings.ContentBounds.Top - page.HeadingHeight - page.PrintTitleHeight; + double originX = GetOriginX(pageSettings, page) + page.HeadingWidth + page.PrintTitleWidth; + double originY = GetOriginY(pageSettings, page) - page.HeadingHeight - page.PrintTitleHeight; AddIncomingSpill(page, range, page.FromRow, page.ToRow, page.FromColumn, page.ToColumn, originX, originY, isPrintTitle: false); pdfPages.Page[i] = page; } @@ -1623,5 +1637,28 @@ private static void EmitBandFrameV(List target, PdfRange range, double } if (rs != null) target.Add(new GridLine(x, rs.Value, x, re)); } + + /// + /// The X coordinate where the page's printed block begins. + /// Currently the left content bound; will include the centering offset. + /// + internal static double GetOriginX(PdfPageSettings pageSettings, Page page) + { + if (!pageSettings.CenterOnPageHorizontally) return pageSettings.ContentBounds.Left; + + var offset = (pageSettings.ContentBounds.Width - page.UsedWidth) / 2d; + return pageSettings.ContentBounds.Left + Math.Max(0d, offset); + } + + /// + /// The Y coordinate where the page's printed block begins (top edge). + /// + internal static double GetOriginY(PdfPageSettings pageSettings, Page page) + { + if (!pageSettings.CenterOnPageVertically) return pageSettings.ContentBounds.Top; + + var offset = (pageSettings.ContentBounds.Height - page.UsedHeight) / 2d; + return pageSettings.ContentBounds.Top - Math.Max(0d, offset); + } } }