Browse Source

refactoring

pull/22922/head
SALİH ÖZKARA 1 year ago
parent
commit
7b70934164
  1. 2
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IHtmlToPdfRenderer.cs
  2. 33
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IText/ITextHtmlToPdfRenderer.cs
  3. 256
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/ProjectPdfGenerator.cs

2
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<Stream> RenderAsync(string title, string html, List<PdfDocument> documents);
Stream Render(string title, string html, List<PdfDocument> documents);
}

33
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<Stream> RenderAsync(string title, string html, List<PdfDocument> documents)
public virtual Stream Render(string title, string html, List<PdfDocument> 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<PdfDocument> documents)
{
var pdfOutlines = pdfDocument.GetOutlines(false);
BuildPdfOutlines(pdfOutlines, documents);
textDocument.Close();
pdfStream.Position = 0;
return Task.FromResult<Stream>(pdfStream);
}
private void BuildPdfOutlines(PdfOutline parentOutline, List<PdfDocument> pdfDocumentNodes)

256
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<DocsProjectPdfGeneratorOptions> Options { get; }
protected IOptions<DocsProjectPdfGeneratorOptions> Options { get; }
protected IProjectPdfFileStore ProjectPdfFileStore { get; }
protected IHtmlToPdfRenderer HtmlToPdfRenderer { get; }
protected ILogger<ProjectPdfGenerator> Logger { get; set; }
protected IDocumentSource DocumentSource { get; set; }
protected DocumentParams DocumentParams { get; set; }
protected Project Project { get; set; }
protected List<PdfDocument> AllPdfDocuments { get; private set; } = [];
protected List<PdfDocument> AllPdfDocuments { get; } = [];
public ProjectPdfGenerator(
IDocumentSourceFactory documentStoreFactory,
IDocumentSourceFactory documentStoreFactory,
IDocumentRepository documentRepository,
IOptions<DocsProjectPdfGeneratorOptions> options,
IDocumentSectionRenderer documentSectionRenderer,
IProjectPdfFileStore projectPdfFileStore,
IHtmlToPdfRenderer htmlToPdfRenderer,
IDocumentToHtmlConverterFactory documentToHtmlConverterFactory,
IProjectPdfFileStore projectPdfFileStore,
IHtmlToPdfRenderer htmlToPdfRenderer,
IDocumentToHtmlConverterFactory documentToHtmlConverterFactory,
ILogger<ProjectPdfGenerator> 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<PdfDocument>();
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<string> BuildHtmlAsync(List<PdfDocument> 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<string> ConvertDocumentsToHtmlAsync(List<PdfDocument> 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<MemoryStream> MergePdfFilesAsync(List<(PdfDocument pdfDocument, Stream stream)> pdfFiles, string title)
protected virtual async Task<MemoryStream> 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<PdfDocumentToHtmlConverterContext> GetDocumentToHtmlConverter(Project project, PdfDocument pdfDocument)
protected virtual IDocumentToHtmlConverter<PdfDocumentToHtmlConverterContext> GetDocumentToHtmlConverter(
Project project, PdfDocument pdfDocument)
{
return DocumentToHtmlConverterFactory.Create<PdfDocumentToHtmlConverterContext>(DocsDomainConsts.PdfDocumentToHtmlConverterPrefix +(pdfDocument.Document.Format ?? project.Format));
return DocumentToHtmlConverterFactory.Create<PdfDocumentToHtmlConverterContext>(
DocsDomainConsts.PdfDocumentToHtmlConverterPrefix + (pdfDocument.Document.Format ?? project.Format));
}
protected virtual async Task<string> 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<NavigationNode> 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<string,List<PdfDocument>> groupedCombinationsDocuments)
}
private void AddParameterCombinationsDocuments(PdfDocument parentPdfDocument,
Dictionary<string, List<PdfDocument>> 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<NavigationNode> GetNavigationAsync(
Project project,
string version,
string languageCode)
{
var navigationDocument = await GetDocumentAsync(project, project.NavigationDocumentName, version, languageCode);
if (!DocsJsonSerializerHelper.TryDeserialize<NavigationNode>(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<DocumentParams> 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<DocumentParams>(documentParamsDocument.Content, out var documentParams))
var documentParamsDocument =
await GetDocumentAsync(project, project.ParametersDocumentName, version, languageCode);
if (!DocsJsonSerializerHelper.TryDeserialize<DocumentParams>(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<Document> 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<DocumentRenderParameters> GenerateAllParameterCombinations(List<string> parameterKeys, Dictionary<string, List<string>> parameters)
private static List<DocumentRenderParameters> GenerateAllParameterCombinations(List<string> parameterKeys,
Dictionary<string, List<string>> parameters)
{
var parameterCombinations = new List<DocumentRenderParameters>();
@ -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<string> 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<string> {originalDocumentName};
if (originalDocumentName.EndsWith("/" + lowerCaseIndex, StringComparison.OrdinalIgnoreCase) || originalDocumentName.Equals(lowerCaseIndex, StringComparison.OrdinalIgnoreCase))
var possibleNames = new List<string> { 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)

Loading…
Cancel
Save