task-tracking-system / TaskTrackingSystem.Shared /SimplePdfReportBuilder.cs
User
fix: date time format, add pdf download, csv to xlsx, adjust dto responses
bec15e2
Raw
History Blame Contribute Delete
6.48 kB
using System.Text;
namespace TaskTrackingSystem.Shared;
public static class SimplePdfReportBuilder
{
private const int PageWidth = 612;
private const int PageHeight = 792;
private const int LeftMargin = 48;
private const int TopMargin = 52;
private const int BottomMargin = 48;
private const int TitleFontSize = 16;
private const int BodyFontSize = 10;
private const int LineSpacing = 14;
private const int BodyLinesPerPage = 42;
public static byte[] BuildTableReport(
string title,
IEnumerable<string>? summaryLines,
IReadOnlyList<string> headers,
IEnumerable<IReadOnlyList<string>> rows)
{
var lines = new List<string>();
if (summaryLines != null)
{
foreach (var line in summaryLines.Where(line => !string.IsNullOrWhiteSpace(line)))
{
lines.AddRange(WrapLine(line.Trim()));
}
}
if (headers.Count > 0)
{
lines.Add(string.Join(" | ", headers));
lines.Add(new string('-', Math.Min(96, Math.Max(24, headers.Sum(h => h.Length + 3)))));
}
foreach (var row in rows)
{
lines.AddRange(WrapLine(string.Join(" | ", row.Select(NormalizeCell))));
}
if (lines.Count == 0)
{
lines.Add("No data available.");
}
return BuildTextReport(title, lines);
}
public static byte[] BuildTextReport(string title, IEnumerable<string> bodyLines)
{
var normalizedLines = bodyLines
.SelectMany(line => WrapLine(line))
.ToList();
var pages = normalizedLines
.Chunk(BodyLinesPerPage)
.Select(chunk => chunk.ToList())
.ToList();
if (pages.Count == 0)
{
pages.Add(new List<string> { "No data available." });
}
var objects = new List<string>();
objects.Add("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
var pageObjectIds = new List<int>();
var contentObjectIds = new List<int>();
var nextObjectId = 4;
foreach (var _ in pages)
{
pageObjectIds.Add(nextObjectId++);
contentObjectIds.Add(nextObjectId++);
}
var pagesKids = string.Join(" ", pageObjectIds.Select(id => $"{id} 0 R"));
objects.Add($"2 0 obj\n<< /Type /Pages /Kids [{pagesKids}] /Count {pages.Count} >>\nendobj\n");
objects.Add("3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n");
for (var pageIndex = 0; pageIndex < pages.Count; pageIndex++)
{
var pageObjectId = pageObjectIds[pageIndex];
var contentObjectId = contentObjectIds[pageIndex];
var pageLines = pages[pageIndex];
var content = BuildContentStream(title, pageLines, pageIndex + 1, pages.Count);
var contentBytes = Encoding.ASCII.GetBytes(content);
objects.Add(
$"{pageObjectId} 0 obj\n" +
$"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {PageWidth} {PageHeight}] /Resources << /Font << /F1 3 0 R >> >> /Contents {contentObjectId} 0 R >>\n" +
"endobj\n");
objects.Add(
$"{contentObjectId} 0 obj\n" +
$"<< /Length {contentBytes.Length} >>\n" +
"stream\n" +
content +
"\nendstream\nendobj\n");
}
var buffer = new StringBuilder();
buffer.Append("%PDF-1.4\n");
var offsets = new List<int> { 0 };
foreach (var obj in objects)
{
offsets.Add(Encoding.ASCII.GetByteCount(buffer.ToString()));
buffer.Append(obj);
}
var xrefStart = Encoding.ASCII.GetByteCount(buffer.ToString());
var xref = new StringBuilder();
xref.Append("xref\n");
xref.Append($"0 {objects.Count + 1}\n");
xref.Append("0000000000 65535 f \n");
foreach (var offset in offsets.Skip(1))
{
xref.Append($"{offset:D10} 00000 n \n");
}
xref.Append("trailer\n");
xref.Append($"<< /Size {objects.Count + 1} /Root 1 0 R >>\n");
xref.Append("startxref\n");
xref.Append($"{xrefStart}\n");
xref.Append("%%EOF");
buffer.Append(xref);
return Encoding.ASCII.GetBytes(buffer.ToString());
}
private static string BuildContentStream(string title, IReadOnlyList<string> lines, int pageNumber, int totalPages)
{
var content = new StringBuilder();
content.AppendLine("BT");
content.AppendLine("/F1 16 Tf");
content.AppendLine($"{LeftMargin} {PageHeight - TopMargin} Td");
content.AppendLine($"({EscapeText(title)}) Tj");
content.AppendLine("/F1 10 Tf");
content.AppendLine($"0 -22 Td");
content.AppendLine($"({EscapeText($"Page {pageNumber} of {totalPages}")}) Tj");
foreach (var line in lines)
{
content.AppendLine($"0 -{LineSpacing} Td");
content.AppendLine($"({EscapeText(line)}) Tj");
}
content.AppendLine("ET");
return content.ToString();
}
private static IEnumerable<string> WrapLine(string? line)
{
if (string.IsNullOrWhiteSpace(line))
{
yield return string.Empty;
yield break;
}
const int maxWidth = 92;
var remaining = line.Trim();
while (remaining.Length > maxWidth)
{
var breakIndex = remaining.LastIndexOf(' ', maxWidth);
if (breakIndex <= 0)
{
breakIndex = maxWidth;
}
yield return remaining[..breakIndex].Trim();
remaining = remaining[breakIndex..].TrimStart();
}
if (!string.IsNullOrWhiteSpace(remaining))
{
yield return remaining;
}
}
private static string NormalizeCell(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "-";
}
return value.Replace("\r", " ").Replace("\n", " ").Trim();
}
private static string EscapeText(string value)
{
var ascii = new string(value.Select(ch => ch is >= ' ' and <= '~' ? ch : '?').ToArray());
return ascii
.Replace("\\", "\\\\")
.Replace("(", "\\(")
.Replace(")", "\\)");
}
}