Browse Source

Feature: visual debugging for layout overflow exceptions (#669)

Marcin Ziąbek 2 years ago
parent
commit
9cb86ccb13
29 changed files with 723 additions and 286 deletions
  1. 188 0
      Source/QuestPDF.Examples/ContentOverflowVisualizationExamples.cs
  2. 8 13
      Source/QuestPDF.Previewer/CommunicationService.cs
  3. 1 0
      Source/QuestPDF.Previewer/DocumentSnapshot.cs
  4. 1 1
      Source/QuestPDF.Previewer/Helpers.cs
  5. 10 2
      Source/QuestPDF.Previewer/InteractiveCanvas.cs
  6. 0 6
      Source/QuestPDF.Previewer/PreviewPage.cs
  7. 3 3
      Source/QuestPDF.Previewer/PreviewerControl.cs
  8. 12 0
      Source/QuestPDF.Previewer/PreviewerWindow.axaml
  9. 12 4
      Source/QuestPDF.Previewer/PreviewerWindowViewModel.cs
  10. 89 54
      Source/QuestPDF/Drawing/DocumentGenerator.cs
  11. 2 4
      Source/QuestPDF/Drawing/Exceptions/DocumentLayoutException.cs
  12. 2 0
      Source/QuestPDF/Drawing/FreeCanvas.cs
  13. 22 7
      Source/QuestPDF/Drawing/PreviewerCanvas.cs
  14. 0 14
      Source/QuestPDF/Drawing/Proxy/DebugStackItem.cs
  15. 1 24
      Source/QuestPDF/Drawing/Proxy/DebuggingProxy.cs
  16. 0 133
      Source/QuestPDF/Drawing/Proxy/DebuggingState.cs
  17. 84 0
      Source/QuestPDF/Drawing/Proxy/Helpers.cs
  18. 46 0
      Source/QuestPDF/Drawing/Proxy/TreeTraversal.cs
  19. 2 0
      Source/QuestPDF/Drawing/SkiaCanvasBase.cs
  20. 1 1
      Source/QuestPDF/Drawing/SpacePlan.cs
  21. 44 0
      Source/QuestPDF/Elements/LayoutOverflowPageMarker.cs
  22. 176 0
      Source/QuestPDF/Elements/LayoutOverflowVisualization.cs
  23. 2 0
      Source/QuestPDF/Infrastructure/IRenderingCanvas.cs
  24. 2 1
      Source/QuestPDF/Infrastructure/PageContext.cs
  25. 2 2
      Source/QuestPDF/Previewer/ExceptionDocument.cs
  26. 3 3
      Source/QuestPDF/Previewer/PreviewerExtensions.cs
  27. 1 0
      Source/QuestPDF/Previewer/PreviewerRefreshCommand.cs
  28. 4 3
      Source/QuestPDF/Previewer/PreviewerService.cs
  29. 5 11
      Source/QuestPDF/Settings.cs

+ 188 - 0
Source/QuestPDF.Examples/ContentOverflowVisualizationExamples.cs

@@ -0,0 +1,188 @@
+using System.Linq;
+using NUnit.Framework;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Examples;
+
+public class ContentOverflowVisualizationExamples
+{
+    [Test]
+    public void DrawOverflow()
+    {
+        RenderingTest
+            .Create()
+            .ShowResults()
+            .ProducePdf()
+            .EnableDebugging()
+            .RenderDocument(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Size(PageSizes.A4);
+                    page.Margin(24);
+                    page.DefaultTextStyle(TextStyle.Default.FontSize(16));
+
+                    page.Header().Text("Document header").FontSize(24).Bold().FontColor(Colors.Blue.Accent2);
+
+                    page.Content().PaddingVertical(24).Column(column =>
+                    {
+                        column.Spacing(16);
+
+                        foreach (var size in Enumerable.Range(20, 20))
+                            column.Item().Width(size * 10).Height(40).Background(Colors.Grey.Lighten3);
+                        
+                        column.Item().Row(row =>
+                        {
+                            row.RelativeItem().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Will it work?").FontSize(20);
+                            row.RelativeItem().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Height(100).ShowEntire().Text(Placeholders.LoremIpsum()).FontSize(20);
+                        });
+                        
+                        foreach (var size in Enumerable.Range(20, 20))
+                            column.Item().Width(size * 10).Height(40).Background(Colors.Grey.Lighten3);
+                    });
+                    
+                    page.Footer().AlignCenter().Text(text =>
+                    {
+                        text.CurrentPageNumber();
+                        text.Span(" / ");
+                        text.TotalPages();
+                    });
+                });
+            });
+    }
+    
+    [Test]
+    public void DrawOverflowRealExample()
+    {
+        var image = Placeholders.Image(400, 300);
+ 
+        RenderingTest
+            .Create()
+            .ShowResults()
+            .PageSize(PageSizes.A4)
+            .ProducePdf()
+            .EnableDebugging()
+            .Render(container =>
+            {
+                container.Column(column =>
+                {
+                    foreach (var i in Enumerable.Range(0, 50))
+                        column.Item().Height(30).Width(i * 5 + 100).Background(Placeholders.BackgroundColor());
+                   
+                    column.Item()
+                        .Padding(24)
+
+                        // constrain area to square 200 x 200
+                        .Width(200)
+                        .Height(200)
+                        .Background(Colors.Grey.Lighten3)
+
+                        // draw image that fits height (and therefore will overflow)
+                        //.ContentOverflowDebugArea()
+                        .Image(image)
+                        .FitHeight();
+                    
+                    foreach (var i in Enumerable.Range(0, 50))
+                        column.Item().Height(30).Width(i * 5 + 100).Background(Placeholders.BackgroundColor());
+                });
+            });
+    }
+    
+    [Test]
+    public void DrawOverflowSimpleExample()
+    {
+        var image = Placeholders.Image(400, 300);
+        
+        RenderingTest
+            .Create()
+            .ShowResults()
+            .PageSize(PageSizes.A4)
+            .EnableDebugging()
+            .ProducePdf()
+            .Render(container =>
+            {
+                container
+                    .Padding(24)
+
+                    // constrain area to square 200 x 200
+                    .Width(200)
+                    .Height(200)
+                    .Background(Colors.Grey.Lighten3)
+
+                    // draw image that fits height (and therefore will overflow)
+                    //.ContentOverflowDebugArea()
+                    .Image(image)
+                    .FitHeight();
+            });
+    }
+    
+    [Test]
+    public void DrawOverflowCases()
+    {
+        RenderingTest
+            .Create()
+            .ShowResults()
+            .PageSize(PageSizes.A4)
+            .EnableDebugging()
+            .ProducePdf()
+            .Render(container =>
+            {
+                container.Padding(24).Row(row =>
+                {
+                    row.Spacing(50);
+                    
+                    row.RelativeItem().ContentFromLeftToRight().Element(GenerateOverflowPatterns);
+                    row.RelativeItem().ContentFromRightToLeft().Element(GenerateOverflowPatterns);
+                });
+
+                void GenerateOverflowPatterns(IContainer container)
+                {
+                    container.Column(column =>
+                    {
+                        column.Spacing(50);
+
+                        column
+                            .Item()
+                            .Element(DrawTestcaseArea)
+                            
+                            .Width(50)
+                            .Height(150)
+                            
+                            .Text("Test");
+                        
+                        column
+                            .Item()
+                            .Element(DrawTestcaseArea)
+                        
+                            .Width(150)
+                            .Height(50)
+                            
+                            .Text("Test");
+                    
+                        column
+                            .Item()
+                            .Element(DrawTestcaseArea)
+
+                            .Width(200)
+                            .Height(150)
+                            
+                            .Text("Test");
+
+                        IContainer DrawTestcaseArea(IContainer container)
+                        {
+                            return container
+                                .Height(200)
+                                .Background(Colors.Grey.Lighten4)
+
+                                .Width(100)
+                                .Height(100)
+                                .Background(Colors.Grey.Lighten1);
+                        }
+                    });
+                }
+            });
+    }
+}

+ 8 - 13
Source/QuestPDF.Previewer/CommunicationService.cs

@@ -12,7 +12,7 @@ class CommunicationService
 {
 {
     public static CommunicationService Instance { get; } = new ();
     public static CommunicationService Instance { get; } = new ();
     
     
-    public event Action<ICollection<PreviewPage>>? OnDocumentRefreshed;
+    public event Action<DocumentSnapshot>? OnDocumentRefreshed;
 
 
     private WebApplication? Application { get; set; }
     private WebApplication? Application { get; set; }
 
 
@@ -60,20 +60,15 @@ class CommunicationService
     
     
     private async Task<IResult> HandleUpdatePreview(HttpRequest request)
     private async Task<IResult> HandleUpdatePreview(HttpRequest request)
     {
     {
-        var command = JsonSerializer.Deserialize<DocumentSnapshot>(request.Form["command"], JsonSerializerOptions);
+        var documentSnapshot = JsonSerializer.Deserialize<DocumentSnapshot>(request.Form["command"], JsonSerializerOptions);
 
 
-        var pages = command
-            .Pages
-            .Select(page =>
-            {
-                using var stream = request.Form.Files[page.Id].OpenReadStream();
-                var picture = SKPicture.Deserialize(stream);
-                        
-                return new PreviewPage(picture, page.Width, page.Height);
-            })
-            .ToList();
+        foreach (var pageSnapshot in documentSnapshot.Pages)
+        {
+            using var stream = request.Form.Files[pageSnapshot.Id].OpenReadStream();
+            pageSnapshot.Picture = SKPicture.Deserialize(stream);
+        }
 
 
-        Task.Run(() => OnDocumentRefreshed(pages));
+        Task.Run(() => OnDocumentRefreshed(documentSnapshot));
         return Results.Ok();
         return Results.Ok();
     }
     }
 }
 }

+ 1 - 0
Source/QuestPDF.Previewer/PreviewerRefreshCommand.cs → Source/QuestPDF.Previewer/DocumentSnapshot.cs

@@ -4,6 +4,7 @@ namespace QuestPDF.Previewer;
 
 
 internal sealed class DocumentSnapshot
 internal sealed class DocumentSnapshot
 {
 {
+    public bool DocumentContentHasLayoutOverflowIssues { get; set; }
     public ICollection<PageSnapshot> Pages { get; set; }
     public ICollection<PageSnapshot> Pages { get; set; }
 
 
     public class PageSnapshot
     public class PageSnapshot

+ 1 - 1
Source/QuestPDF.Previewer/Helpers.cs

@@ -4,7 +4,7 @@ namespace QuestPDF.Previewer;
 
 
 class Helpers
 class Helpers
 {
 {
-    public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<PreviewPage> pages)
+    public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<DocumentSnapshot.PageSnapshot> pages)
     {
     {
         using var stream = File.Create(filePath);
         using var stream = File.Create(filePath);
             
             

+ 10 - 2
Source/QuestPDF.Previewer/InteractiveCanvas.cs

@@ -9,7 +9,7 @@ namespace QuestPDF.Previewer;
 class InteractiveCanvas : ICustomDrawOperation
 class InteractiveCanvas : ICustomDrawOperation
 {
 {
     public Rect Bounds { get; set; }
     public Rect Bounds { get; set; }
-    public ICollection<PreviewPage> Pages { get; set; }
+    public ICollection<DocumentSnapshot.PageSnapshot> Pages { get; set; }
 
 
     private float Width => (float)Bounds.Width;
     private float Width => (float)Bounds.Width;
     private float Height => (float)Bounds.Height;
     private float Height => (float)Bounds.Height;
@@ -133,13 +133,21 @@ class InteractiveCanvas : ICustomDrawOperation
         {
         {
             canvas.Translate(-page.Width / 2f, 0);
             canvas.Translate(-page.Width / 2f, 0);
             DrawBlankPage(canvas, page.Width, page.Height);
             DrawBlankPage(canvas, page.Width, page.Height);
-            canvas.DrawPicture(page.Picture);
+            DrawPageSnapshot(canvas, page);
             canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
             canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
         }
         }
 
 
         canvas.SetMatrix(originalMatrix);
         canvas.SetMatrix(originalMatrix);
         DrawInnerGradient(canvas);
         DrawInnerGradient(canvas);
     }
     }
+
+    private static void DrawPageSnapshot(SKCanvas canvas, DocumentSnapshot.PageSnapshot pageSnapshot)
+    {
+        canvas.Save();
+        canvas.ClipRect(new SKRect(0, 0, pageSnapshot.Width, pageSnapshot.Height));
+        canvas.DrawPicture(pageSnapshot.Picture);
+        canvas.Restore();
+    }
     
     
     public void Dispose() { }
     public void Dispose() { }
     public bool Equals(ICustomDrawOperation? other) => false;
     public bool Equals(ICustomDrawOperation? other) => false;

+ 0 - 6
Source/QuestPDF.Previewer/PreviewPage.cs

@@ -1,6 +0,0 @@
-using SkiaSharp;
-
-namespace QuestPDF.Previewer
-{
-    record PreviewPage(SKPicture Picture, float Width, float Height);
-}

+ 3 - 3
Source/QuestPDF.Previewer/PreviewerControl.cs

@@ -10,10 +10,10 @@ namespace QuestPDF.Previewer
     {
     {
         private InteractiveCanvas InteractiveCanvas { get; set; } = new ();
         private InteractiveCanvas InteractiveCanvas { get; set; } = new ();
         
         
-        public static readonly StyledProperty<ObservableCollection<PreviewPage>> PagesProperty =
-            AvaloniaProperty.Register<PreviewerControl, ObservableCollection<PreviewPage>>(nameof(Pages));
+        public static readonly StyledProperty<ObservableCollection<DocumentSnapshot.PageSnapshot>> PagesProperty =
+            AvaloniaProperty.Register<PreviewerControl, ObservableCollection<DocumentSnapshot.PageSnapshot>>(nameof(Pages));
         
         
-        public ObservableCollection<PreviewPage>? Pages
+        public ObservableCollection<DocumentSnapshot.PageSnapshot>? Pages
         {
         {
             get => GetValue(PagesProperty);
             get => GetValue(PagesProperty);
             set => SetValue(PagesProperty, value);
             set => SetValue(PagesProperty, value);

+ 12 - 0
Source/QuestPDF.Previewer/PreviewerWindow.axaml

@@ -52,6 +52,18 @@
 			                            ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
 			                            ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
 			                            Pages="{Binding Pages, Mode=OneWay}" />
 			                            Pages="{Binding Pages, Mode=OneWay}" />
 			
 			
+			<Border IsVisible="{Binding DocumentContentHasLayoutOverflowIssues}" Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" HorizontalAlignment="Left" Padding="16,8" Background="#F44336" CornerRadius="8" BoxShadow="0 0 8 0 #44000000" Margin="32">
+				<StackPanel Orientation="Horizontal" Spacing="8">
+					<Viewbox Width="24" Height="24">
+						<Canvas Width="24" Height="24">
+							<Path Fill="White" Data="M20 17H22V15H20V17M20 7V13H22V7M6 16H11V18H6M6 12H14V14H6M4 2C2.89 2 2 2.89 2 4V20C2 21.11 2.89 22 4 22H16C17.11 22 18 21.11 18 20V8L12 2M4 4H11V9H16V20H4Z" />
+						</Canvas>
+					</Viewbox>
+					
+					<TextBlock VerticalAlignment="Center">Document has layout problems</TextBlock>
+				</StackPanel>
+			</Border>
+			
 			<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32">
 			<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32">
 				<Button Classes="actions"
 				<Button Classes="actions"
 				        Command="{Binding ShowPdfCommand, Mode=OneTime}"
 				        Command="{Binding ShowPdfCommand, Mode=OneTime}"

+ 12 - 4
Source/QuestPDF.Previewer/PreviewerWindowViewModel.cs

@@ -8,13 +8,20 @@ namespace QuestPDF.Previewer
 {
 {
     internal sealed class PreviewerWindowViewModel : ReactiveObject
     internal sealed class PreviewerWindowViewModel : ReactiveObject
     {
     {
-        private ObservableCollection<PreviewPage> _pages = new();
-        public ObservableCollection<PreviewPage> Pages
+        private ObservableCollection<DocumentSnapshot.PageSnapshot> _pages = new();
+        public ObservableCollection<DocumentSnapshot.PageSnapshot> Pages
         {
         {
             get => _pages;
             get => _pages;
             set => this.RaiseAndSetIfChanged(ref _pages, value);
             set => this.RaiseAndSetIfChanged(ref _pages, value);
         }
         }
         
         
+        private bool _documentContentHasLayoutOverflowIssues;
+        public bool DocumentContentHasLayoutOverflowIssues
+        {
+            get => _documentContentHasLayoutOverflowIssues;
+            set => this.RaiseAndSetIfChanged(ref _documentContentHasLayoutOverflowIssues, value);
+        }
+        
         private float _currentScroll;
         private float _currentScroll;
         public float CurrentScroll
         public float CurrentScroll
         {
         {
@@ -75,12 +82,13 @@ namespace QuestPDF.Previewer
             openBrowserProcess.Start();
             openBrowserProcess.Start();
         }
         }
         
         
-        private void HandleUpdatePreview(ICollection<PreviewPage> pages)
+        private void HandleUpdatePreview(DocumentSnapshot documentSnapshot)
         {
         {
             var oldPages = Pages;
             var oldPages = Pages;
             
             
             Pages.Clear();
             Pages.Clear();
-            Pages = new ObservableCollection<PreviewPage>(pages);
+            Pages = new ObservableCollection<DocumentSnapshot.PageSnapshot>(documentSnapshot.Pages);
+            DocumentContentHasLayoutOverflowIssues = documentSnapshot.DocumentContentHasLayoutOverflowIssues;
             
             
             foreach (var page in oldPages)
             foreach (var page in oldPages)
                 page.Picture.Dispose();
                 page.Picture.Dispose();

+ 89 - 54
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
@@ -10,6 +11,7 @@ using QuestPDF.Elements.Text.Items;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using QuestPDF.Previewer;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
@@ -74,7 +76,7 @@ namespace QuestPDF.Drawing
                 $"The library does not require any license key. We trust our users, and therefore the process is simple. " +
                 $"The library does not require any license key. We trust our users, and therefore the process is simple. " +
                 $"To disable license validation and turn off this exception, please configure an eligible license using the QuestPDF.Settings.License API, for example: {newParagraph}" +
                 $"To disable license validation and turn off this exception, please configure an eligible license using the QuestPDF.Settings.License API, for example: {newParagraph}" +
                 $"\"QuestPDF.Settings.License = LicenseType.Community;\" {newParagraph}" +
                 $"\"QuestPDF.Settings.License = LicenseType.Community;\" {newParagraph}" +
-                $"Learn more on: https://www.questpdf.com/license/configuration.html {newParagraph}";
+                $"Learn more on: https://www.questpdf.com/license-configuration.html {newParagraph}";
             
             
             throw new Exception(exceptionMessage)
             throw new Exception(exceptionMessage)
             {
             {
@@ -82,11 +84,11 @@ namespace QuestPDF.Drawing
             };
             };
         }
         }
 
 
-        internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
+        internal static PreviewerDocumentSnapshot GeneratePreviewerContent(IDocument document)
         {
         {
-            var canvas = new SkiaPictureCanvas();
+            var canvas = new PreviewerCanvas();
             RenderDocument(canvas, document, DocumentSettings.Default);
             RenderDocument(canvas, document, DocumentSettings.Default);
-            return canvas.Pictures;
+            return canvas.GetContent();
         }
         }
         
         
         private static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentSettings settings) where TCanvas : ICanvas, IRenderingCanvas
         private static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentSettings settings) where TCanvas : ICanvas, IRenderingCanvas
@@ -105,23 +107,19 @@ namespace QuestPDF.Drawing
         private static void RenderSingleDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentSettings settings)
         private static void RenderSingleDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentSettings settings)
             where TCanvas : ICanvas, IRenderingCanvas
             where TCanvas : ICanvas, IRenderingCanvas
         {
         {
-            const int documentId = 0;
-            
-            var debuggingState = new DebuggingState();
             var useOriginalImages = canvas is ImageCanvas;
             var useOriginalImages = canvas is ImageCanvas;
 
 
-            var content = ConfigureContent(document, settings, debuggingState, documentId, useOriginalImages);
+            var content = ConfigureContent(document, settings, useOriginalImages);
 
 
             var pageContext = new PageContext();
             var pageContext = new PageContext();
-            RenderPass(pageContext, new FreeCanvas(), content, debuggingState);
+            RenderPass(pageContext, new FreeCanvas(), content);
             pageContext.ResetPageNumber();
             pageContext.ResetPageNumber();
-            RenderPass(pageContext, canvas, content, debuggingState);
+            RenderPass(pageContext, canvas, content);
         }
         }
         
         
         private static void RenderMergedDocument<TCanvas>(TCanvas canvas, MergedDocument document, DocumentSettings settings)
         private static void RenderMergedDocument<TCanvas>(TCanvas canvas, MergedDocument document, DocumentSettings settings)
             where TCanvas : ICanvas, IRenderingCanvas
             where TCanvas : ICanvas, IRenderingCanvas
         {
         {
-            var debuggingState = new DebuggingState();
             var useOriginalImages = canvas is ImageCanvas;
             var useOriginalImages = canvas is ImageCanvas;
             
             
             var documentParts = Enumerable
             var documentParts = Enumerable
@@ -129,7 +127,7 @@ namespace QuestPDF.Drawing
                 .Select(index => new
                 .Select(index => new
                 {
                 {
                     DocumentId = index,
                     DocumentId = index,
-                    Content = ConfigureContent(document.Documents[index], settings, debuggingState, index, useOriginalImages)
+                    Content = ConfigureContent(document.Documents[index], settings, useOriginalImages)
                 })
                 })
                 .ToList();
                 .ToList();
 
 
@@ -140,7 +138,7 @@ namespace QuestPDF.Drawing
                 foreach (var documentPart in documentParts)
                 foreach (var documentPart in documentParts)
                 {
                 {
                     documentPageContext.SetDocumentId(documentPart.DocumentId);
                     documentPageContext.SetDocumentId(documentPart.DocumentId);
-                    RenderPass(documentPageContext, new FreeCanvas(), documentPart.Content, debuggingState);
+                    RenderPass(documentPageContext, new FreeCanvas(), documentPart.Content);
                 }
                 }
                 
                 
                 documentPageContext.ResetPageNumber();
                 documentPageContext.ResetPageNumber();
@@ -148,7 +146,7 @@ namespace QuestPDF.Drawing
                 foreach (var documentPart in documentParts)
                 foreach (var documentPart in documentParts)
                 {
                 {
                     documentPageContext.SetDocumentId(documentPart.DocumentId);
                     documentPageContext.SetDocumentId(documentPart.DocumentId);
-                    RenderPass(documentPageContext, canvas, documentPart.Content, debuggingState);   
+                    RenderPass(documentPageContext, canvas, documentPart.Content);   
                 }
                 }
             }
             }
             else
             else
@@ -158,14 +156,14 @@ namespace QuestPDF.Drawing
                     var pageContext = new PageContext();
                     var pageContext = new PageContext();
                     pageContext.SetDocumentId(documentPart.DocumentId);
                     pageContext.SetDocumentId(documentPart.DocumentId);
                     
                     
-                    RenderPass(pageContext, new FreeCanvas(), documentPart.Content, debuggingState);
+                    RenderPass(pageContext, new FreeCanvas(), documentPart.Content);
                     pageContext.ResetPageNumber();
                     pageContext.ResetPageNumber();
-                    RenderPass(pageContext, canvas, documentPart.Content, debuggingState);
+                    RenderPass(pageContext, canvas, documentPart.Content);
                 }
                 }
             }
             }
         }
         }
 
 
-        private static Container ConfigureContent(IDocument document, DocumentSettings settings, DebuggingState debuggingState, int documentIndex, bool useOriginalImages)
+        private static Container ConfigureContent(IDocument document, DocumentSettings settings, bool useOriginalImages)
         {
         {
             var container = new DocumentContainer();
             var container = new DocumentContainer();
             document.Compose(container);
             document.Compose(container);
@@ -178,28 +176,28 @@ namespace QuestPDF.Drawing
                     
                     
             if (Settings.EnableCaching)
             if (Settings.EnableCaching)
                 content.ApplyCaching();
                 content.ApplyCaching();
-
-            if (Settings.EnableDebugging)
-                content.ApplyDebugging(debuggingState);
-
+            
             return content;
             return content;
         }
         }
 
 
-        private static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DebuggingState? debuggingState)
+        private static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, ContainerElement content)
             where TCanvas : ICanvas, IRenderingCanvas
             where TCanvas : ICanvas, IRenderingCanvas
         {
         {
-            InjectDependencies(content, pageContext, canvas);
+            content.InjectDependencies(pageContext, canvas);
             content.VisitChildren(x => (x as IStateResettable)?.ResetState());
             content.VisitChildren(x => (x as IStateResettable)?.ResetState());
-            
+
             while(true)
             while(true)
             {
             {
-                pageContext.IncrementPageNumber();
-                debuggingState?.Reset();
-                
                 var spacePlan = content.Measure(Size.Max);
                 var spacePlan = content.Measure(Size.Max);
 
 
                 if (spacePlan.Type == SpacePlanType.Wrap)
                 if (spacePlan.Type == SpacePlanType.Wrap)
                 {
                 {
+                    if (Settings.EnableDebugging)
+                    {
+                        ApplyLayoutDebugging();
+                        continue;
+                    }
+                    
                     canvas.EndDocument();
                     canvas.EndDocument();
                     ThrowLayoutException();
                     ThrowLayoutException();
                 }
                 }
@@ -208,6 +206,7 @@ namespace QuestPDF.Drawing
                 {
                 {
                     canvas.BeginPage(spacePlan);
                     canvas.BeginPage(spacePlan);
                     content.Draw(spacePlan);
                     content.Draw(spacePlan);
+                    pageContext.IncrementPageNumber();
                 }
                 }
                 catch (Exception exception)
                 catch (Exception exception)
                 {
                 {
@@ -217,27 +216,74 @@ namespace QuestPDF.Drawing
 
 
                 canvas.EndPage();
                 canvas.EndPage();
 
 
-                if (pageContext.CurrentPage >= Settings.DocumentLayoutExceptionThreshold)
-                {
-                    canvas.EndDocument();
-                    ThrowLayoutException();
-                }
-                
                 if (spacePlan.Type == SpacePlanType.FullRender)
                 if (spacePlan.Type == SpacePlanType.FullRender)
                     break;
                     break;
             }
             }
 
 
-            void ThrowLayoutException()
+            if (Settings.EnableDebugging)
+            {
+                ConfigureLayoutOverflowMarker();
+                CheckIfDocumentHasLayoutOverflowIssues();
+            }
+
+            void ApplyLayoutDebugging()
+            {
+                content.RemoveExistingProxies();
+
+                content.ApplyLayoutOverflowDetection();
+                content.Measure(Size.Max);
+
+                var overflowState = content.ExtractElementsOfType<OverflowDebuggingProxy>().FirstOrDefault();
+                overflowState.ApplyLayoutOverflowVisualization();
+                
+                content.ApplyContentDirection();
+                content.InjectDependencies(pageContext, canvas);
+
+                content.RemoveExistingProxies();
+            }
+
+            void ConfigureLayoutOverflowMarker()
             {
             {
-                var message = $"Composed layout generates infinite document. This may happen in two cases. " +
-                              $"1) Your document and its layout configuration is correct but the content takes more than {Settings.DocumentLayoutExceptionThreshold} pages. " +
-                              $"In this case, please increase the value {nameof(QuestPDF)}.{nameof(Settings)}.{nameof(Settings.DocumentLayoutExceptionThreshold)} static property. " +
-                              $"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." +
-                              $"Please analyze your documents structure to detect this element and fix its size constraints.";
+                var layoutOverflowPageMarker = new LayoutOverflowPageMarker();
+                    
+                content.CreateProxy(child =>
+                {
+                    layoutOverflowPageMarker.Child = child;
+                    return layoutOverflowPageMarker;
+                });
 
 
-                var elementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode.";
+                var pageNumbersWithLayoutIssues = content
+                    .ExtractElementsOfType<LayoutOverflowVisualization>()
+                    .SelectMany(x => x.Flatten())
+                    .SelectMany(x => x.Value.VisibleOnPageNumbers)
+                    .Distinct();
 
 
-                throw new DocumentLayoutException(message, elementTrace);
+                layoutOverflowPageMarker.PageNumbersWithLayoutIssues = new HashSet<int>(pageNumbersWithLayoutIssues);
+            }
+
+            void CheckIfDocumentHasLayoutOverflowIssues()
+            {
+                var hasLayoutOverflowVisualizationElements = content
+                    .ExtractElementsOfType<LayoutOverflowVisualization>()
+                    .SelectMany(x => x.Flatten())
+                    .Any();
+
+                canvas.DocumentContentHasLayoutOverflowIssues |= hasLayoutOverflowVisualizationElements;
+            }
+            
+            void ThrowLayoutException()
+            {
+                var newLine = Environment.NewLine;
+                var newParagraph = newLine + newLine;
+                    
+                var message =
+                    $"The provided document content contains conflicting size constraints. " +
+                    $"For example, some elements may require more space than is available. {newParagraph}" +
+                    $"To quickly determine the place where the problem is likely occurring, please generate the document with the attached debugger. " +
+                    $"The library will generate a PDF document with visually annotated places where layout constraints are invalid. {newParagraph}" +
+                    $"Alternatively, if you don’t want to or cannot attach the debugger, you can set the {nameof(QuestPDF)}.{nameof(Settings)}.{nameof(Settings.EnableDebugging)} flag to true.";
+                
+                throw new DocumentLayoutException(message);
             }
             }
         }
         }
 
 
@@ -262,18 +308,7 @@ namespace QuestPDF.Drawing
             });
             });
         }
         }
 
 
-        private static void ApplyDebugging(this Container content, DebuggingState? debuggingState)
-        {
-            if (debuggingState == null)
-                return;
-            
-            content.VisitChildren(x =>
-            {
-                x.CreateProxy(y => new DebuggingProxy(debuggingState, y));
-            });
-        }
-        
-        internal static void ApplyContentDirection(this Element? content, ContentDirection direction)
+        internal static void ApplyContentDirection(this Element? content, ContentDirection? direction = null)
         {
         {
             if (content == null)
             if (content == null)
                 return;
                 return;
@@ -285,7 +320,7 @@ namespace QuestPDF.Drawing
             }
             }
 
 
             if (content is IContentDirectionAware contentDirectionAware)
             if (content is IContentDirectionAware contentDirectionAware)
-                contentDirectionAware.ContentDirection = direction;
+                contentDirectionAware.ContentDirection = direction ?? contentDirectionAware.ContentDirection;
             
             
             foreach (var child in content.GetChildren())
             foreach (var child in content.GetChildren())
                 ApplyContentDirection(child, direction);
                 ApplyContentDirection(child, direction);

+ 2 - 4
Source/QuestPDF/Drawing/Exceptions/DocumentLayoutException.cs

@@ -4,11 +4,9 @@ namespace QuestPDF.Drawing.Exceptions
 {
 {
     public class DocumentLayoutException : Exception
     public class DocumentLayoutException : Exception
     {
     {
-        public string? ElementTrace { get; }
-
-        internal DocumentLayoutException(string message, string? elementTrace = null) : base(message)
+        internal DocumentLayoutException(string message) : base(message)
         {
         {
-            ElementTrace = elementTrace;
+            
         }
         }
     }
     }
 }
 }

+ 2 - 0
Source/QuestPDF/Drawing/FreeCanvas.cs

@@ -7,6 +7,8 @@ namespace QuestPDF.Drawing
     {
     {
         #region IRenderingCanvas
         #region IRenderingCanvas
 
 
+        public bool DocumentContentHasLayoutOverflowIssues { get; set; }
+        
         public void BeginDocument()
         public void BeginDocument()
         {
         {
             
             

+ 22 - 7
Source/QuestPDF/Drawing/SkiaPictureCanvas.cs → Source/QuestPDF/Drawing/PreviewerCanvas.cs

@@ -4,28 +4,34 @@ using SkiaSharp;
 
 
 namespace QuestPDF.Drawing
 namespace QuestPDF.Drawing
 {
 {
-    internal sealed class PreviewerPicture
+    internal class PreviewerPageSnapshot
     {
     {
         public SKPicture Picture { get; set; }
         public SKPicture Picture { get; set; }
         public Size Size { get; set; }
         public Size Size { get; set; }
 
 
-        public PreviewerPicture(SKPicture picture, Size size)
+        public PreviewerPageSnapshot(SKPicture picture, Size size)
         {
         {
             Picture = picture;
             Picture = picture;
             Size = size;
             Size = size;
         }
         }
     }
     }
-
-    internal sealed class SkiaPictureCanvas : SkiaCanvasBase
+    
+    internal class PreviewerDocumentSnapshot
+    {
+        public ICollection<PreviewerPageSnapshot> Pictures { get; set; }
+        public bool DocumentContentHasLayoutOverflowIssues { get; set; }
+    }
+    
+    internal class PreviewerCanvas : SkiaCanvasBase
     {
     {
         private SKPictureRecorder? PictureRecorder { get; set; }
         private SKPictureRecorder? PictureRecorder { get; set; }
         private Size? CurrentPageSize { get; set; }
         private Size? CurrentPageSize { get; set; }
 
 
-        public ICollection<PreviewerPicture> Pictures { get; } = new List<PreviewerPicture>();
+        private ICollection<PreviewerPageSnapshot> PageSnapshots { get; } = new List<PreviewerPageSnapshot>();
         
         
         public override void BeginDocument()
         public override void BeginDocument()
         {
         {
-            Pictures.Clear();
+            PageSnapshots.Clear();
         }
         }
 
 
         public override void BeginPage(Size size)
         public override void BeginPage(Size size)
@@ -41,12 +47,21 @@ namespace QuestPDF.Drawing
             var picture = PictureRecorder?.EndRecording();
             var picture = PictureRecorder?.EndRecording();
             
             
             if (picture != null && CurrentPageSize.HasValue)
             if (picture != null && CurrentPageSize.HasValue)
-                Pictures.Add(new PreviewerPicture(picture, CurrentPageSize.Value));
+                PageSnapshots.Add(new PreviewerPageSnapshot(picture, CurrentPageSize.Value));
 
 
             PictureRecorder?.Dispose();
             PictureRecorder?.Dispose();
             PictureRecorder = null;
             PictureRecorder = null;
         }
         }
 
 
         public override void EndDocument() { }
         public override void EndDocument() { }
+
+        public PreviewerDocumentSnapshot GetContent()
+        {
+            return new PreviewerDocumentSnapshot
+            {
+                Pictures = PageSnapshots,
+                DocumentContentHasLayoutOverflowIssues = DocumentContentHasLayoutOverflowIssues,
+            };
+        }
     }
     }
 }
 }

+ 0 - 14
Source/QuestPDF/Drawing/Proxy/DebugStackItem.cs

@@ -1,14 +0,0 @@
-using System.Collections.Generic;
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.Drawing.Proxy
-{
-    internal sealed class DebugStackItem
-    {
-        public IElement Element { get; internal set; }
-        public Size AvailableSpace { get; internal set; }
-        public SpacePlan SpacePlan { get; internal set; }
-
-        public ICollection<DebugStackItem> Stack { get; internal set; } = new List<DebugStackItem>();
-    }
-}

+ 1 - 24
Source/QuestPDF/Drawing/Proxy/DebuggingProxy.cs

@@ -1,24 +1 @@
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.Drawing.Proxy
-{
-    internal sealed class DebuggingProxy : ElementProxy
-    {
-        private DebuggingState DebuggingState { get; }
-
-        public DebuggingProxy(DebuggingState debuggingState, Element child)
-        {
-            DebuggingState = debuggingState;
-            Child = child;
-        }
-        
-        internal override SpacePlan Measure(Size availableSpace)
-        {
-            DebuggingState.RegisterMeasure(Child, availableSpace);
-            var spacePlan = base.Measure(availableSpace);
-            DebuggingState.RegisterMeasureResult(Child, spacePlan);
-
-            return spacePlan;
-        }
-    }
-}
+

+ 0 - 133
Source/QuestPDF/Drawing/Proxy/DebuggingState.cs

@@ -1,133 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using QuestPDF.Elements;
-using QuestPDF.Helpers;
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.Drawing.Proxy
-{
-    internal sealed class DebuggingState
-    {
-        private DebugStackItem? Root { get; set; }
-        private Stack<DebugStackItem> Stack { get; set; }
-
-        public DebuggingState()
-        {
-            Reset();
-        }
-        
-        public void Reset()
-        {
-            Root = null;
-            Stack = new Stack<DebugStackItem>();
-        }
-        
-        public void RegisterMeasure(IElement element, Size availableSpace)
-        {
-            if (element.GetType() == typeof(Container))
-                return;
-            
-            var item = new DebugStackItem
-            {
-                Element = element,
-                AvailableSpace = availableSpace
-            };
-
-            Root ??= item;
-            
-            if (Stack.Any())
-                Stack.Peek().Stack.Add(item);
-
-            Stack.Push(item);
-        }
-
-        public void RegisterMeasureResult(IElement element, SpacePlan spacePlan)
-        {
-            if (element.GetType() == typeof(Container))
-                return;
-            
-            var item = Stack.Pop();
-
-            if (item.Element != element)
-                throw new Exception();
-            
-            item.SpacePlan = spacePlan;
-        }
-        
-        public string BuildTrace()
-        {
-            if (Root == null)
-                return null;
-            
-            var builder = new StringBuilder();
-            var nestingLevel = 0;
-
-            Traverse(Root);
-            return builder.ToString();
-
-            void Traverse(DebugStackItem item)
-            {
-                var indent = new string(' ', nestingLevel * 4);
-                var title = item.Element.GetType().Name;
-
-                if (item.Element is DebugPointer debugPointer)
-                {
-                    title = debugPointer.Target;
-                    title += debugPointer.Highlight ? " 🌟" : string.Empty;
-                }
-                
-                if (item.SpacePlan.Type != SpacePlanType.FullRender)
-                    title = "🔥 " + title;
-
-                builder.AppendLine(indent + title);
-                builder.AppendLine(indent + new string('-', title.Length));
-                
-                builder.AppendLine(indent + "Available space: " + item.AvailableSpace);
-                builder.AppendLine(indent + "Requested space: " + item.SpacePlan);
-                
-                foreach (var configuration in GetElementConfiguration(item.Element))
-                    builder.AppendLine(indent + configuration);
-
-                builder.AppendLine();
-                
-                nestingLevel++;
-                item.Stack.ToList().ForEach(Traverse);
-                nestingLevel--;
-            }
-
-            static IEnumerable<string> GetElementConfiguration(IElement element)
-            {
-                if (element is DebugPointer)
-                    return Enumerable.Empty<string>();
-                
-                return element
-                    .GetType()
-                    .GetProperties()
-                    .Select(x => new
-                    {
-                        Property = x.Name.PrettifyName(),
-                        Value = x.GetValue(element)
-                    })
-                    .Where(x => !(x.Value is IElement))
-                    .Where(x => x.Value is string || !(x.Value is IEnumerable))
-                    .Where(x => !(x.Value is TextStyle))
-                    .Select(x => $"{x.Property}: {FormatValue(x.Value)}");
-
-                string FormatValue(object value)
-                {
-                    const int maxLength = 100;
-                    
-                    var text = value?.ToString() ?? "-";
-
-                    if (text.Length < maxLength)
-                        return text;
-
-                    return text.AsSpan(0, maxLength).ToString() + "...";
-                }
-            }
-        }
-    }
-}

+ 84 - 0
Source/QuestPDF/Drawing/Proxy/Helpers.cs

@@ -0,0 +1,84 @@
+using System.Linq;
+using System.Text;
+using QuestPDF.Elements;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Drawing.Proxy;
+
+internal static class Helpers
+{
+    public static void ApplyLayoutOverflowDetection(this Element container)
+    {
+        container.VisitChildren(x =>
+        {
+            x.CreateProxy(y => y is ElementProxy ? y : new OverflowDebuggingProxy(y));
+        });
+    }
+    
+    public static void ApplyLayoutOverflowVisualization(this TreeNode<OverflowDebuggingProxy> hierarchyRoot)
+    {
+        Traverse(hierarchyRoot);
+        
+        void Traverse(TreeNode<OverflowDebuggingProxy> parent)
+        {
+            if (parent.Value.SpacePlanType == null)
+                return;
+            
+            if (parent.Value.SpacePlanType == SpacePlanType.FullRender)
+                return;
+            
+            var childrenWithWraps = parent.Children.Where(x => x.Value.SpacePlanType is SpacePlanType.Wrap).ToList();
+            var childrenWithPartialRenders = parent.Children.Where(x => x.Value.SpacePlanType is SpacePlanType.PartialRender).ToList();
+
+            if (childrenWithWraps.Any())
+            {
+                childrenWithWraps.ForEach(Traverse);
+            }
+            else if (childrenWithPartialRenders.Any())
+            {
+                childrenWithPartialRenders.ForEach(Traverse);
+            }
+            else
+            {
+                parent.Value.CreateProxy(x => new LayoutOverflowVisualization { Child = x });
+            }
+        }
+    }
+
+    public static void RemoveExistingProxies(this Element content)
+    {
+        content.VisitChildren(x =>
+        {
+            x.CreateProxy(y => y is ElementProxy proxy ? proxy.Child : y);
+        });
+    }
+
+    public static string HierarchyToString(this Element root)
+    {
+        var indentationCache = Enumerable.Range(0, 128).Select(x => new string(' ', x)).ToArray();
+        
+        var indentationLevel = 0;
+        var result = new StringBuilder();
+        
+        Traverse(root);
+
+        return result.ToString();
+        
+        void Traverse(Element parent)
+        {
+            var elementName = (parent as DebugPointer)?.Target ?? parent.GetType().Name;
+            
+            result.AppendLine();
+            result.Append(indentationCache[indentationLevel]);
+            result.Append(elementName);
+
+            indentationLevel++;
+            
+            foreach (var child in parent.GetChildren())
+                Traverse(child);
+
+            indentationLevel--;
+        }
+    }
+}

+ 46 - 0
Source/QuestPDF/Drawing/Proxy/TreeTraversal.cs

@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Linq;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Drawing.Proxy;
+
+internal class TreeNode<T>
+{
+    public T Value { get; }
+    public ICollection<TreeNode<T>> Children { get; } = new List<TreeNode<T>>();
+    
+    public TreeNode(T Value)
+    {
+        this.Value = Value;
+    }
+}
+
+internal static class TreeTraversal
+{
+    public static IEnumerable<TreeNode<T>> ExtractElementsOfType<T>(this Element element) where T : Element
+    {
+        if (element is T proxy)
+        {
+            var result = new TreeNode<T>(proxy);
+                
+            foreach (var treeNode in proxy.GetChildren().SelectMany(ExtractElementsOfType<T>))
+                result.Children.Add(treeNode);
+                
+            yield return result;
+        }
+        else
+        {
+            foreach (var treeNode in element.GetChildren().SelectMany(ExtractElementsOfType<T>))
+                yield return treeNode;
+        }
+    }
+    
+    public static IEnumerable<TreeNode<T>> Flatten<T>(this TreeNode<T> element) where T : Element
+    {
+        yield return element;
+
+        foreach (var child in element.Children)
+            foreach (var innerChild in Flatten(child))
+                yield return innerChild;
+    }
+}

+ 2 - 0
Source/QuestPDF/Drawing/SkiaCanvasBase.cs

@@ -8,6 +8,8 @@ namespace QuestPDF.Drawing
     {
     {
         internal SKCanvas Canvas { get; set; }
         internal SKCanvas Canvas { get; set; }
 
 
+        public bool DocumentContentHasLayoutOverflowIssues { get; set; }
+        
         public abstract void BeginDocument();
         public abstract void BeginDocument();
         public abstract void EndDocument();
         public abstract void EndDocument();
         
         

+ 1 - 1
Source/QuestPDF/Drawing/SpacePlan.cs

@@ -16,7 +16,7 @@ namespace QuestPDF.Drawing
             Height = height;
             Height = height;
         }
         }
 
 
-        internal static SpacePlan Wrap() => new SpacePlan(SpacePlanType.Wrap, 0, 0);
+        internal static SpacePlan  Wrap() => new SpacePlan(SpacePlanType.Wrap, 0, 0);
         
         
         internal static SpacePlan PartialRender(float width, float height) => new SpacePlan(SpacePlanType.PartialRender, width, height);
         internal static SpacePlan PartialRender(float width, float height) => new SpacePlan(SpacePlanType.PartialRender, width, height);
 
 

+ 44 - 0
Source/QuestPDF/Elements/LayoutOverflowPageMarker.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+using SkiaSharp;
+
+namespace QuestPDF.Elements;
+
+internal class LayoutOverflowPageMarker : ContainerElement
+{
+    public HashSet<int> PageNumbersWithLayoutIssues { get; set; } = new();
+    
+    private const string LineColor = Colors.Red.Medium;
+    private const byte LineOpacity = 64;
+    private const float BorderThickness = 24f;
+
+    internal override void Draw(Size availableSpace)
+    {
+        Child?.Draw(availableSpace);
+        
+        if (!PageNumbersWithLayoutIssues.Contains(PageContext.CurrentPage))
+            return;
+
+        DrawPageIndication(availableSpace);
+    }
+
+    private void DrawPageIndication(Size availableSpace)
+    {
+        if (Canvas is not SkiaCanvasBase canvasBase)
+            return;
+
+        using var indicatorPaint = new SKPaint
+        {
+            StrokeWidth = BorderThickness * 2, // half of the width will be outside of the page area
+            Color = SKColor.Parse(LineColor).WithAlpha(LineOpacity),
+            IsStroke = true
+        };
+        
+        var skiaCanvas = canvasBase.Canvas;
+        skiaCanvas.DrawRect(0, 0, availableSpace.Width, availableSpace.Height, indicatorPaint);
+    }
+}

+ 176 - 0
Source/QuestPDF/Elements/LayoutOverflowVisualization.cs

@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using QuestPDF.Drawing;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+using SkiaSharp;
+
+namespace QuestPDF.Elements;
+
+internal class LayoutOverflowVisualization : ContainerElement, IContentDirectionAware
+{
+    private const float BorderThickness = 1.5f;
+    private const float StripeThickness = 1.5f;
+    private const float StripeScale = 6f;
+    private const string LineColor = Colors.Red.Medium;
+    private const string AvailableAreaColor = Colors.Green.Medium;
+    private const string OverflowAreaColor = Colors.Red.Medium;
+    private const byte AreaOpacity = 64;
+
+    public ContentDirection ContentDirection { get; set; }
+    public ICollection<int> VisibleOnPageNumbers { get; set; } = new List<int>();
+
+    internal override SpacePlan Measure(Size availableSpace)
+    {
+        var childSize = base.Measure(availableSpace);
+        
+        if (childSize.Type == SpacePlanType.FullRender)
+            return childSize;
+        
+        return SpacePlan.FullRender(availableSpace);
+    }
+        
+    internal override void Draw(Size availableSpace)
+    {
+        VisibleOnPageNumbers.Add(PageContext.CurrentPage);
+        
+        // measure content area
+        var childSize = base.Measure(availableSpace);
+        
+        if (childSize.Type == SpacePlanType.FullRender)
+        {
+            Child?.Draw(availableSpace);
+            return;
+        }
+        
+        // check overflow area
+        var contentSize = 
+            TryVerticalOverflow(availableSpace) 
+            ?? TryHorizontalOverflow(availableSpace) 
+            ?? TryUnconstrainedOverflow() 
+            ?? Size.Max;
+        
+        // draw content
+        var translate = ContentDirection == ContentDirection.RightToLeft
+            ? new Position(availableSpace.Width - contentSize.Width, 0)
+            : Position.Zero;
+        
+        Canvas.Translate(translate);
+        Child?.Draw(contentSize);
+        Canvas.Translate(translate.Reverse());
+        
+        // draw overflow area
+        var overflowTranslate = ContentDirection == ContentDirection.RightToLeft ? new Position(availableSpace.Width, 0) : Position.Zero;
+        var overflowScale = ContentDirection == ContentDirection.RightToLeft ? -1 : 1;
+        
+        Canvas.Translate(overflowTranslate);
+        Canvas.Scale(overflowScale, 1);
+        
+        DrawOverflowArea(availableSpace, contentSize);
+        
+        Canvas.Scale(overflowScale, 1);
+        Canvas.Translate(overflowTranslate.Reverse());
+    }
+
+    private Size? TryOverflow(Size targetSpace)
+    {
+        var contentSize = base.Measure(targetSpace);
+        return contentSize.Type == SpacePlanType.Wrap ? null : contentSize;
+    }
+    
+    private Size? TryVerticalOverflow(Size availableSpace)
+    {
+        var overflowSpace = new Size(availableSpace.Width, Size.Infinity);
+        return TryOverflow(overflowSpace);
+    }
+    
+    private Size? TryHorizontalOverflow(Size availableSpace)
+    {
+        var overflowSpace = new Size(Size.Infinity, availableSpace.Height);
+        return TryOverflow(overflowSpace);
+    }
+    
+    private Size? TryUnconstrainedOverflow()
+    {
+        var overflowSpace = new Size(Size.Infinity, Size.Infinity);
+        return TryOverflow(overflowSpace);
+    }
+    
+    private void DrawOverflowArea(Size availableSpace, Size contentSize)
+    {
+        if (Canvas is not SkiaCanvasBase canvasBase)
+            return;
+        
+        var skiaCanvas = canvasBase.Canvas;
+
+        DrawAvailableSpaceBackground();
+
+        skiaCanvas.Save();
+        ClipOverflowAreaVisibility();
+        DrawOverflowArea();
+        DrawCheckerboardPattern();
+        skiaCanvas.Restore();
+
+        DrawContentAreaBorder();
+
+        void DrawAvailableSpaceBackground()
+        {
+            using var paint = new SKPaint
+            {
+                Color = SKColor.Parse(AvailableAreaColor).WithAlpha(AreaOpacity)
+            };
+        
+            skiaCanvas.DrawRect(0, 0, availableSpace.Width, availableSpace.Height, paint);
+        }
+        
+        void DrawContentAreaBorder()
+        {
+            using var borderPaint = new SKPaint
+            {
+                Color = SKColor.Parse(LineColor),
+                IsStroke = true,
+                StrokeWidth = BorderThickness
+            };
+
+            skiaCanvas.DrawRect(0, 0, contentSize.Width, contentSize.Height, borderPaint);
+        }
+        
+        void DrawOverflowArea()
+        {
+            using var areaPaint = new SKPaint
+            {
+                Color = SKColor.Parse(OverflowAreaColor).WithAlpha(AreaOpacity)
+            };
+
+            skiaCanvas.DrawRect(0, 0, contentSize.Width, contentSize.Height, areaPaint);
+        }
+        
+        void DrawCheckerboardPattern()
+        {
+            var matrix = SKMatrix.CreateScale(StripeScale, StripeScale).PostConcat(SKMatrix.CreateRotation((float)(Math.PI / 4)));
+
+            using var paint = new SKPaint
+            {
+                Color = SKColor.Parse(LineColor),
+                PathEffect = SKPathEffect.Create2DLine(StripeThickness, matrix),
+                IsAntialias = true
+            };
+            
+            var targetArea = new SKRect(0,0,contentSize.Width, contentSize.Height);
+            targetArea.Inflate(StripeScale * 2, StripeScale * 2);
+            
+            skiaCanvas.DrawRect(targetArea, paint);
+        }
+
+        void ClipOverflowAreaVisibility()
+        {
+            var path = new SKPath();
+
+            path.AddRect(new SKRect(0, 0, contentSize.Width, contentSize.Height), SKPathDirection.Clockwise);
+            path.AddRect(new SKRect(0, 0, Math.Min(availableSpace.Width, contentSize.Width), Math.Min(availableSpace.Height, contentSize.Height)), SKPathDirection.CounterClockwise);
+
+            skiaCanvas.Save();
+            skiaCanvas.ClipPath(path);
+        }
+    }
+}

+ 2 - 0
Source/QuestPDF/Infrastructure/IRenderingCanvas.cs

@@ -2,6 +2,8 @@
 {
 {
     internal interface IRenderingCanvas
     internal interface IRenderingCanvas
     {
     {
+        bool DocumentContentHasLayoutOverflowIssues { get; set; }
+        
         void BeginDocument();
         void BeginDocument();
         void EndDocument();
         void EndDocument();
         
         

+ 2 - 1
Source/QuestPDF/Infrastructure/PageContext.cs

@@ -14,11 +14,12 @@ namespace QuestPDF.Infrastructure
         internal void SetDocumentId(int id)
         internal void SetDocumentId(int id)
         {
         {
             CurrentDocumentId = id;
             CurrentDocumentId = id;
+            ResetPageNumber();
         }
         }
         
         
         internal void ResetPageNumber()
         internal void ResetPageNumber()
         {
         {
-            CurrentPage = 0;
+            CurrentPage = 1;
         }
         }
         
         
         internal void IncrementPageNumber()
         internal void IncrementPageNumber()

+ 2 - 2
Source/QuestPDF/Previewer/ExceptionDocument.cs

@@ -33,7 +33,7 @@ namespace QuestPDF.Previewer
                 page.Margin(50);
                 page.Margin(50);
                 page.DefaultTextStyle(x => x.FontSize(16));
                 page.DefaultTextStyle(x => x.FontSize(16));
 
 
-                page.Foreground().PaddingTop(5).Border(10).BorderColor(Colors.Red.Medium);
+                page.Foreground().Border(12).BorderColor(Colors.Red.Medium);
                 
                 
                 page.Header()
                 page.Header()
                     .ShowOnce()
                     .ShowOnce()
@@ -65,7 +65,7 @@ namespace QuestPDF.Previewer
                             .Column(column =>
                             .Column(column =>
                             {
                             {
                                 column.Item().Text("Exception").FontSize(36).FontColor(Colors.Red.Medium).Bold();
                                 column.Item().Text("Exception").FontSize(36).FontColor(Colors.Red.Medium).Bold();
-                                column.Item().PaddingTop(-10).Text("Please refer to the details for further insight and troubleshooting.").FontSize(18).FontColor(Colors.Red.Medium).Bold();
+                                column.Item().PaddingTop(-5).Text("Please refer to the details for further insight and troubleshooting.").FontSize(18).FontColor(Colors.Red.Medium).Bold();
                             }); 
                             }); 
                     });
                     });
 
 

+ 3 - 3
Source/QuestPDF/Previewer/PreviewerExtensions.cs

@@ -39,16 +39,16 @@ namespace QuestPDF.Previewer
                 var pictures = GetPictures();
                 var pictures = GetPictures();
                 return previewerService.RefreshPreview(pictures);
                 return previewerService.RefreshPreview(pictures);
                 
                 
-                ICollection<PreviewerPicture> GetPictures()
+                PreviewerDocumentSnapshot GetPictures()
                 {
                 {
                     try
                     try
                     {
                     {
-                        return DocumentGenerator.GeneratePreviewerPictures(document);
+                        return DocumentGenerator.GeneratePreviewerContent(document);
                     }
                     }
                     catch (Exception exception)
                     catch (Exception exception)
                     {
                     {
                         var exceptionDocument = new ExceptionDocument(exception);
                         var exceptionDocument = new ExceptionDocument(exception);
-                        return DocumentGenerator.GeneratePreviewerPictures(exceptionDocument);
+                        return DocumentGenerator.GeneratePreviewerContent(exceptionDocument);
                     }
                     }
                 }
                 }
             }
             }

+ 1 - 0
Source/QuestPDF/Previewer/PreviewerRefreshCommand.cs

@@ -8,6 +8,7 @@ namespace QuestPDF.Previewer
 {
 {
     internal class PreviewerRefreshCommand
     internal class PreviewerRefreshCommand
     {
     {
+        public bool DocumentContentHasLayoutOverflowIssues { get; set; }
         public ICollection<Page> Pages { get; set; }
         public ICollection<Page> Pages { get; set; }
 
 
         public class Page
         public class Page

+ 4 - 3
Source/QuestPDF/Previewer/PreviewerService.cs

@@ -131,13 +131,13 @@ namespace QuestPDF.Previewer
             }
             }
         }
         }
         
         
-        public async Task RefreshPreview(ICollection<PreviewerPicture> pictures)
+        public async Task RefreshPreview(PreviewerDocumentSnapshot previewerDocumentSnapshot)
         {
         {
             using var multipartContent = new MultipartFormDataContent();
             using var multipartContent = new MultipartFormDataContent();
 
 
             var pages = new List<PreviewerRefreshCommand.Page>();
             var pages = new List<PreviewerRefreshCommand.Page>();
             
             
-            foreach (var picture in pictures)
+            foreach (var picture in previewerDocumentSnapshot.Pictures)
             {
             {
                 var page = new PreviewerRefreshCommand.Page
                 var page = new PreviewerRefreshCommand.Page
                 {
                 {
@@ -153,6 +153,7 @@ namespace QuestPDF.Previewer
 
 
             var command = new PreviewerRefreshCommand
             var command = new PreviewerRefreshCommand
             {
             {
+                DocumentContentHasLayoutOverflowIssues = previewerDocumentSnapshot.DocumentContentHasLayoutOverflowIssues,
                 Pages = pages
                 Pages = pages
             };
             };
             
             
@@ -160,7 +161,7 @@ namespace QuestPDF.Previewer
 
 
             using var _ = await HttpClient.PostAsync("/update/preview", multipartContent);
             using var _ = await HttpClient.PostAsync("/update/preview", multipartContent);
 
 
-            foreach (var picture in pictures)
+            foreach (var picture in previewerDocumentSnapshot.Pictures)
                 picture.Picture.Dispose();
                 picture.Picture.Dispose();
         }
         }
     }
     }

+ 5 - 11
Source/QuestPDF/Settings.cs

@@ -1,4 +1,5 @@
-using QuestPDF.Infrastructure;
+using System;
+using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF
 namespace QuestPDF
 {
 {
@@ -10,13 +11,7 @@ namespace QuestPDF
         /// </summary>
         /// </summary>
         public static LicenseType? License { get; set; }
         public static LicenseType? License { get; set; }
         
         
-        /// <summary>
-        /// This value represents the maximum number of pages that the library produces.
-        /// This is useful when layout constraints are too strong, e.g. one element does not fit in another.
-        /// In such cases, the library would produce document of infinite length, consuming all available resources.
-        /// To break the algorithm and save the environment, the library breaks the rendering process after reaching specified length of document.
-        /// If your content requires generating longer documents, please assign the most reasonable value.
-        /// </summary>
+        [Obsolete("This setting is ignored since the 2023.10 version. The new infinite layout detection algorithm works automatically. You can safely remove this setting from your codebase.")]
         public static int DocumentLayoutExceptionThreshold { get; set; } = 250;
         public static int DocumentLayoutExceptionThreshold { get; set; } = 250;
         
         
         /// <summary>
         /// <summary>
@@ -28,10 +23,9 @@ namespace QuestPDF
         
         
         /// <summary>
         /// <summary>
         /// This flag generates additional document elements to improve layout debugging experience.
         /// This flag generates additional document elements to improve layout debugging experience.
-        /// When the DocumentLayoutException is thrown, the library is able to provide additional execution context.
-        /// It includes layout calculation results and path to the problematic area.
+        /// When the provided content contains size constraints impossible to meet, the library generates special visual annotations to help determining the root cause.
         /// </summary>
         /// </summary>
-        /// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
+        /// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>  
         public static bool EnableDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
         public static bool EnableDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
         
         
         /// <summary>
         /// <summary>