From 7b70934164d9560d6764f5b11b0939ec6a592a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Fri, 13 Jun 2025 16:38:53 +0300 Subject: [PATCH] refactoring --- .../Docs/Projects/Pdf/IHtmlToPdfRenderer.cs | 2 +- .../Pdf/IText/ITextHtmlToPdfRenderer.cs | 33 ++- .../Docs/Projects/Pdf/ProjectPdfGenerator.cs | 256 +++++++++--------- 3 files changed, 158 insertions(+), 133 deletions(-) diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IHtmlToPdfRenderer.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IHtmlToPdfRenderer.cs index 56fdc2f995..cee1b96185 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IHtmlToPdfRenderer.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IHtmlToPdfRenderer.cs @@ -6,5 +6,5 @@ namespace Volo.Docs.Projects.Pdf; public interface IHtmlToPdfRenderer { - Task RenderAsync(string title, string html, List documents); + Stream Render(string title, string html, List documents); } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IText/ITextHtmlToPdfRenderer.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IText/ITextHtmlToPdfRenderer.cs index c0d609be9c..a5fa814dcb 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IText/ITextHtmlToPdfRenderer.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IText/ITextHtmlToPdfRenderer.cs @@ -21,15 +21,26 @@ public class ITextHtmlToPdfRenderer : IHtmlToPdfRenderer, ITransientDependency Options = options; } - public virtual Task RenderAsync(string title, string html, List documents) + public virtual Stream Render(string title, string html, List documents) { var pdfStream = new MemoryStream(); - var pdfWrite = new PdfWriter(pdfStream); - var pdfDocument = new iText.Kernel.Pdf.PdfDocument(pdfWrite); - pdfDocument.GetDocumentInfo().SetTitle(title); - var textDocument = new ITextDocument(pdfDocument); - pdfWrite.SetCloseStream(false); - + using (var pdfWriter = new PdfWriter(pdfStream)) + { + pdfWriter.SetCloseStream(false); + using (var pdfDocument = new iText.Kernel.Pdf.PdfDocument(pdfWriter)) + { + pdfDocument.GetDocumentInfo().SetTitle(title); + CreatePdfFromHtml(html, pdfDocument); + AddOutlinesToPdf(pdfDocument, documents); + } + } + + pdfStream.Position = 0; + return pdfStream; + } + + private void CreatePdfFromHtml(string html, iText.Kernel.Pdf.PdfDocument pdfDocument) + { var converter = new ConverterProperties(); var tagWorkerFactory = new HtmlIdTagWorkerFactory(pdfDocument); converter.SetTagWorkerFactory(tagWorkerFactory); @@ -37,12 +48,12 @@ public class ITextHtmlToPdfRenderer : IHtmlToPdfRenderer, ITransientDependency HtmlConverter.ConvertToDocument(html, pdfDocument, converter); tagWorkerFactory.AddNamedDestinations(); + } + + private void AddOutlinesToPdf(iText.Kernel.Pdf.PdfDocument pdfDocument, List documents) + { var pdfOutlines = pdfDocument.GetOutlines(false); BuildPdfOutlines(pdfOutlines, documents); - - textDocument.Close(); - pdfStream.Position = 0; - return Task.FromResult(pdfStream); } private void BuildPdfOutlines(PdfOutline parentOutline, List pdfDocumentNodes) diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/ProjectPdfGenerator.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/ProjectPdfGenerator.cs index 618f823f80..391d4c77ae 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/ProjectPdfGenerator.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/ProjectPdfGenerator.cs @@ -23,23 +23,23 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency protected IDocumentToHtmlConverterFactory DocumentToHtmlConverterFactory { get; } protected IDocumentRepository DocumentRepository { get; } protected IDocumentSectionRenderer DocumentSectionRenderer { get; } - protected IOptions Options { get; } + protected IOptions Options { get; } protected IProjectPdfFileStore ProjectPdfFileStore { get; } protected IHtmlToPdfRenderer HtmlToPdfRenderer { get; } protected ILogger Logger { get; set; } protected IDocumentSource DocumentSource { get; set; } protected DocumentParams DocumentParams { get; set; } protected Project Project { get; set; } - protected List AllPdfDocuments { get; private set; } = []; + protected List AllPdfDocuments { get; } = []; public ProjectPdfGenerator( - IDocumentSourceFactory documentStoreFactory, + IDocumentSourceFactory documentStoreFactory, IDocumentRepository documentRepository, IOptions options, IDocumentSectionRenderer documentSectionRenderer, - IProjectPdfFileStore projectPdfFileStore, - IHtmlToPdfRenderer htmlToPdfRenderer, - IDocumentToHtmlConverterFactory documentToHtmlConverterFactory, + IProjectPdfFileStore projectPdfFileStore, + IHtmlToPdfRenderer htmlToPdfRenderer, + IDocumentToHtmlConverterFactory documentToHtmlConverterFactory, ILogger logger) { DocumentStoreFactory = documentStoreFactory; @@ -51,55 +51,44 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency DocumentToHtmlConverterFactory = documentToHtmlConverterFactory; Logger = logger; } - + public virtual async Task GenerateAsync(Project project, string version, string languageCode) { Project = project; DocumentSource = DocumentStoreFactory.Create(project.DocumentStoreType); DocumentParams = await GetDocumentParamsAsync(project, version, languageCode); - + var navigation = await GetNavigationAsync(project, version, languageCode); - AllPdfDocuments = new List(); await SetAllPdfDocumentsAsync(navigation.Items, project, version, languageCode); var title = Options.Value.CalculatePdfFileTitle?.Invoke(project) ?? project.Name; - var tempStreams = new List<(PdfDocument pdfDocument, Stream stream)>(); + var tempFiles = new List<(PdfDocument pdfDocument, Stream stream)>(); try { - var index = 0; - foreach (var document in AllPdfDocuments) + for (var i = 0; i < AllPdfDocuments.Count; i++) { - try - { + var document = AllPdfDocuments[i]; + Logger.LogInformation("Processing chunk {Index}/{Total}", i + 1, AllPdfDocuments.Count); - Logger.LogInformation("Processing chunk {Index}/{Total}", index + 1, AllPdfDocuments.Count); + var chunkHtml = await BuildHtmlAsync([document]); - var chunkHtml = await BuildHtmlAsync([document]); + var pdfStream = HtmlToPdfRenderer.Render($"{title} - Part {document.Title}", chunkHtml, [document]); - var pdfStream = await HtmlToPdfRenderer.RenderAsync($"{title} - Part {document.Title}", chunkHtml, [document]); - - Logger.LogInformation("Chunk {Index} rendered to PDF", index + 1); + Logger.LogInformation("Chunk {Index} rendered to PDF", i + 1); - tempStreams.Add((document, pdfStream)); - - index++; - } - catch (Exception e) - { - Logger.LogError(e, "Error processing chunk {Index} for project {ProjectName}", index + 1, project.Name); - } + tempFiles.Add((document, pdfStream)); } - + Logger.LogInformation("All chunks processed, merging PDF files"); - var mergedPdfStream = await MergePdfFilesAsync(tempStreams, title); + var mergedPdfStream = await MergePdfFilesAsync(tempFiles); await ProjectPdfFileStore.SetAsync(project, version, languageCode, mergedPdfStream); } - catch(Exception e) + catch (Exception e) { Logger.LogError(e, "An error occurred while generating the PDF for project {ProjectName}", project.Name); - foreach (var tempStream in tempStreams) + foreach (var tempStream in tempFiles) { try { @@ -113,6 +102,7 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency } finally { + AllPdfDocuments.Clear(); GC.Collect(); GC.WaitForPendingFinalizers(); } @@ -121,15 +111,15 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency protected virtual async Task BuildHtmlAsync(List pdfDocuments) { var htmlContent = await ConvertDocumentsToHtmlAsync(pdfDocuments); - + var htmlBuilder = new StringBuilder(); htmlBuilder.Append(Options.Value.HtmlLayout); htmlBuilder.Replace(DocsProjectPdfGeneratorOptions.StylePlaceholder, Options.Value.HtmlStyle); htmlBuilder.Replace(DocsProjectPdfGeneratorOptions.ContentPlaceholder, htmlContent); - + return htmlBuilder.ToString(); } - + protected virtual async Task ConvertDocumentsToHtmlAsync(List pdfDocuments) { var contentBuilder = new StringBuilder(); @@ -140,7 +130,9 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency { var renderedDocument = await RenderDocumentAsync(pdfDocument); var documentToHtmlConverter = GetDocumentToHtmlConverter(Project, pdfDocument); - var htmlContent = documentToHtmlConverter.Convert(new PdfDocumentToHtmlConverterContext(renderedDocument, pdfDocument, DocumentParams)); + var htmlContent = + documentToHtmlConverter.Convert( + new PdfDocumentToHtmlConverterContext(renderedDocument, pdfDocument, DocumentParams)); contentBuilder.AppendLine(htmlContent); } @@ -149,11 +141,12 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency contentBuilder.AppendLine(await ConvertDocumentsToHtmlAsync(pdfDocument.Children)); } } - + return contentBuilder.ToString(); } - protected virtual async Task MergePdfFilesAsync(List<(PdfDocument pdfDocument, Stream stream)> pdfFiles, string title) + protected virtual async Task MergePdfFilesAsync( + List<(PdfDocument pdfDocument, Stream stream)> pdfFiles) { if (pdfFiles.Count == 0) { @@ -162,50 +155,59 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency var zipStream = new MemoryStream(); - + + using (var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, true)) { - using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, true); - foreach (var (doc, pdfFile) in pdfFiles) + for (var index = 0; index < pdfFiles.Count; index++) { - if (pdfFile.CanSeek) - { - pdfFile.Position = 0; - } - - var entry = zipArchive.CreateEntry($"{title}_{doc.Title}.pdf", CompressionLevel.Fastest); + var (doc, pdfFile) = pdfFiles[index]; + var entry = zipArchive.CreateEntry($"{index + 1}_{doc.Title}.pdf", CompressionLevel.Fastest); await using var entryStream = entry.Open(); await pdfFile.CopyToAsync(entryStream); - await pdfFile.DisposeAsync(); + try + { + await pdfFile.DisposeAsync(); + } + catch (Exception e) + { + Logger.LogWarning(e, "Error disposing PDF file stream for document {DocumentTitle}", doc.Title); + } } } + zipStream.Position = 0; return zipStream; } - - protected virtual IDocumentToHtmlConverter GetDocumentToHtmlConverter(Project project, PdfDocument pdfDocument) + + protected virtual IDocumentToHtmlConverter GetDocumentToHtmlConverter( + Project project, PdfDocument pdfDocument) { - return DocumentToHtmlConverterFactory.Create(DocsDomainConsts.PdfDocumentToHtmlConverterPrefix +(pdfDocument.Document.Format ?? project.Format)); + return DocumentToHtmlConverterFactory.Create( + DocsDomainConsts.PdfDocumentToHtmlConverterPrefix + (pdfDocument.Document.Format ?? project.Format)); } - + protected virtual async Task RenderDocumentAsync(PdfDocument pdfDocument) { var parameters = new DocumentRenderParameters(); - if (pdfDocument.RenderParameters != null) + if (pdfDocument.RenderParameters == null) { - foreach (var renderParameter in pdfDocument.RenderParameters) - { - var documentParam = DocumentParams.Parameters.FirstOrDefault(p => p.Name == renderParameter.Key); - parameters.Add(renderParameter.Key, renderParameter.Value); - parameters.Add(renderParameter.Key + "_Value",documentParam?.Values[renderParameter.Value] ?? renderParameter.Value); - } + return await DocumentSectionRenderer.RenderAsync(pdfDocument.Document.Content, parameters); } - + + foreach (var renderParameter in pdfDocument.RenderParameters) + { + var documentParam = DocumentParams.Parameters.FirstOrDefault(p => p.Name == renderParameter.Key); + parameters.Add(renderParameter.Key, renderParameter.Value); + parameters.Add(renderParameter.Key + "_Value", + documentParam?.Values[renderParameter.Value] ?? renderParameter.Value); + } + return await DocumentSectionRenderer.RenderAsync(pdfDocument.Document.Content, parameters); } - + protected virtual async Task SetAllPdfDocumentsAsync( List navigations, Project project, @@ -228,10 +230,11 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency { Title = navigation.Text, IgnoreOnOutline = navigation.Path == Options.Value.IndexPagePath, - Id = UrlHelper.IsExternalLink(navigation.Path) ? navigation.Path: null + Id = UrlHelper.IsExternalLink(navigation.Path) ? navigation.Path : null }; - if (!navigation.Path.IsNullOrWhiteSpace() && !UrlHelper.IsExternalLink(navigation.Path) && !navigation.HasChildItems) + if (!navigation.Path.IsNullOrWhiteSpace() && !UrlHelper.IsExternalLink(navigation.Path) && + !navigation.HasChildItems) { await HandleLeafDocumentAsync( navigation, @@ -255,7 +258,7 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency parentPdfDocument.Children.AddIfNotContains(pdfDocument); } } - + if (navigation.HasChildItems) { AddParameterCombinationsDocuments(parentPdfDocument, groupedCombinationsDocuments); @@ -269,11 +272,12 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency } catch (Exception e) { - Logger.LogWarning(e, $"Cannot get document for the path '{navigation.Path}' in the project {project.Name}."); + Logger.LogWarning(e, + $"Cannot get document for the path '{navigation.Path}' in the project {project.Name}."); } } } - + private async Task HandleLeafDocumentAsync( NavigationNode navigation, Project project, @@ -309,22 +313,25 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency groupedCombinationsDocuments[key] = []; } - var combinationDocument = i == 0 ? leafDocument : new PdfDocument - { - Document = document, - Id = GetDocumentId(document.Name, document.Format ?? project.Format, combination, false), - Title = GetDocumentTitle(navigation.Text, combination, DocumentParams), - RenderParameters = combination - }; + var combinationDocument = i == 0 + ? leafDocument + : new PdfDocument + { + Document = document, + Id = GetDocumentId(document.Name, document.Format ?? project.Format, combination, false), + Title = GetDocumentTitle(navigation.Text, combination, DocumentParams), + RenderParameters = combination + }; groupedCombinationsDocuments[key].Add(combinationDocument); } - } - - private void AddParameterCombinationsDocuments(PdfDocument parentPdfDocument, Dictionary> groupedCombinationsDocuments) + } + + private void AddParameterCombinationsDocuments(PdfDocument parentPdfDocument, + Dictionary> groupedCombinationsDocuments) { foreach (var combinations in groupedCombinationsDocuments) - { + { if (parentPdfDocument == null) { AllPdfDocuments.AddIfNotContains(combinations.Value); @@ -334,33 +341,31 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency parentPdfDocument.Children.AddIfNotContains(combinations.Value); } } - + groupedCombinationsDocuments.Clear(); } - + private async Task GetNavigationAsync( Project project, string version, string languageCode) { var navigationDocument = await GetDocumentAsync(project, project.NavigationDocumentName, version, languageCode); - + if (!DocsJsonSerializerHelper.TryDeserialize(navigationDocument.Content, out var navigation)) { - throw new UserFriendlyException($"Cannot validate navigation file '{project.NavigationDocumentName}' for the project {project.Name}."); + throw new UserFriendlyException( + $"Cannot validate navigation file '{project.NavigationDocumentName}' for the project {project.Name}."); } if (!Options.Value.IndexPagePath.IsNullOrWhiteSpace()) { - navigation.Items.Insert(0, new NavigationNode - { - Path = Options.Value.IndexPagePath - }); + navigation.Items.Insert(0, new NavigationNode { Path = Options.Value.IndexPagePath }); } - + return navigation; } - + private async Task GetDocumentParamsAsync( Project project, string version, @@ -373,13 +378,16 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency try { - var documentParamsDocument = await GetDocumentAsync(project, project.ParametersDocumentName, version, languageCode); - - if (!DocsJsonSerializerHelper.TryDeserialize(documentParamsDocument.Content, out var documentParams)) + var documentParamsDocument = + await GetDocumentAsync(project, project.ParametersDocumentName, version, languageCode); + + if (!DocsJsonSerializerHelper.TryDeserialize(documentParamsDocument.Content, + out var documentParams)) { - throw new UserFriendlyException($"Cannot validate document params file '{project.ParametersDocumentName}' for the project {project.Name}."); + throw new UserFriendlyException( + $"Cannot validate document params file '{project.ParametersDocumentName}' for the project {project.Name}."); } - + return documentParams; } catch (Exception e) @@ -388,25 +396,25 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency return new DocumentParams(); } } - + private async Task GetDocumentAsync( - Project project, + Project project, string documentName, string version, string languageCode) { version = string.IsNullOrWhiteSpace(version) ? project.LatestVersionBranchName : version; - + Document document = null; Exception firstException = null; - + var possibleNames = GetPossibleNames(documentName, project.Format); document = await DocumentRepository.FindAsync(project.Id, possibleNames, languageCode, version); if (document != null) { return document; } - + foreach (var name in possibleNames) { try @@ -421,15 +429,16 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency } } - if(document == null) + if (document == null) { throw firstException!; } return document; } - - private List GenerateAllParameterCombinations(List parameterKeys, Dictionary> parameters) + + private static List GenerateAllParameterCombinations(List parameterKeys, + Dictionary> parameters) { var parameterCombinations = new List(); @@ -437,7 +446,7 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency { return parameterCombinations; } - + GenerateCombinations(0, new DocumentRenderParameters()); return parameterCombinations; @@ -458,33 +467,35 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency } } - private static string GetDocumentId(string documentName, string format, DocumentRenderParameters parameters, bool isFirstCombinationDocument) + private static string GetDocumentId(string documentName, string format, DocumentRenderParameters parameters, + bool isFirstCombinationDocument) { - var id = documentName.Replace("." + format, string.Empty).Replace("/","-").Replace(" ", "-").ToLower(); - if (parameters != null && !isFirstCombinationDocument) - { - id = $"{id}{parameters.Select(x => $"{x.Key}_{x.Value}").JoinAsString("-")}"; - } - - return id; + var id = documentName.Replace("." + format, string.Empty).Replace("/", "-").Replace(" ", "-").ToLower(); + if (parameters != null && !isFirstCombinationDocument) + { + id = $"{id}{parameters.Select(x => $"{x.Key}_{x.Value}").JoinAsString("-")}"; + } + + return id; } - - private static string GetDocumentTitle(string title, DocumentRenderParameters parameters, DocumentParams documentParams) + + private static string GetDocumentTitle(string title, DocumentRenderParameters parameters, + DocumentParams documentParams) { if (parameters == null || parameters.Count <= 0) { return title; } - + var paramValues = parameters.Select(x => { var documentParam = documentParams.Parameters.FirstOrDefault(p => p.Name == x.Key); return $"{documentParam?.DisplayName ?? x.Key} : {documentParam?.Values[x.Value] ?? x.Value}"; }); - + return title.Trim() + $" ({string.Join(", ", paramValues)})"; } - + private static List GetPossibleNames(string originalDocumentName, string format) { var extension = Path.GetExtension(originalDocumentName); @@ -492,7 +503,7 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency { extension = "." + format; } - + if (!extension.Equals("." + format, StringComparison.OrdinalIgnoreCase)) { return [originalDocumentName]; @@ -502,33 +513,36 @@ public class ProjectPdfGenerator : IProjectPdfGenerator, ITransientDependency var titleCaseIndex = "Index." + format; var indexLength = lowerCaseIndex.Length; - var possibleNames = new List {originalDocumentName}; - if (originalDocumentName.EndsWith("/" + lowerCaseIndex, StringComparison.OrdinalIgnoreCase) || originalDocumentName.Equals(lowerCaseIndex, StringComparison.OrdinalIgnoreCase)) + var possibleNames = new List { originalDocumentName }; + if (originalDocumentName.EndsWith("/" + lowerCaseIndex, StringComparison.OrdinalIgnoreCase) || + originalDocumentName.Equals(lowerCaseIndex, StringComparison.OrdinalIgnoreCase)) { var indexPart = originalDocumentName.Right(indexLength); - var documentNameWithoutIndex = originalDocumentName.Left(originalDocumentName.Length - lowerCaseIndex.Length); + var documentNameWithoutIndex = + originalDocumentName.Left(originalDocumentName.Length - lowerCaseIndex.Length); - if(indexPart != lowerCaseIndex) + if (indexPart != lowerCaseIndex) { possibleNames.Add(documentNameWithoutIndex + lowerCaseIndex); } - if(indexPart != titleCaseIndex) + if (indexPart != titleCaseIndex) { possibleNames.Add(documentNameWithoutIndex + titleCaseIndex); } } else { - var documentNameWithoutExtension = RemoveFileExtensionFromPath(originalDocumentName, format).EnsureEndsWith('/'); + var documentNameWithoutExtension = + RemoveFileExtensionFromPath(originalDocumentName, format).EnsureEndsWith('/'); possibleNames.Add(documentNameWithoutExtension + lowerCaseIndex); possibleNames.Add(documentNameWithoutExtension + titleCaseIndex); } return possibleNames; } - + private static string RemoveFileExtensionFromPath(string path, string format) { if (path == null)