Browse Source

Improve support for links (URL hyperlinks and internal links to named destinations) in accordance with PDF/UA-1 requirements.

Marcin Ziąbek 2 months ago
parent
commit
2c6a2d0e38

+ 2 - 2
Source/QuestPDF.UnitTests/TestEngine/MockCanvas.cs

@@ -46,8 +46,8 @@ namespace QuestPDF.UnitTests.TestEngine
         public void ClipRectangle(SkRect clipArea) => throw new NotImplementedException();
         public void ClipRoundedRectangle(SkRoundedRect clipArea) => throw new NotImplementedException();
         
-        public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
-        public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
+        public void DrawHyperlink(Size size, string url, string? description) => throw new NotImplementedException();
+        public void DrawSectionLink(Size size, string sectionName, string? description) => throw new NotImplementedException();
         public void DrawSection(string sectionName) => throw new NotImplementedException();
         
         public void SetSemanticNodeId(int nodeId) => throw new NotImplementedException();

+ 2 - 2
Source/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs

@@ -43,8 +43,8 @@ namespace QuestPDF.UnitTests.TestEngine
         public void ClipRectangle(SkRect clipArea) => throw new NotImplementedException();
         public void ClipRoundedRectangle(SkRoundedRect clipArea) => throw new NotImplementedException();
         
-        public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
-        public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
+        public void DrawHyperlink(Size size, string url, string? description) => throw new NotImplementedException();
+        public void DrawSectionLink(Size size, string sectionName, string? description) => throw new NotImplementedException();
         public void DrawSection(string sectionName) => throw new NotImplementedException();
         
         public void SetSemanticNodeId(int nodeId) => throw new NotImplementedException();

+ 2 - 2
Source/QuestPDF/Drawing/DrawingCanvases/FreeDrawingCanvas.cs

@@ -133,12 +133,12 @@ namespace QuestPDF.Drawing.DrawingCanvases
             
         }
         
-        public void DrawHyperlink(string url, Size size)
+        public void DrawHyperlink(Size size, string url, string? description)
         {
            
         }
 
-        public void DrawSectionLink(string sectionName, Size size)
+        public void DrawSectionLink(Size size, string sectionName, string? description)
         {
             
         }

+ 4 - 4
Source/QuestPDF/Drawing/DrawingCanvases/ProxyDrawingCanvas.cs

@@ -147,14 +147,14 @@ internal sealed class ProxyDrawingCanvas : IDrawingCanvas, IDisposable
         Target.ClipRoundedRectangle(clipArea);
     }
 
-    public void DrawHyperlink(string url, Size size)
+    public void DrawHyperlink(Size size, string url, string? description)
     {
-        Target.DrawHyperlink(url, size);
+        Target.DrawHyperlink(size, url, description);
     }
 
-    public void DrawSectionLink(string sectionName, Size size)
+    public void DrawSectionLink(Size size, string sectionName, string? description)
     {
-        Target.DrawSectionLink(sectionName, size);
+        Target.DrawSectionLink(size, sectionName, description);
     }
 
     public void DrawSection(string sectionName)

+ 4 - 4
Source/QuestPDF/Drawing/DrawingCanvases/SkiaDrawingCanvas.cs

@@ -216,14 +216,14 @@ namespace QuestPDF.Drawing.DrawingCanvases
             CurrentCanvas.ClipRoundedRectangle(clipArea);
         }
         
-        public void DrawHyperlink(string url, Size size)
+        public void DrawHyperlink(Size size, string url, string? description)
         {
-            CurrentCanvas.AnnotateUrl(size.Width, size.Height, url);
+            CurrentCanvas.AnnotateUrl(size.Width, size.Height, url, description);
         }
         
-        public void DrawSectionLink(string sectionName, Size size)
+        public void DrawSectionLink(Size size, string sectionName, string? description)
         {
-            CurrentCanvas.AnnotateDestinationLink(size.Width, size.Height, sectionName);
+            CurrentCanvas.AnnotateDestinationLink(size.Width, size.Height, sectionName, description);
         }
 
         public void DrawSection(string sectionName)

+ 2 - 1
Source/QuestPDF/Elements/Hyperlink.cs

@@ -8,6 +8,7 @@ namespace QuestPDF.Elements
     {
         public ContentDirection ContentDirection { get; set; }
         public string Url { get; set; } = "https://www.questpdf.com";
+        public string? Description { get; set; }
         
         internal override void Draw(Size availableSpace)
         {
@@ -21,7 +22,7 @@ namespace QuestPDF.Elements
                 : new Position(availableSpace.Width - targetSize.Width, 0);
 
             Canvas.Translate(horizontalOffset);
-            Canvas.DrawHyperlink(Url, availableSpace);
+            Canvas.DrawHyperlink(availableSpace, Url, Description);
             Canvas.Translate(horizontalOffset.Reverse());
             
             base.Draw(availableSpace);

+ 2 - 1
Source/QuestPDF/Elements/SectionLink.cs

@@ -7,6 +7,7 @@ namespace QuestPDF.Elements
     internal sealed class SectionLink : ContainerElement
     {
         public string SectionName { get; set; }
+        public string? Description { get; set; }
         
         internal override void Draw(Size availableSpace)
         {
@@ -16,7 +17,7 @@ namespace QuestPDF.Elements
                 return;
 
             var targetName = PageContext.GetDocumentLocationName(SectionName);
-            Canvas.DrawSectionLink(targetName, targetSize);
+            Canvas.DrawSectionLink(targetSize, targetName, Description);
             base.Draw(availableSpace);
         }
 

+ 28 - 0
Source/QuestPDF/Elements/SemanticTag.cs

@@ -34,6 +34,9 @@ internal class SemanticTag : ContainerElement
         if (TagType is "H" or "H1" or "H2" or "H3" or "H4" or "H5" or "H6")
             UpdateHeaderText();
         
+        if (TagType is "Link")
+            UpdateInnerLink();
+        
         var id = SemanticTreeManager.GetNextNodeId();
             
         SemanticTreeNode = new SemanticTreeNode
@@ -73,4 +76,29 @@ internal class SemanticTag : ContainerElement
             }
         }
     }
+    
+    private void UpdateInnerLink()
+    {
+        if (string.IsNullOrWhiteSpace(Alt))
+            return;
+        
+        var currentChild = Child;
+        
+        while (currentChild != null)
+        {
+            if (currentChild is Hyperlink hyperlink)
+            {
+                hyperlink.Description = Alt;
+                return;
+            }
+            
+            if (currentChild is SectionLink sectionLink)
+            {
+                sectionLink.Description = Alt;
+                return;
+            }
+            
+            currentChild = (currentChild as ContainerElement)?.Child;
+        }
+    }
 }

+ 2 - 2
Source/QuestPDF/Elements/Text/TextBlock.cs

@@ -240,7 +240,7 @@ namespace QuestPDF.Elements.Text
                             continue;
                         
                         Canvas.Translate(offset);
-                        Canvas.DrawHyperlink(hyperlink.Url, new Size(position.Width, position.Height));
+                        Canvas.DrawHyperlink(new Size(position.Width, position.Height), hyperlink.Url, hyperlink.Text);
                         Canvas.Translate(offset.Reverse());
                     }
                 }
@@ -261,7 +261,7 @@ namespace QuestPDF.Elements.Text
                             continue;
                         
                         Canvas.Translate(offset);
-                        Canvas.DrawSectionLink(targetName, new Size(position.Width, position.Height));
+                        Canvas.DrawSectionLink(new Size(position.Width, position.Height), targetName, sectionLink.Text);
                         Canvas.Translate(offset.Reverse());
                     }
                 }

+ 2 - 2
Source/QuestPDF/Fluent/SemanticExtensions.cs

@@ -343,9 +343,9 @@ public static class SemanticExtensions
     /// Applies the semantic "Link" tag to the specified container.
     /// This is used to signify that the content represents a link or hyperlink within a document.
     /// </summary>
-    public static IContainer SemanticLink(this IContainer container)
+    public static IContainer SemanticLink(this IContainer container, string alternativeText)
     {
-        return container.SemanticTag("Link");
+        return container.SemanticTag("Link", alternativeText: alternativeText);
     }
     
     #endregion

+ 2 - 2
Source/QuestPDF/Infrastructure/IDrawingCanvas.cs

@@ -37,8 +37,8 @@ namespace QuestPDF.Infrastructure
         void ClipRectangle(SkRect clipArea);
         void ClipRoundedRectangle(SkRoundedRect clipArea);
         
-        void DrawHyperlink(string url, Size size);
-        void DrawSectionLink(string sectionName, Size size);
+        void DrawHyperlink(Size size, string url, string? description);
+        void DrawSectionLink(Size size, string sectionName, string? description);
         void DrawSection(string sectionName);
         
         void SetSemanticNodeId(int nodeId);

+ 16 - 6
Source/QuestPDF/Skia/SkCanvas.cs

@@ -116,9 +116,9 @@ internal sealed class SkCanvas : IDisposable
         API.canvas_clip_rounded_rectangle(Instance, rect);
     }
     
-    public void AnnotateUrl(float width, float height, string url)
+    public void AnnotateUrl(float width, float height, string url, string? description)
     {
-        API.canvas_annotate_url(Instance, width, height, url);
+        API.canvas_annotate_url(Instance, width, height, url, description);
     }
     
     public void AnnotateDestination(string destinationName)
@@ -126,9 +126,9 @@ internal sealed class SkCanvas : IDisposable
         API.canvas_annotate_destination(Instance, destinationName);
     }
     
-    public void AnnotateDestinationLink(float width, float height, string destinationName)
+    public void AnnotateDestinationLink(float width, float height, string destinationName, string? description)
     {
-        API.canvas_annotate_destination_link(Instance, width, height, destinationName);
+        API.canvas_annotate_destination_link(Instance, width, height, destinationName, description);
     }
     
     public SkCanvasMatrix GetCurrentMatrix()
@@ -227,13 +227,23 @@ internal sealed class SkCanvas : IDisposable
         public static extern void canvas_clip_rounded_rectangle(IntPtr canvas, SkRoundedRect rect);
         
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
-        public static extern void canvas_annotate_url(IntPtr canvas, float width, float height, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string url);
+        public static extern void canvas_annotate_url(
+            IntPtr canvas, 
+            float width, 
+            float height, 
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string url,
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string? description);
 
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         public static extern void canvas_annotate_destination(IntPtr canvas, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName);
 
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
-        public static extern void canvas_annotate_destination_link(IntPtr canvas, float width, float height, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName);
+        public static extern void canvas_annotate_destination_link(
+            IntPtr canvas, 
+            float width,
+            float height, 
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName,
+            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string? description);
         
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         public static extern SkCanvasMatrix canvas_get_matrix9(IntPtr canvas);