From 98b226480dda41e3aa8d2bc6c48badac76b9f7b2 Mon Sep 17 00:00:00 2001 From: Jan Schneider Date: Tue, 4 Nov 2025 12:13:37 +0100 Subject: [PATCH] fix: support radial gradients and extended gradients --- .../PdfSharp/Drawing.Pdf/PdfGraphicsState.cs | 33 ++++---- .../src/PdfSharp/Pdf.Advanced/PdfShading.cs | 68 +++++++++++++++-- .../Pdf.Advanced/PdfShadingPattern.cs | 15 ++++ .../PdfSharp.Tests/Pdf/creation/BasicTests.cs | 76 +++++++++++++++++++ 4 files changed, 172 insertions(+), 20 deletions(-) diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/PdfGraphicsState.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/PdfGraphicsState.cs index 69fcc3f7..5e899d8a 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/PdfGraphicsState.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Drawing.Pdf/PdfGraphicsState.cs @@ -229,21 +229,28 @@ public void RealizeBrush(XBrush brush, PdfColorMode colorMode, int renderingMode { if (renderingMode != 0) throw new InvalidOperationException("Rendering modes other than 0 can only be used with solid color brushes."); - - if (brush is XLinearGradientBrush gradientBrush) + + Debug.Assert(UnrealizedCtm.IsIdentity, "Must realize ctm first."); + var matrix = renderer.DefaultViewMatrix; + matrix.Prepend(EffectiveCtm); + var pattern = new PdfShadingPattern(renderer.Owner); + + switch (brush) { - Debug.Assert(UnrealizedCtm.IsIdentity, "Must realize ctm first."); - XMatrix matrix = renderer.DefaultViewMatrix; - matrix.Prepend(EffectiveCtm); - PdfShadingPattern pattern = new PdfShadingPattern(renderer.Owner); - pattern.SetupFromBrush(gradientBrush, matrix, renderer); - string name = renderer.Resources.AddPattern(pattern); - renderer.AppendFormatString("/Pattern cs\n", name); - renderer.AppendFormatString("{0} scn\n", name); - - // Invalidate fill color. - _realizedFillColor = XColor.Empty; + case XLinearGradientBrush gradientBrush: + pattern.SetupFromBrush(gradientBrush, matrix, renderer); + break; + case XRadialGradientBrush radialGradient: + pattern.SetupFromBrush(radialGradient, matrix, renderer); + break; } + + var name = renderer.Resources.AddPattern(pattern); + renderer.AppendFormatString("/Pattern cs\n", name); + renderer.AppendFormatString("{0} scn\n", name); + + // Invalidate fill color. + _realizedFillColor = XColor.Empty; } } diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfShading.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfShading.cs index 496597c6..64bb72e2 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfShading.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfShading.cs @@ -38,8 +38,6 @@ internal void SetupFromBrush(XLinearGradientBrush brush, XGraphicsPdfRenderer re XColor color1 = ColorSpaceHelper.EnsureColorMode(colorMode, brush._color1); XColor color2 = ColorSpaceHelper.EnsureColorMode(colorMode, brush._color2); - PdfDictionary function = new PdfDictionary(); - Elements[Keys.ShadingType] = new PdfInteger(2); if (colorMode != PdfColorMode.Cmyk) Elements[Keys.ColorSpace] = new PdfName("/DeviceRGB"); @@ -95,21 +93,77 @@ internal void SetupFromBrush(XLinearGradientBrush brush, XGraphicsPdfRenderer re } const string format = Config.SignificantDecimalPlaces3; - Elements[Keys.Coords] = new PdfLiteral("[{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "}]", x1, y1, x2, y2); + Elements[Keys.Coords] = + new PdfLiteral("[{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "}]", x1, y1, + x2, y2); //Elements[Keys.Background] = new PdfRawItem("[0 1 1]"); //Elements[Keys.Domain] = - Elements[Keys.Function] = function; - //Elements[Keys.Extend] = new PdfRawItem("[true true]"); + Elements[Keys.Function] = SetupGradient(color1, color2); + Elements[Keys.Extend] = new PdfLiteral("[{0} {1}]", + brush.ExtendLeft ? "true" : "false", + brush.ExtendRight ? "true" : "false"); + } - string clr1 = "[" + PdfEncoders.ToString(color1, colorMode) + "]"; - string clr2 = "[" + PdfEncoders.ToString(color2, colorMode) + "]"; + /// + /// Setups the shading from the specified brush. + /// + internal void SetupFromBrush(XRadialGradientBrush brush, XGraphicsPdfRenderer renderer) + { + if (brush == null) + throw new ArgumentNullException(nameof(brush)); + + var colorMode = _document.Options.ColorMode; + var color1 = ColorSpaceHelper.EnsureColorMode(colorMode, brush._color1); + var color2 = ColorSpaceHelper.EnsureColorMode(colorMode, brush._color2); + + Elements[Keys.ShadingType] = new PdfInteger(3); + if (colorMode != PdfColorMode.Cmyk) + Elements[Keys.ColorSpace] = new PdfName("/DeviceRGB"); + else + Elements[Keys.ColorSpace] = new PdfName("/DeviceCMYK"); + + double x1 = 0, y1 = 0, x2 = 0, y2 = 0; + + XPoint pt1 = renderer.WorldToView(brush._point1); + XPoint pt2 = renderer.WorldToView(brush._point2); + + x1 = pt1.X; + y1 = pt1.Y; + x2 = pt2.X; + y2 = pt2.Y; + + const string format = Config.SignificantDecimalPlaces3; + Elements[Keys.Coords] = + new PdfLiteral( + "[{0:" + format + "} {1:" + format + "} {2:" + format + "} {3:" + format + "} {4:" + format + + "} {5:" + format + "}]", x1, y1, brush.InnerRadius, + x2, y2, brush.OuterRadius); + + //Elements[Keys.Background] = new PdfRawItem("[0 1 1]"); + //Elements[Keys.Domain] = + + Elements[Keys.Function] = SetupGradient(color1, color2); + Elements[Keys.Extend] = new PdfLiteral("[{0} {1}]", + brush.ExtendLeft ? "true" : "false", + brush.ExtendRight ? "true" : "false"); + } + + private PdfDictionary SetupGradient(XColor color1, XColor color2) + { + var colorMode = _document.Options.ColorMode; + var function = new PdfDictionary(); + + var clr1 = "[" + PdfEncoders.ToString(color1, colorMode) + "]"; + var clr2 = "[" + PdfEncoders.ToString(color2, colorMode) + "]"; function.Elements["/FunctionType"] = new PdfInteger(2); function.Elements["/C0"] = new PdfLiteral(clr1); function.Elements["/C1"] = new PdfLiteral(clr2); function.Elements["/Domain"] = new PdfLiteral("[0 1]"); function.Elements["/N"] = new PdfInteger(1); + + return function; } /// diff --git a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfShadingPattern.cs b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfShadingPattern.cs index f45b2264..0a12fa20 100644 --- a/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfShadingPattern.cs +++ b/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Advanced/PdfShadingPattern.cs @@ -43,6 +43,21 @@ internal void SetupFromBrush(XLinearGradientBrush brush, XMatrix matrix, XGraphi //Elements[Keys.Matrix] = new PdfLiteral("[" + PdfEncoders.ToString(matrix) + "]"); Elements.SetMatrix(Keys.Matrix, matrix); } + + /// + /// Setups the shading pattern from the specified brush. + /// + internal void SetupFromBrush(XRadialGradientBrush brush, XMatrix matrix, XGraphicsPdfRenderer renderer) + { + if (brush == null) + throw new ArgumentNullException(nameof(brush)); + + PdfShading shading = new PdfShading(_document); + shading.SetupFromBrush(brush, renderer); + Elements[Keys.Shading] = shading; + //Elements[Keys.Matrix] = new PdfLiteral("[" + PdfEncoders.ToString(matrix) + "]"); + Elements.SetMatrix(Keys.Matrix, matrix); + } /// /// Common keys for all streams. diff --git a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/creation/BasicTests.cs b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/creation/BasicTests.cs index 41d2f455..afbcdcc3 100644 --- a/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/creation/BasicTests.cs +++ b/src/foundation/src/PDFsharp/tests/PdfSharp.Tests/Pdf/creation/BasicTests.cs @@ -372,5 +372,81 @@ public void Create_all_boxes_BasicTests() // ...and start a viewer. PdfFileUtility.ShowDocumentIfDebugging(filename); } + + [Fact] + public void Create_gradient_brushes() + { + // Create a new PDF document. + var document = new PdfDocument(); + document.Info.Title = "Created with PDFsharp"; + + // Create an empty page in this document. + var page = document.AddPage(); + var gfx = XGraphics.FromPdfPage(page); + + gfx.Save(); + gfx.TranslateTransform(50, 50); + gfx.DrawRectangle(null, new XLinearGradientBrush(new XPoint(50, 50), new XPoint(150, 150), XColors.Red, XColors.Green) + { + ExtendLeft = false, + ExtendRight = false + }, new XRect(0, 0, 200, 200)); + + gfx.Restore(); + gfx.Save(); + gfx.TranslateTransform(300, 50); + + gfx.DrawRectangle(null, new XLinearGradientBrush(new XPoint(50, 50), new XPoint(150, 150), XColors.Red, XColors.Green) + { + ExtendLeft = true, + ExtendRight = true + }, new XRect(0, 0, 200, 200)); + + gfx.Restore(); + gfx.Save(); + gfx.TranslateTransform(50, 300); + + gfx.DrawRectangle(null, new XRadialGradientBrush(new XPoint(100, 100), new XPoint(100, 100), 50, 100, XColors.Red, XColors.Green) + { + ExtendLeft = false, + ExtendRight = false + }, new XRect(0, 0, 200, 200)); + + gfx.Restore(); + gfx.Save(); + gfx.TranslateTransform(300, 300); + + gfx.DrawRectangle(null, new XRadialGradientBrush(new XPoint(100, 100), new XPoint(100, 100), 50, 100, XColors.Red, XColors.Green) + { + ExtendLeft = true, + ExtendRight = true + }, new XRect(0, 0, 200, 200)); + + gfx.Restore(); + gfx.Save(); + gfx.TranslateTransform(50, 550); + + gfx.DrawRectangle(null, new XRadialGradientBrush(new XPoint(50, 50), new XPoint(150, 150), 50, 100, XColors.Red, XColors.Green) + { + ExtendLeft = false, + ExtendRight = false + }, new XRect(0, 0, 200, 200)); + + gfx.Restore(); + gfx.Save(); + gfx.TranslateTransform(300, 550); + + gfx.DrawRectangle(null, new XRadialGradientBrush(new XPoint(50, 50), new XPoint(150, 150), 50, 100, XColors.Red, XColors.Green) + { + ExtendLeft = true, + ExtendRight = true + }, new XRect(0, 0, 200, 200)); + + // Save the document... + string filename = PdfFileUtility.GetTempPdfFileName("GradientTest"); + document.Save(filename); + // ...and start a viewer. + PdfFileUtility.ShowDocumentIfDebugging(filename); + } } }