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 ClipRectangle(SkRect clipArea) => throw new NotImplementedException();
         public void ClipRoundedRectangle(SkRoundedRect 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 DrawSection(string sectionName) => throw new NotImplementedException();
         
         
         public void SetSemanticNodeId(int nodeId) => 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 ClipRectangle(SkRect clipArea) => throw new NotImplementedException();
         public void ClipRoundedRectangle(SkRoundedRect 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 DrawSection(string sectionName) => throw new NotImplementedException();
         
         
         public void SetSemanticNodeId(int nodeId) => 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);
         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)
     public void DrawSection(string sectionName)

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

@@ -216,14 +216,14 @@ namespace QuestPDF.Drawing.DrawingCanvases
             CurrentCanvas.ClipRoundedRectangle(clipArea);
             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)
         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 ContentDirection ContentDirection { get; set; }
         public string Url { get; set; } = "https://www.questpdf.com";
         public string Url { get; set; } = "https://www.questpdf.com";
+        public string? Description { get; set; }
         
         
         internal override void Draw(Size availableSpace)
         internal override void Draw(Size availableSpace)
         {
         {
@@ -21,7 +22,7 @@ namespace QuestPDF.Elements
                 : new Position(availableSpace.Width - targetSize.Width, 0);
                 : new Position(availableSpace.Width - targetSize.Width, 0);
 
 
             Canvas.Translate(horizontalOffset);
             Canvas.Translate(horizontalOffset);
-            Canvas.DrawHyperlink(Url, availableSpace);
+            Canvas.DrawHyperlink(availableSpace, Url, Description);
             Canvas.Translate(horizontalOffset.Reverse());
             Canvas.Translate(horizontalOffset.Reverse());
             
             
             base.Draw(availableSpace);
             base.Draw(availableSpace);

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

@@ -7,6 +7,7 @@ namespace QuestPDF.Elements
     internal sealed class SectionLink : ContainerElement
     internal sealed class SectionLink : ContainerElement
     {
     {
         public string SectionName { get; set; }
         public string SectionName { get; set; }
+        public string? Description { get; set; }
         
         
         internal override void Draw(Size availableSpace)
         internal override void Draw(Size availableSpace)
         {
         {
@@ -16,7 +17,7 @@ namespace QuestPDF.Elements
                 return;
                 return;
 
 
             var targetName = PageContext.GetDocumentLocationName(SectionName);
             var targetName = PageContext.GetDocumentLocationName(SectionName);
-            Canvas.DrawSectionLink(targetName, targetSize);
+            Canvas.DrawSectionLink(targetSize, targetName, Description);
             base.Draw(availableSpace);
             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")
         if (TagType is "H" or "H1" or "H2" or "H3" or "H4" or "H5" or "H6")
             UpdateHeaderText();
             UpdateHeaderText();
         
         
+        if (TagType is "Link")
+            UpdateInnerLink();
+        
         var id = SemanticTreeManager.GetNextNodeId();
         var id = SemanticTreeManager.GetNextNodeId();
             
             
         SemanticTreeNode = new SemanticTreeNode
         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;
                             continue;
                         
                         
                         Canvas.Translate(offset);
                         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());
                         Canvas.Translate(offset.Reverse());
                     }
                     }
                 }
                 }
@@ -261,7 +261,7 @@ namespace QuestPDF.Elements.Text
                             continue;
                             continue;
                         
                         
                         Canvas.Translate(offset);
                         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());
                         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.
     /// 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.
     /// This is used to signify that the content represents a link or hyperlink within a document.
     /// </summary>
     /// </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
     #endregion

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

@@ -37,8 +37,8 @@ namespace QuestPDF.Infrastructure
         void ClipRectangle(SkRect clipArea);
         void ClipRectangle(SkRect clipArea);
         void ClipRoundedRectangle(SkRoundedRect 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 DrawSection(string sectionName);
         
         
         void SetSemanticNodeId(int nodeId);
         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);
         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)
     public void AnnotateDestination(string destinationName)
@@ -126,9 +126,9 @@ internal sealed class SkCanvas : IDisposable
         API.canvas_annotate_destination(Instance, destinationName);
         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()
     public SkCanvasMatrix GetCurrentMatrix()
@@ -227,13 +227,23 @@ internal sealed class SkCanvas : IDisposable
         public static extern void canvas_clip_rounded_rectangle(IntPtr canvas, SkRoundedRect rect);
         public static extern void canvas_clip_rounded_rectangle(IntPtr canvas, SkRoundedRect rect);
         
         
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         [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)]
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         public static extern void canvas_annotate_destination(IntPtr canvas, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName);
         public static extern void canvas_annotate_destination(IntPtr canvas, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8StringMarshaller))] string destinationName);
 
 
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         [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)]
         [DllImport(SkiaAPI.LibraryName, CallingConvention = CallingConvention.Cdecl)]
         public static extern SkCanvasMatrix canvas_get_matrix9(IntPtr canvas);
         public static extern SkCanvasMatrix canvas_get_matrix9(IntPtr canvas);