Implementation
static Future<Uint8List> generate({
required String? companyName,
required String? companyAddress,
required Uint8List? companyLogoBytes,
int? logoWidth,
int? logoHeight,
required String reportName,
required List<List<NrbHeaderCell>> headers,
required List<List<ReportCell>> tableData,
required List<double> columnWidths,
}) async {
final bytesBuilder = BytesBuilder();
final List<int> pageObjectIds = [];
final Map<int, int> objectOffsets = {};
int nextObjId = 1;
void _addObj(int id, String content, {Uint8List? stream}) {
objectOffsets[id] = bytesBuilder.length;
bytesBuilder.add(utf8.encode("$id 0 obj\n$content"));
if (stream != null) {
bytesBuilder.add(utf8.encode("\nstream\n"));
bytesBuilder.add(stream);
bytesBuilder.add(utf8.encode("\nendstream"));
}
bytesBuilder.add(utf8.encode("\nendobj\n"));
}
// 1. PDF Header
bytesBuilder.add(utf8.encode("%PDF-1.7\n"));
bytesBuilder.add(utf8.encode("%\u00FF\u00FF\u00FF\u00FF\n"));
int totalCols = columnWidths.length;
double pageWidth = (totalCols * 65.0 + 60.0 > 595.27) ? totalCols * 65.0 + 60.0 : 595.27;
double pageHeight = 841.89;
double leftMargin = 30.0;
double rightMargin = 30.0;
double topMargin = 30.0;
double bottomMargin = 50.0;
double availableWidth = pageWidth - leftMargin - rightMargin;
double totalRawWidth = columnWidths.fold(0.0, (a, b) => a + b);
List<double> scaledWidths = totalRawWidth > 0
? columnWidths.map((w) => w * (availableWidth / totalRawWidth)).toList()
: List.filled(totalCols, availableWidth / totalCols);
double baseFontSize = (totalCols <= 10) ? 9.0 : 7.0;
double headerFontSize = baseFontSize + 1.0;
int fontNormalId = nextObjId++;
int fontBoldId = nextObjId++;
int catalogId = nextObjId++;
int pagesTreeId = nextObjId++;
int? logoObjId;
if (companyLogoBytes != null) logoObjId = nextObjId++;
List<String> pagesContent = [];
StringBuffer currentContent = StringBuffer();
double currentY = pageHeight - topMargin;
String _pdfColor(Color color) =>
"${(color.r).toStringAsFixed(3)} ${(color.g).toStringAsFixed(3)} ${(color.b).toStringAsFixed(3)}";
void _drawRect(StringBuffer b, double x, double y, double w, double h, Color fill) {
b.write("q ${_pdfColor(fill)} rg $x ${y - h} $w $h re f Q\n");
}
void _drawText(StringBuffer b, String text, double x, double y, double size, Color color, {bool isBold = false}) {
final escapedText = text.replaceAll('\\', '\\\\').replaceAll('(', '\\(').replaceAll(')', '\\)');
b.write("q ${_pdfColor(color)} rg BT /F${isBold ? "FB" : "FN"} $size Tf $x $y Td ($escapedText) Tj ET Q\n");
}
void _drawLine(StringBuffer b, double x1, double y1, double x2, double y2) {
b.write("q 0.5 w 0 G $x1 $y1 m $x2 $y2 l s Q\n");
}
void _addFooter(StringBuffer b, int pageNum) {
_drawText(b, "Page $pageNum | This report is generated electronically, no signature is required.", leftMargin, 20, 8, Colors.grey);
}
void _renderTableHeaders(StringBuffer b) {
List<List<bool>> occupied = List.generate(headers.length, (_) => List.filled(totalCols, false));
for (int r = 0; r < headers.length; r++) {
double rowHeight = 25.0;
int currentCol = 0;
for (var cell in headers[r]) {
while (currentCol < totalCols && occupied[r][currentCol]) currentCol++;
if (currentCol >= totalCols) break;
double cellX = leftMargin + scaledWidths.take(currentCol).fold(0.0, (a, b) => a + b);
double cellW = scaledWidths.skip(currentCol).take(cell.colSpan).fold(0.0, (a, b) => a + b);
double cellH = rowHeight * cell.rowSpan;
_drawRect(b, cellX, currentY, cellW, cellH, cell.backgroundColor);
_drawText(b, cell.text, cellX + 5, currentY - (cellH/2) - 4, headerFontSize, cell.foregroundColor, isBold: true);
for (int rr = 0; rr < cell.rowSpan; rr++) {
for (int cc = 0; cc < cell.colSpan; cc++) {
if (r + rr < headers.length && currentCol + cc < totalCols) occupied[r + rr][currentCol + cc] = true;
}
}
currentCol += cell.colSpan;
}
currentY -= rowHeight;
}
}
void _startNewPage() {
if (currentContent.isNotEmpty) {
_addFooter(currentContent, pagesContent.length + 1);
pagesContent.add(currentContent.toString());
}
currentContent = StringBuffer();
currentY = pageHeight - topMargin;
}
// --- PAGE 1 BRANDING ---
_startNewPage();
double logoW = 1.1 * 72.0;
double logoH = 0.8 * 72.0;
if (companyName != null || companyAddress != null || companyLogoBytes != null) {
double textX = leftMargin;
if (companyLogoBytes != null && logoObjId != null) {
currentContent.write("q $logoW 0 0 $logoH $leftMargin ${currentY - logoH} cm /Img1 Do Q\n");
textX = leftMargin + logoW + 15;
}
_drawText(currentContent, companyName ?? "Company Name", textX, currentY - 20, 16, Colors.black, isBold: true);
if (companyAddress != null) {
_drawText(currentContent, companyAddress, textX, currentY - 40, 10, Colors.grey);
}
currentY -= (logoH > 60) ? logoH + 20 : 60;
}
_drawText(currentContent, reportName, pageWidth / 2 - 50, currentY - 15, 12, Colors.black, isBold: true);
currentY -= 30;
_renderTableHeaders(currentContent);
// --- DATA ROWS ---
for (var row in tableData) {
double rowHeight = 22.0;
if (currentY - rowHeight < bottomMargin) {
_startNewPage();
_renderTableHeaders(currentContent);
}
double currentX = leftMargin;
for (int c = 0; c < row.length; c++) {
var cell = row[c];
double cellW = scaledWidths[c];
_drawRect(currentContent, currentX, currentY, cellW, rowHeight, cell.exportBackgroundColor ?? Colors.white);
_drawLine(currentContent, currentX, currentY, currentX + cellW, currentY);
_drawLine(currentContent, currentX, currentY - rowHeight, currentX + cellW, currentY - rowHeight);
_drawLine(currentContent, currentX, currentY, currentX, currentY - rowHeight);
currentContent.write("q 0.5 w 0 G ${currentX + cellW} $currentY m ${currentX + cellW} ${currentY - rowHeight} l s Q\n");
_drawText(currentContent, cell.exportValue, currentX + 5, currentY - 15, baseFontSize, cell.exportTextColor ?? Colors.black, isBold: cell.exportIsBold);
currentX += cellW;
}
currentY -= rowHeight;
}
_addFooter(currentContent, pagesContent.length + 1);
pagesContent.add(currentContent.toString());
// --- WRITE OBJECTS ---
_addObj(fontNormalId, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>");
_addObj(fontBoldId, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>");
if (companyLogoBytes != null && logoObjId != null && logoWidth != null && logoHeight != null) {
_addObj(logoObjId, "<< /Type /XObject /Subtype /Image /Width $logoWidth /Height $logoHeight /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length ${companyLogoBytes.length} >>", stream: companyLogoBytes);
}
for (var content in pagesContent) {
int streamId = nextObjId++;
int pageId = nextObjId++;
pageObjectIds.add(pageId);
final bytes = utf8.encode(content);
_addObj(streamId, "<< /Length ${bytes.length} >>", stream: Uint8List.fromList(bytes));
_addObj(pageId, "<< /Type /Page /Parent $pagesTreeId 0 R /MediaBox [0 0 $pageWidth $pageHeight] /Contents $streamId 0 R /Resources << /Font << /FN $fontNormalId 0 R /FB $fontBoldId 0 R >> /XObject << /Img1 $logoObjId 0 R >> >> >>");
}
_addObj(pagesTreeId, "<< /Type /Pages /Kids [${pageObjectIds.map((id) => "$id 0 R").join(" ")}] /Count ${pageObjectIds.length} >>");
_addObj(catalogId, "<< /Type /Catalog /Pages $pagesTreeId 0 R >>");
int startXref = bytesBuilder.length;
bytesBuilder.add(utf8.encode("xref\n0 ${nextObjId}\n0000000000 65535 f \r\n"));
for (int i = 1; i < nextObjId; i++) {
bytesBuilder.add(utf8.encode("${(objectOffsets[i] ?? 0).toString().padLeft(10, '0')} 00000 n \r\n"));
}
bytesBuilder.add(utf8.encode("trailer\n<< /Size $nextObjId /Root $catalogId 0 R >>\nstartxref\n$startXref\n%%EOF"));
return bytesBuilder.toBytes();
}