From 5dc0e7b29a7d7d5502432bf9a1fbb1e6626327c5 Mon Sep 17 00:00:00 2001 From: Garry Xiao Date: Thu, 25 Apr 2024 13:29:03 +1200 Subject: [PATCH] Stable layout ready --- ConsoleApp1/Program.cs | 25 +- com.etsoo.EasyPdf/Content/PdfBlock.cs | 109 ++++--- com.etsoo.EasyPdf/Content/PdfBlockLine.cs | 14 + com.etsoo.EasyPdf/Content/PdfChunk.cs | 14 - com.etsoo.EasyPdf/Content/PdfDiv.cs | 41 ++- com.etsoo.EasyPdf/Content/PdfHR.cs | 37 ++- com.etsoo.EasyPdf/Content/PdfImage.cs | 36 ++- com.etsoo.EasyPdf/Content/PdfOperator.cs | 60 +++- com.etsoo.EasyPdf/Content/PdfStyle.cs | 50 ++-- com.etsoo.EasyPdf/Content/PdfStyleProperty.cs | 29 +- com.etsoo.EasyPdf/Content/PdfTextChunk.cs | 11 +- com.etsoo.EasyPdf/Objects/PdfPage.cs | 275 ++++++++++++++---- com.etsoo.EasyPdf/PdfExtensions.cs | 33 +++ com.etsoo.EasyPdf/PdfWriter.cs | 2 + 14 files changed, 529 insertions(+), 207 deletions(-) diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs index ad0e51a..fc2e036 100644 --- a/ConsoleApp1/Program.cs +++ b/ConsoleApp1/Program.cs @@ -23,7 +23,7 @@ // Global styles pdf.Style.FontSize = 12; -pdf.Style.Border = new PdfStyleBorder(PdfColor.Red, 8); +pdf.Style.Border = new PdfStyleBorder(PdfColor.Red, 6); // Fonts //await pdf.Fonts.LoadAsync("C:\\Windows\\Fonts\\simsun.ttc"); @@ -41,8 +41,9 @@ // Paragraphs var p1 = new PdfDiv(); var img = await PdfImage.LoadAsync("D:\\etsoo.png"); -img.Style.SetHeight(40).SetOpacity(0.3f); +img.Style.SetHeight(40).SetOpacity(0.9f); p1.Add(img); +//p1.Style.SetBorder(PdfColor.Red).SetBackgroundColor("#f3f3f3").SetPadding(6); await w.WriteAsync(p1); var h1 = new PdfHeading(PdfHeading.Level.H1); @@ -54,6 +55,13 @@ h1.Add(new PdfSuperscript("®")); await w.WriteAsync(h1); +var hr = new PdfHR() +{ + Color = PdfColor.Red +}; +hr.Style.SetOpacity(0.5f); +await w.WriteAsync(hr); + var p2 = new PdfParagraph(); p2.Add("青岛亿速思维\n网络科技有限公司 粗体").Style.SetFontStyle(PdfFontStyle.Bold); p2.Add(PdfLineBreak.New); @@ -62,22 +70,19 @@ p2.Add("上海亿商网络科技有限公司 粗体 & 斜体").Style.SetFontStyle(PdfFontStyle.BoldItalic); await w.WriteAsync(p2); -var hr = new PdfHR(); -await w.WriteAsync(hr); - var p5 = new PdfParagraph(); p5.Style.SetFontSize(10) .SetTextAlign(PdfTextAlign.Justify) - .SetLineHeight(16) - .SetBorder(PdfColor.Blue, 1) - .SetBackgroundColor("#f3f3f3") - .SetPadding(0); + .SetLineHeight(15) + .SetBorder(PdfColor.Blue, 0.75f, PdfStyleBorderStyle.Dotted) + .SetBackgroundColor("#f6f6f6") + .SetPadding(6); p5.Add("亿速思维(ETSOO)自成立以来致力于自主研发,在过去的20年中一直秉持着对技术的不懈追求和创新精神,为中小企业提供高效的信息化管理解决方案。公司的使命在于为客户创造价值,通过持续创新和卓越的服务,助力企业实现数字化转型。ETSOO has been dedicated to independent research and development since its establishment, maintaining an unwavering pursuit of technology and spirit of innovation over the past 20 years. We specialize in providing efficient information management solutions for small and medium-sized enterprises. Our mission is to create value for our customers by facilitating digital transformation through continuous innovation and excellent service."); await w.WriteAsync(p5); var p6 = new PdfParagraph(); p6.Style.SetPosition(PdfPosition.Absolute) - .SetTop(160) + .SetTop(200) .SetFontSize(36) .SetColor(new PdfColor(255, 0, 0)) .SetOpacity(0.1f) diff --git a/com.etsoo.EasyPdf/Content/PdfBlock.cs b/com.etsoo.EasyPdf/Content/PdfBlock.cs index 495b946..3449713 100644 --- a/com.etsoo.EasyPdf/Content/PdfBlock.cs +++ b/com.etsoo.EasyPdf/Content/PdfBlock.cs @@ -17,6 +17,12 @@ public abstract class PdfBlock : IPdfElement /// public Vector2 BasePoint { get; set; } + /// + /// Start point + /// 开始点 + /// + public Vector2 StartPoint; + /// /// Current line /// 当前行 @@ -38,6 +44,12 @@ public abstract class PdfBlock : IPdfElement private RectangleF layout; private bool hasBorder; + /// + /// Bottom adjust + /// 底部调整 + /// + protected float BottomAdjust { get; private set; } + /// /// Set parent style /// 设置父样式 @@ -57,38 +69,53 @@ public virtual void SetParentStyle(PdfStyle parentStyle, float fontSize) /// Current writer /// Current style /// Current rectangle + /// Current point /// Current completed line /// New line /// - protected virtual async Task NewLineActionAsync(IPdfPage page, PdfWriter writer, PdfStyle style, RectangleF rect, PdfBlockLine line, PdfBlockLine? newLine) + protected virtual async Task NewLineActionAsync(IPdfPage page, PdfWriter writer, PdfStyle style, RectangleF rect, PdfPoint point, PdfBlockLine line, PdfBlockLine? newLine) { var bgcolor = style.BackgroundColor; - if (line.Chunks.Length > 0 && (bgcolor.HasValue || hasBorder)) + if (bgcolor.HasValue) { + var adjust = PdfTextChunk.LineHeightAdjust + 1; + // Start point - var startPoint = line.Chunks.First().StartPoint; + var startPoint = line.Chunks.FirstOrDefault()?.StartPoint ?? StartPoint; var x = layout.X; var width = layout.Width; + var lineHeight = line.Height; float y, height; if (line.First) { // First line - height = startPoint.Y - layout.Y + line.Height; + height = startPoint.Y - layout.Y + lineHeight; + + if (newLine == null) + { + // Also the last line + height += BottomAdjust; + } + else + { + height += adjust; + } + y = layout.Y + height; } else if (newLine == null) { // Last line - height = line.Height + style.Padding?.Bottom ?? 0; - y = startPoint.Y + height; + height = lineHeight + BottomAdjust; + y = startPoint.Y + adjust + height; } else { // Other lines - height = line.Height; - y = startPoint.Y + height; + height = lineHeight; + y = startPoint.Y + adjust + height; } // Line rectangle @@ -96,32 +123,28 @@ protected virtual async Task NewLineActionAsync(IPdfPage page, PdfWriter writer, var lineRect = new RectangleF(gPoint.X, gPoint.Y, width, height); // Draw background color - if (bgcolor.HasValue) - { - //await page.WriteBackgroundAsync(bgcolor.Value, lineRect); - } + await page.WriteBackgroundAsync(bgcolor.Value, lineRect); + } - // Draw border - if (hasBorder) - { - var border = style.Border!.DeepClone(); + // Border + if (hasBorder && newLine == null) + { + var adjust = PdfTextChunk.LineHeightAdjust + 1; - if (line.Index == 0) - { - border.Bottom.Width = 0; - } - else if (newLine == null) - { - border.Top.Width = 0; - } - else - { - border.Bottom.Width = 0; - border.Top.Width = 0; - } + // Start point + var startPoint = line.Chunks.FirstOrDefault()?.StartPoint ?? StartPoint; - await page.WriteBorderAsync(border, lineRect); - } + var width = layout.Width; + var height = startPoint.Y - layout.Y + line.Height + BottomAdjust + adjust; + var x = layout.X; + var y = layout.Y + height; + + // Line rectangle + var gPoint = page.CalculatePoint(new Vector2(x, y)); + var lineRect = new RectangleF(gPoint.X, gPoint.Y, width, height); + + // Draw border + await page.WriteBorderAsync(style.Border!, lineRect); } // Update current line reference @@ -143,6 +166,9 @@ public virtual async ValueTask WriteAsync(IPdfPage page, PdfWriter writer) if (Rendered) throw new InvalidOperationException("The block has been rendered."); + // Back to most left + page.CurrentPoint.X = 0; + // Save graphics state await page.SaveStateAsync(); @@ -158,6 +184,8 @@ public virtual async ValueTask WriteAsync(IPdfPage page, PdfWriter writer) // Rectangle var (layout, rect) = style.GetRectangle(page.ContentRect.Size, page.CurrentPoint); this.layout = layout; + + BottomAdjust = layout.Bottom - rect.Bottom; hasBorder = style.Border?.HasBorder ?? false; // Rotate @@ -187,14 +215,20 @@ public virtual async ValueTask WriteAsync(IPdfPage page, PdfWriter writer) Y = rect.Y } : page.CurrentPoint; + // Start point + StartPoint = point.ToVector2(); + // Write - await WriteInnerAsync(page, writer, style, rect, point); + var completed = await WriteInnerAsync(page, writer, style, rect, point); // Store graphics state await page.RestoreStateAsync(); // Adjust - AdjustBottom(point, style); + if (completed) + { + AdjustBottom(point, style); + } // Update render status Rendered = true; @@ -208,9 +242,8 @@ public virtual async ValueTask WriteAsync(IPdfPage page, PdfWriter writer) /// Style protected virtual void AdjustBottom(PdfPoint point, PdfStyle style) { - var paddingBottom = style.Padding?.Bottom ?? 0; - var marginBottom = style.Margin?.Bottom ?? 0 + style.Border?.Bottom.Width ?? 0; - point.Y += paddingBottom + marginBottom; + var marginBottom = style.Margin?.Bottom ?? 0; + point.Y += BottomAdjust + marginBottom; } /// @@ -222,7 +255,7 @@ protected virtual void AdjustBottom(PdfPoint point, PdfStyle style) /// Current style /// Current rectangle /// Current point - /// Task - protected abstract ValueTask WriteInnerAsync(IPdfPage page, PdfWriter writer, PdfStyle style, RectangleF rect, PdfPoint point); + /// Completed or not + protected abstract ValueTask WriteInnerAsync(IPdfPage page, PdfWriter writer, PdfStyle style, RectangleF rect, PdfPoint point); } } diff --git a/com.etsoo.EasyPdf/Content/PdfBlockLine.cs b/com.etsoo.EasyPdf/Content/PdfBlockLine.cs index 09f3fb6..116b302 100644 --- a/com.etsoo.EasyPdf/Content/PdfBlockLine.cs +++ b/com.etsoo.EasyPdf/Content/PdfBlockLine.cs @@ -279,6 +279,12 @@ public record PdfBlockLine /// public PdfBlockLineChunk[] Chunks => [.. chunks]; + /// + /// Has text + /// 是否有文本 + /// + public bool HasText => chunks.Any(c => c.Chars.Count > 0); + internal PdfBlockLine(int index) { Index = index; @@ -306,6 +312,14 @@ public void AddChunk(PdfBlockLineChunk chunk) MaxFontHeight = fontHeight; } } + else + { + // No font, like picture + if (chunk.Height > MaxFontHeight) + { + MaxFontHeight = chunk.Height; + } + } } } } \ No newline at end of file diff --git a/com.etsoo.EasyPdf/Content/PdfChunk.cs b/com.etsoo.EasyPdf/Content/PdfChunk.cs index dc7fe83..daa017f 100644 --- a/com.etsoo.EasyPdf/Content/PdfChunk.cs +++ b/com.etsoo.EasyPdf/Content/PdfChunk.cs @@ -105,26 +105,12 @@ public virtual async Task WriteAsync(PdfWriter writer, RectangleF rect, Pd await writer.DefineOpacityAsync(opacity, true); } - // Margin left - var marginLeft = style.Margin?.Left ?? 0; - - // Margin right - var marginRight = style.Margin?.Right ?? 0; - // Start point StartPoint = point.ToVector2(); - // Margin left - point.X += marginLeft; - line.Width += marginLeft; - // Inner rendering var newPage = await WriteInnerAsync(writer, style, rect, point, line, newLineAction); - // Margin right - point.X += marginRight; - line.Width += marginRight; - // End point EndPoint = point.ToVector2(); diff --git a/com.etsoo.EasyPdf/Content/PdfDiv.cs b/com.etsoo.EasyPdf/Content/PdfDiv.cs index 7985f1d..4eb175d 100644 --- a/com.etsoo.EasyPdf/Content/PdfDiv.cs +++ b/com.etsoo.EasyPdf/Content/PdfDiv.cs @@ -51,28 +51,24 @@ public PdfTextChunk Add(ReadOnlySpan content) return chunk; } - protected override async Task NewLineActionAsync(IPdfPage page, PdfWriter writer, PdfStyle style, RectangleF rect, PdfBlockLine line, PdfBlockLine? newLine) + protected override async Task NewLineActionAsync(IPdfPage page, PdfWriter writer, PdfStyle style, RectangleF rect, PdfPoint point, PdfBlockLine line, PdfBlockLine? newLine) { - // Output previous line - await WriteLineAsync(line, page, writer, rect.Width, style, newLine); + await base.NewLineActionAsync(page, writer, style, rect, point, line, newLine); - await base.NewLineActionAsync(page, writer, style, rect, line, newLine); + // Output previous line + await WriteLineAsync(line, page, writer, point, rect.Width, style, newLine); } - protected override async ValueTask WriteInnerAsync(IPdfPage page, PdfWriter writer, PdfStyle style, RectangleF rect, PdfPoint point) + protected override async ValueTask WriteInnerAsync(IPdfPage page, PdfWriter writer, PdfStyle style, RectangleF rect, PdfPoint point) { var len = chunks.Count; if (len == 0) { - return; + return true; } - // Margin & padding - var marginBottom = style.Margin?.Bottom ?? 0; - var paddingBottom = style.Padding?.Bottom ?? 0; - // New line action - Task action(PdfBlockLine line, PdfBlockLine? newLine) => NewLineActionAsync(page, writer, style, rect, line, newLine); + Task action(PdfBlockLine line, PdfBlockLine? newLine) => NewLineActionAsync(page, writer, style, rect, point, line, newLine); var c = 0; PdfChunk chunk = chunks[0]; @@ -80,9 +76,9 @@ protected override async ValueTask WriteInnerAsync(IPdfPage page, PdfWriter writ { chunk = chunks[c]; - var completed = await chunk.WriteAsync(writer, rect, point, CurrentLine, action); + var newPage = await chunk.WriteAsync(writer, rect, point, CurrentLine, action); - if (completed) + if (newPage) { break; } @@ -94,7 +90,7 @@ protected override async ValueTask WriteInnerAsync(IPdfPage page, PdfWriter writ // Move to the beginning of the next line point.X = rect.X; - point.Y += CurrentLine.Height; + point.Y += CurrentLine.Height + (CurrentLine.Height - CurrentLine.MaxFontHeight); } if (c < len) @@ -112,20 +108,16 @@ protected override async ValueTask WriteInnerAsync(IPdfPage page, PdfWriter writ await writer.NewPageAsync(); await writer.WriteAsync(newBlock); + + return false; } else { - // Move the margin bottom - base.AdjustBottom(point, style); + return true; } } - protected override void AdjustBottom(PdfPoint point, PdfStyle style) - { - // Ignore as done previously - } - - protected async Task WriteLineAsync(PdfBlockLine line, IPdfPage page, PdfWriter writer, float width, PdfStyle style, PdfBlockLine? newLine) + protected async Task WriteLineAsync(PdfBlockLine line, IPdfPage page, PdfWriter writer, PdfPoint point, float width, PdfStyle style, PdfBlockLine? newLine) { var chunkCount = line.Chunks.Length; if (chunkCount == 0) @@ -180,7 +172,7 @@ protected async Task WriteLineAsync(PdfBlockLine line, IPdfPage page, PdfWriter // Line height var lineHeight = line.Height; var firstHeight = line.Chunks[0].Font?.LineHeight ?? line.Chunks[0].Height; - var firstLineAdjust = lineHeight - line.MaxFontHeight; + var firstLineAdjust = lineHeight > line.MaxFontHeight ? lineHeight - line.MaxFontHeight - PdfTextChunk.LineHeightAdjust : 0; var lineActionDone = false; var chunkIndex = 0; @@ -220,7 +212,8 @@ protected async Task WriteLineAsync(PdfBlockLine line, IPdfPage page, PdfWriter newChunk.AdjustStartPoint(0, -firstLineAdjust); } } - page.CurrentPoint.Y -= firstLineAdjust; + + point.Y -= firstLineAdjust; lineActionDone = true; } diff --git a/com.etsoo.EasyPdf/Content/PdfHR.cs b/com.etsoo.EasyPdf/Content/PdfHR.cs index d7766df..1213501 100644 --- a/com.etsoo.EasyPdf/Content/PdfHR.cs +++ b/com.etsoo.EasyPdf/Content/PdfHR.cs @@ -4,11 +4,46 @@ namespace com.etsoo.EasyPdf.Content { + /// + /// PDF horizontal rule + /// PDF 水平线 + /// public class PdfHR : PdfBlock { - protected override async ValueTask WriteInnerAsync(IPdfPage page, PdfWriter writer, PdfStyle style, RectangleF rect, PdfPoint point) + /// + /// Color + /// 颜色 + /// + public PdfColor Color { get; set; } = PdfColor.Black; + + /// + /// Line width + /// 线条宽度 + /// + public float Width { get; set; } = 1; + + protected override async ValueTask WriteInnerAsync(IPdfPage page, PdfWriter writer, PdfStyle style, RectangleF rect, PdfPoint point) { + await NewLineActionAsync(page, writer, style, rect, point, CurrentLine, null); + return true; + } + + public override void SetParentStyle(PdfStyle parentStyle, float fontSize) + { + base.SetParentStyle(parentStyle, fontSize); + + // Default styles + Style.Margin ??= new PdfStyleSpace(6, 0); + + if (Style.Border == null) + { + var border = new PdfStyleBorder(Color, Width); + border.Left.Width = 0; + border.Right.Width = 0; + border.Top.Width = 0; + Style.Border = border; + } } } } diff --git a/com.etsoo.EasyPdf/Content/PdfImage.cs b/com.etsoo.EasyPdf/Content/PdfImage.cs index 38c4d43..bce96bd 100644 --- a/com.etsoo.EasyPdf/Content/PdfImage.cs +++ b/com.etsoo.EasyPdf/Content/PdfImage.cs @@ -105,6 +105,7 @@ public float ResizeFactor private readonly IImageFormat format; private int width; private int height; + private PdfStyleBorder? border; private PngCompressionLevel pngCompressionLevel { @@ -131,26 +132,46 @@ public PdfImage(Image image) : base(PdfChunkType.Image) this.format = format; } - public override Task CalculatePositionAsync(IPdfPage page, PdfBlockLine line, PdfBlockLineChunk chunk) + public override async Task CalculatePositionAsync(IPdfPage page, PdfBlockLine line, PdfBlockLineChunk chunk) { var point = page.CalculatePoint(chunk.StartPoint); // Image rendering starts from bottom to top // width & height is different from the text rendering - var pointBytes = PdfOperator.Tm(width, 0, 0, height, point.X, point.Y - height, true); + var ptWidth = width.PixelToPt(); + var ptHeight = height.PixelToPt(); + + var x = point.X; + var y = point.Y - ptHeight; + + var pointBytes = PdfOperator.Tm(ptWidth, 0, 0, ptHeight, x, y, true); chunk.InsertAfter(pointBytes, PdfOperator.q); - return Task.CompletedTask; + + // Border + // For simplicity, draw the border on top of the image + if (border != null) + { + var rect = new System.Drawing.RectangleF(x, y, ptWidth, ptHeight); + await page.WriteBorderAsync(border, rect); + } } public override async Task WriteInnerAsync(PdfWriter writer, PdfStyle style, System.Drawing.RectangleF rect, PdfPoint point, PdfBlockLine line, Func newLineAction) { - // Image size + // Border + var border = style.Border; + if (border?.HasBorder is true) + { + this.border = border; + } + + // Image size, in pixels var imageWidth = image.Width; var imageHeight = image.Height; - // Target image display size - (width, height) = style.GetSize(imageWidth, imageHeight, writer.CurrentPage.ContentRect); + // Target image display size in pixels + (width, height) = style.GetSize(imageWidth, imageHeight, rect); // Resize size var resizeWidth = (int)(width * resizeFactor); @@ -165,7 +186,6 @@ public override async Task WriteInnerAsync(PdfWriter writer, PdfStyle styl resizeHeight = imageHeight; } - // Auto resize to save space if (resizeWidth != image.Width || resizeHeight != image.Height) { @@ -244,7 +264,7 @@ public override async Task WriteInnerAsync(PdfWriter writer, PdfStyle styl }; // New chunk - var chunk = new PdfBlockLineChunk(null, height, StartPoint, false) + var chunk = new PdfBlockLineChunk(null, height.PixelToPt(), StartPoint, false) { Owner = this, Operators = operators, diff --git a/com.etsoo.EasyPdf/Content/PdfOperator.cs b/com.etsoo.EasyPdf/Content/PdfOperator.cs index 77f1f60..de5b6a4 100644 --- a/com.etsoo.EasyPdf/Content/PdfOperator.cs +++ b/com.etsoo.EasyPdf/Content/PdfOperator.cs @@ -1,5 +1,4 @@ -using com.etsoo.HtmlUtils; -using System.Drawing; +using System.Drawing; using System.Numerics; using System.Text; @@ -350,20 +349,42 @@ .. Encoding.ASCII.GetBytes($"{point.X} {point.Y}"), } /// - /// Defines the dash pattern, where 'width' represents the length of the dash and 'height' represents the length of the gap + /// Draw circle /// - /// Size, null to reset + /// Start x + /// Start y + /// Radius /// Bytes - public static byte[] Zd(HtmlSize? size = null) + public static byte[] Circle(float x, float y, float r) { - if (size.HasValue) - { - return Zd(size.Value); - } - else - { - return d; - } + var c = 0.552284749831f; + return + [ + .. Zm(x + r, y), + .. Zc(x + r, y + r * c, x + r * c, y + r, x, y + r), + .. Zc(x - r * c, y + r, x - r, y + r * c, x - r, y), + .. Zc(x - r, y - r * c, x - r * c, y - r, x, y - r), + .. Zc(x + r * c, y - r, x + r, y - r * c, x + r, y) + ]; + } + + /// + /// Append a cubic Bezier curve to the current path + /// + /// Control point 1 x + /// Control point 1 y + /// Control point 2 x + /// Control point 2 y + /// Target x + /// Target y + /// Bytes + public static byte[] Zc(float x1, float y1, float x2, float y2, float x3, float y3) + { + return + [ + .. Encoding.ASCII.GetBytes($"{x1} {y1} {x2} {y2} {x3} {y3}"), + .. " c\n"u8 + ]; } /// @@ -401,10 +422,21 @@ .. Encoding.ASCII.GetBytes($"/{refName}"), /// Point /// Bytes public static byte[] Zm(Vector2 point) + { + return Zm(point.X, point.Y); + } + + /// + /// Moves the current point to the position (point) without drawing anything + /// + /// X + /// Y + /// Bytes + public static byte[] Zm(float x, float y) { return [ - .. Encoding.ASCII.GetBytes($"{point.X} {point.Y}"), + .. Encoding.ASCII.GetBytes($"{x} {y}"), .. " m\n"u8 ]; } diff --git a/com.etsoo.EasyPdf/Content/PdfStyle.cs b/com.etsoo.EasyPdf/Content/PdfStyle.cs index ccca6be..659146a 100644 --- a/com.etsoo.EasyPdf/Content/PdfStyle.cs +++ b/com.etsoo.EasyPdf/Content/PdfStyle.cs @@ -82,12 +82,26 @@ public PdfStyle SetBorder(PdfStyleBorder? border) /// Width /// Style /// Style - public PdfStyle SetBorder(PdfColor color, int width = 1, PdfStyleBorderStyle style = PdfStyleBorderStyle.Solid) + public PdfStyle SetBorder(PdfColor color, float width = 1, PdfStyleBorderStyle style = PdfStyleBorderStyle.Solid) { Border = new PdfStyleBorder(color, width, style); return this; } + /// + /// Set border + /// 设置边框 + /// + /// Border color + /// Width + /// Style + /// Style + public PdfStyle SetBorder(string color, float width = 1, PdfStyleBorderStyle style = PdfStyleBorderStyle.Solid) + { + Border = new PdfStyleBorder(PdfColor.Parse(color) ?? PdfColor.Black, width, style); + return this; + } + /// /// Color /// 颜色 @@ -568,16 +582,16 @@ public float GetLineHeight(float fontLineHeight) var width = Width ?? size?.Width ?? 0; var height = Height ?? size?.Height ?? 0; - var marginLeft = (Margin?.Left ?? 0) + (Border?.Left.Width ?? 0); - var marginTop = (Margin?.Top ?? 0) + (Border?.Top.Width ?? 0); + var marginLeft = Margin?.Left ?? 0; + var marginTop = Margin?.Top ?? 0; - var marginRight = (Margin?.Right ?? 0) + (Border?.Right.Width ?? 0); - var marginBottom = (Margin?.Bottom ?? 0) + (Border?.Bottom.Width ?? 0); + var marginRight = Margin?.Right ?? 0; + var marginBottom = Margin?.Bottom ?? 0; - var paddingLeft = Padding?.Left ?? 0; - var paddingTop = Padding?.Top ?? 0; - var paddingRight = Padding?.Right ?? 0; - var paddingBottom = Padding?.Bottom ?? 0; + var paddingLeft = (Padding?.Left ?? 0) + (Border?.Left.Width ?? 0); + var paddingTop = (Padding?.Top ?? 0) + (Border?.Top.Width ?? 0); + var paddingRight = (Padding?.Right ?? 0) + (Border?.Right.Width ?? 0); + var paddingBottom = (Padding?.Bottom ?? 0) + (Border?.Bottom.Width ?? 0); width -= marginLeft + marginRight; height -= marginTop + marginBottom; @@ -621,6 +635,7 @@ public float GetLineHeight(float fontLineHeight) var rectLayout = new RectangleF(left, top, width, height); + // box-sizing: border-box behavior width -= paddingLeft + paddingRight; height -= paddingTop + paddingBottom; left += paddingLeft; @@ -639,12 +654,13 @@ public float GetLineHeight(float fontLineHeight) /// Get display size /// 获取显示尺寸 /// - /// Source width - /// Source height + /// Source width in pixels + /// Source height in pixels /// Target display rectangle - /// Result + /// Result in pixels public (int width, int height) GetSize(float sourceWidth, float sourceHeight, RectangleF rect) { + // CSS width & height in points var cssWidth = Width; var cssHeight = Height; @@ -653,11 +669,11 @@ public float GetLineHeight(float fontLineHeight) var ratio = sourceWidth / sourceHeight; if (cssWidth.HasValue) { - width = (int)cssWidth.Value; + width = (int)cssWidth.Value.PtToPixel(); if (cssHeight.HasValue) { - height = (int)cssHeight.Value; + height = (int)cssHeight.Value.PtToPixel(); } else { @@ -668,13 +684,13 @@ public float GetLineHeight(float fontLineHeight) { if (cssHeight.HasValue) { - height = (int)cssHeight.Value; + height = (int)cssHeight.Value.PtToPixel(); width = (int)(height * ratio); } else { - width = (int)rect.Width; - height = (int)rect.Height; + width = (int)rect.Width.PtToPixel(); + height = (int)rect.Height.PtToPixel(); } } diff --git a/com.etsoo.EasyPdf/Content/PdfStyleProperty.cs b/com.etsoo.EasyPdf/Content/PdfStyleProperty.cs index 7ce8394..eb43182 100644 --- a/com.etsoo.EasyPdf/Content/PdfStyleProperty.cs +++ b/com.etsoo.EasyPdf/Content/PdfStyleProperty.cs @@ -162,6 +162,7 @@ public enum PdfTextAlign public enum PdfStyleBorderStyle { None, + Dashed, Dotted, Solid } @@ -172,15 +173,21 @@ public enum PdfStyleBorderStyle /// public record PdfStyleBorderSide { + /// + /// None border + /// 空边框 + /// + public static PdfStyleBorderSide None => new(PdfColor.Black, 0, PdfStyleBorderStyle.None); + public virtual PdfColor Color { get; set; } - public virtual int Width { get; set; } + public virtual float Width { get; set; } public virtual PdfStyleBorderStyle Style { get; set; } public virtual bool HasBorder => Style != PdfStyleBorderStyle.None && Width > 0; - public PdfStyleBorderSide(PdfColor color, int width = 1, PdfStyleBorderStyle style = PdfStyleBorderStyle.Solid) + public PdfStyleBorderSide(PdfColor color, float width = 1, PdfStyleBorderStyle style = PdfStyleBorderStyle.Solid) { Color = color; Width = width; @@ -212,7 +219,7 @@ override public PdfColor Color } } - override public int Width + override public float Width { get { @@ -256,13 +263,13 @@ override public PdfStyleBorderStyle Style public PdfStyleBorderSide Left { get; internal set; } - public PdfStyleSpace? Radius { get; set; } + public PdfStyleSpace? Radius { get; internal set; } public override bool HasBorder => Left.HasBorder || Top.HasBorder || Right.HasBorder || Bottom.HasBorder; public bool SameStyle => Left.Equals(Top) && Left.Equals(Right) && Left.Equals(Bottom); - public PdfStyleBorder(PdfColor color, int width = 1, PdfStyleBorderStyle style = PdfStyleBorderStyle.Solid) + public PdfStyleBorder(PdfColor color, float width = 1, PdfStyleBorderStyle style = PdfStyleBorderStyle.Solid) : base(color, width, style) { Top = new PdfStyleBorderSide(color, width, style); @@ -270,17 +277,5 @@ public PdfStyleBorder(PdfColor color, int width = 1, PdfStyleBorderStyle style = Bottom = new PdfStyleBorderSide(color, width, style); Left = new PdfStyleBorderSide(color, width, style); } - - public PdfStyleBorder DeepClone() - { - return new PdfStyleBorder(Color, Width, Style) - { - Top = Top with { }, - Right = Right with { }, - Bottom = Bottom with { }, - Left = Left with { }, - Radius = Radius - }; - } } } diff --git a/com.etsoo.EasyPdf/Content/PdfTextChunk.cs b/com.etsoo.EasyPdf/Content/PdfTextChunk.cs index 7c89eaf..bc8ce8f 100644 --- a/com.etsoo.EasyPdf/Content/PdfTextChunk.cs +++ b/com.etsoo.EasyPdf/Content/PdfTextChunk.cs @@ -14,6 +14,12 @@ namespace com.etsoo.EasyPdf.Content /// public class PdfTextChunk : PdfChunk { + /// + /// Line height adjustment + /// 行高调整 + /// + public const float LineHeightAdjust = 1; + /// /// Content /// 内容 @@ -80,7 +86,7 @@ public override Task CalculatePositionAsync(IPdfPage page, PdfBlockLine line, Pd // Lending if (Type == PdfChunkType.Text && chunk.Font != null) { - var lineHeight = line.Height; + var lineHeight = line.Height - LineHeightAdjust; if (chunk.Font.LineHeight < lineHeight) { var index = chunk.FindOperator(PdfOperator.TLBytes); @@ -137,7 +143,8 @@ public override async Task WriteInnerAsync(PdfWriter writer, PdfStyle styl if (Font == null) { Font = font; - lineHeight = style.GetLineHeight(font.LineHeight); + + lineHeight = style.GetLineHeight(font.LineHeight) + LineHeightAdjust; } else { diff --git a/com.etsoo.EasyPdf/Objects/PdfPage.cs b/com.etsoo.EasyPdf/Objects/PdfPage.cs index 6997fca..be66332 100644 --- a/com.etsoo.EasyPdf/Objects/PdfPage.cs +++ b/com.etsoo.EasyPdf/Objects/PdfPage.cs @@ -185,6 +185,7 @@ public async Task EndTextAsync() private RectangleF GetPageRectangle(RectangleF docRect, PdfStyle style) { + // box-sizing: border-box behavior var adjustLeft = (style.Padding?.Left ?? 0) + (style.Border?.Left?.Width ?? 0); var adjustTop = (style.Padding?.Top ?? 0) + (style.Border?.Top?.Width ?? 0); var adjustRight = (style.Padding?.Right ?? 0) + (style.Border?.Right?.Width ?? 0); @@ -363,6 +364,205 @@ public async Task WriteBackgroundAsync(PdfColor bgcolor, RectangleF rect) await RestoreStateAsync(); } + private async Task CreateDashedLineXAsync(float width, Vector2 start, Vector2 end) + { + if (width < 1.5) width = 1.5f; + + var (w, g) = CalculateDashedGap(end.X - start.X, width); + + var step = 0; + while (start.X < end.X) + { + if (step % 2 == 0) + { + start.X += w; + + if (start.X > end.X) + await Stream.WriteAsync(PdfOperator.Zl(end)); + else + await Stream.WriteAsync(PdfOperator.Zl(start)); + } + else + { + start.X += g; + await Stream.WriteAsync(PdfOperator.Zm(start)); + } + + step++; + } + } + + private (float width, float gap) CalculateDashedGap(float width, float gap, int multiple = 2) + { + var count = Convert.ToInt32((width + gap) / ((1 + multiple) * gap)); + var newGap = (width - multiple * gap * count) / (count - 1); + return (multiple * gap, newGap); + } + + private async Task CreateDashedLineYAsync(float width, Vector2 start, Vector2 end) + { + if (width < 1.5) width = 1.5f; + + var (w, g) = CalculateDashedGap(end.Y - start.Y, width); + + var step = 0; + while (start.Y < end.Y) + { + if (step % 2 == 0) + { + start.Y += w; + + if (start.Y > end.Y) + await Stream.WriteAsync(PdfOperator.Zl(end)); + else + await Stream.WriteAsync(PdfOperator.Zl(start)); + } + else + { + start.Y += g; + await Stream.WriteAsync(PdfOperator.Zm(start)); + } + + step++; + } + } + + private async Task CreateDottedLineXAsync(float width, Vector2 start, Vector2 end) + { + if (width < 1.5) width = 1.5f; + + var (w, g) = CalculateDashedGap(end.X - start.X, width, 1); + + var step = 0; + while (start.X < end.X) + { + if (step % 2 == 0) + { + await Stream.WriteAsync(PdfOperator.Circle(start.X + w / 2, start.Y, w / 2)); + start.X += w; + } + else + { + start.X += g; + await Stream.WriteAsync(PdfOperator.Zm(start)); + } + + step++; + } + } + + private async Task CreateDottedLineYAsync(float width, Vector2 start, Vector2 end) + { + if (width < 1.5) width = 1.5f; + + var (w, g) = CalculateDashedGap(end.Y - start.Y, width, 1); + + var step = 0; + while (start.Y < end.Y) + { + if (step % 2 == 0) + { + await Stream.WriteAsync(PdfOperator.Circle(start.X, start.Y + w / 2, w / 2)); + start.Y += w; + } + else + { + start.Y += g; + await Stream.WriteAsync(PdfOperator.Zm(start)); + } + + step++; + } + } + + private async Task DrawBorderAsync(float width, PdfColor color, PdfStyleBorderStyle style, Vector2 start, Vector2 end, bool vertical) + { + if (style == PdfStyleBorderStyle.Dotted) + { + await Stream.WriteAsync(PdfOperator.Zw(0)); + await Stream.WriteAsync(PdfOperator.RG2(color)); + + if (vertical) + await CreateDottedLineYAsync(width, start, end); + else + await CreateDottedLineXAsync(width, start, end); + + await Stream.WriteAsync(PdfOperator.B); + } + else + { + await Stream.WriteAsync(PdfOperator.RG(color)); + await Stream.WriteAsync(PdfOperator.Zw(width)); + await Stream.WriteAsync(PdfOperator.Zm(start)); + + if (style == PdfStyleBorderStyle.Dashed) + { + if (vertical) + await CreateDashedLineYAsync(width, start, end); + else + await CreateDashedLineXAsync(width, start, end); + } + else + { + await Stream.WriteAsync(PdfOperator.Zl(end)); + } + + await Stream.WriteAsync(PdfOperator.S); + } + } + + private async Task WriteLeftBorderAsync(RectangleF rect, float width, PdfColor color, PdfStyleBorderStyle style) + { + var fx = rect.X + width / 2; + var fy = rect.Y; + var start = new Vector2(fx, fy); + + var tx = fx; + var ty = rect.Y + rect.Height; + var end = new Vector2(tx, ty); + + await DrawBorderAsync(width, color, style, start, end, true); + } + + private async Task WriteTopBorderAsync(RectangleF rect, float width, PdfColor color, PdfStyleBorderStyle style) + { + var fx = rect.X; + var fy = rect.Y + rect.Height - width / 2; + var start = new Vector2(fx, fy); + + var tx = rect.X + rect.Width; + var ty = fy; + var end = new Vector2(tx, ty); + + await DrawBorderAsync(width, color, style, start, end, false); + } + + private async Task WriteRightBorderAsync(RectangleF rect, float width, PdfColor color, PdfStyleBorderStyle style) + { + var fx = rect.X + rect.Width - width / 2; + var fy = rect.Y + rect.Height; + var end = new Vector2(fx, fy); + + var tx = fx; + var ty = rect.Y; + var start = new Vector2(tx, ty); + + await DrawBorderAsync(width, color, style, start, end, true); + } + + private async Task WriteBottomBorderAsync(RectangleF rect, float width, PdfColor color, PdfStyleBorderStyle style) + { + var fx = rect.X + rect.Width; + var fy = rect.Y + width / 2; + var end = new Vector2(fx, fy); + + var tx = rect.X; + var ty = fy; + var start = new Vector2(tx, ty); + + await DrawBorderAsync(width, color, style, start, end, false); + } + /// /// Write border /// 输出边框 @@ -375,93 +575,44 @@ public async Task WriteBorderAsync(PdfStyleBorder border, RectangleF rect) // Save graphics state await SaveStateAsync(); - if (border.SameStyle) + var left = border.Left; + if (border.SameStyle && (left.Style == PdfStyleBorderStyle.Solid)) { // Reduce operators for simple style - var width = border.Left.Width; + var width = left.Width; var widthHalf = width / 2.0F; rect.Inflate(-widthHalf, -widthHalf); - await Stream.WriteAsync(PdfOperator.RG(border.Left.Color)); + await Stream.WriteAsync(PdfOperator.RG(left.Color)); await Stream.WriteAsync(PdfOperator.Zw(width)); await Stream.WriteAsync(PdfOperator.Zre(rect)); await Stream.WriteAsync(PdfOperator.S); } else { - var leftWidth = border.Left.Width; - var leftWidthHalf = leftWidth / 2.0F; - + var leftWidth = left.Width; var topWidth = border.Top.Width; - var topWidthHalf = topWidth / 2.0F; - var rightWidth = border.Right.Width; - var rightWidthHalf = rightWidth / 2.0F; - var bottomWidth = border.Bottom.Width; - var bottomWidthHalf = bottomWidth / 2.0F; - // Extended half width - float fx, fy, tx, ty; - - if (leftWidth > 0) + if (leftWidth > 0 && border.Left.Style != PdfStyleBorderStyle.None) { - fx = rect.X - leftWidthHalf; - fy = rect.Y - bottomWidth; - - tx = fx; - ty = rect.Y + rect.Height + topWidth; - - await Stream.WriteAsync(PdfOperator.Zm(new Vector2(fx, fy))); - await Stream.WriteAsync(PdfOperator.RG(border.Left.Color)); - await Stream.WriteAsync(PdfOperator.Zw(leftWidth)); - await Stream.WriteAsync(PdfOperator.Zl(new Vector2(tx, ty))); - await Stream.WriteAsync(PdfOperator.S); + await WriteLeftBorderAsync(rect, leftWidth, border.Left.Color, border.Left.Style); } - if (topWidth > 0) + if (topWidth > 0 && border.Top.Style != PdfStyleBorderStyle.None) { - fx = rect.X - leftWidth; - fy = rect.Y + rect.Height + topWidthHalf; - - tx = rect.X + rect.Width + rightWidth; - ty = fy; - - await Stream.WriteAsync(PdfOperator.Zm(new Vector2(fx, fy))); - await Stream.WriteAsync(PdfOperator.RG(border.Top.Color)); - await Stream.WriteAsync(PdfOperator.Zw(topWidth)); - await Stream.WriteAsync(PdfOperator.Zl(new Vector2(tx, ty))); - await Stream.WriteAsync(PdfOperator.S); + await WriteTopBorderAsync(rect, topWidth, border.Top.Color, border.Top.Style); } - if (rightWidth > 0) + if (rightWidth > 0 && border.Right.Style != PdfStyleBorderStyle.None) { - fx = rect.X + rect.Width + rightWidthHalf; - fy = rect.Y + rect.Height + topWidth; - - tx = fx; - ty = rect.Y - bottomWidth; - - await Stream.WriteAsync(PdfOperator.Zm(new Vector2(fx, fy))); - await Stream.WriteAsync(PdfOperator.RG(border.Right.Color)); - await Stream.WriteAsync(PdfOperator.Zw(rightWidth)); - await Stream.WriteAsync(PdfOperator.Zl(new Vector2(tx, ty))); - await Stream.WriteAsync(PdfOperator.S); + await WriteRightBorderAsync(rect, rightWidth, border.Right.Color, border.Right.Style); } - if (bottomWidth > 0) + if (bottomWidth > 0 && border.Bottom.Style != PdfStyleBorderStyle.None) { - fx = rect.X + rect.Width + rightWidth; - fy = rect.Y - bottomWidthHalf; - - tx = rect.X - leftWidth; - ty = fy; - - await Stream.WriteAsync(PdfOperator.Zm(new Vector2(fx, fy))); - await Stream.WriteAsync(PdfOperator.RG(border.Bottom.Color)); - await Stream.WriteAsync(PdfOperator.Zw(bottomWidth)); - await Stream.WriteAsync(PdfOperator.Zl(new Vector2(tx, ty))); - await Stream.WriteAsync(PdfOperator.S); + await WriteBottomBorderAsync(rect, bottomWidth, border.Bottom.Color, border.Bottom.Style); } } diff --git a/com.etsoo.EasyPdf/PdfExtensions.cs b/com.etsoo.EasyPdf/PdfExtensions.cs index 929ac87..4dcac56 100644 --- a/com.etsoo.EasyPdf/PdfExtensions.cs +++ b/com.etsoo.EasyPdf/PdfExtensions.cs @@ -334,6 +334,39 @@ public static IPdfType[] Parse(this ReadOnlySpan data) return items.ToArray(); } + /// + /// Pixel to point + /// 像素到点 + /// + /// Pixels + /// Result + public static float PixelToPt(this float pixel) + { + return pixel * 0.75f; + } + + /// + /// Pixel to point + /// 像素到点 + /// + /// Pixels + /// Result + public static float PixelToPt(this int pixel) + { + return pixel * 0.75f; + } + + /// + /// Point to pixel + /// 点到像素 + /// + /// Points + /// Results + public static float PtToPixel(this float pt) + { + return pt / 0.75f; + } + /// /// Convert PdfArray to Rectangle /// 转换PDF数组为正方形数据 diff --git a/com.etsoo.EasyPdf/PdfWriter.cs b/com.etsoo.EasyPdf/PdfWriter.cs index c6d3ceb..4efa19d 100644 --- a/com.etsoo.EasyPdf/PdfWriter.cs +++ b/com.etsoo.EasyPdf/PdfWriter.cs @@ -74,6 +74,8 @@ public void AdjustMargin(PdfStyleSpace? margin) { if (margin == null) { + // Reset the chain + lastMargin = null; return; }