generate static method

Future<Uint8List> generate({
  1. required String? companyName,
  2. required String? companyAddress,
  3. required Uint8List? companyLogoBytes,
  4. int? logoWidth,
  5. int? logoHeight,
  6. required String reportName,
  7. required List<List<NrbHeaderCell>> headers,
  8. required List<List<ReportCell>> tableData,
  9. required List<double> columnWidths,
})

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();
}