printComplexReceipt static method

Future<bool> printComplexReceipt({
  1. required List<ReceiptContent> contents,
  2. int printerWidth = 576,
  3. double padding = 10,
  4. bool cutPaperAfter = true,
  5. bool optimizeAsciiText = true,
})

Print a complex receipt with mixed content types.

  • contents: List of content items (text, images, barcodes, etc.)
  • printerWidth: Printer width in dots
  • padding: Padding around content
  • cutPaperAfter: Whether to cut paper after printing
  • optimizeAsciiText: Whether to optimize ASCII text printing

Returns true if printing was successful.

Implementation

static Future<bool> printComplexReceipt({
  required List<ReceiptContent> contents,
  int printerWidth = 576,
  double padding = 10,
  bool cutPaperAfter = true,
  bool optimizeAsciiText = true, // Enable/disable optimization
}) async {
  try {
    // Optimization: Directly print ASCII text when possible
    if (optimizeAsciiText) {
      bool overallSuccess = true;
      List<ReceiptContent> imageRenderBatch = [];

      // Process each content item
      for (int i = 0; i < contents.length; i++) {
        ReceiptContent content = contents[i];

        // Check if this content can be directly printed (ASCII text or blank lines)
        if (_canDirectPrint(content)) {
          // If we have a batch of items to image-render, do that first
          if (imageRenderBatch.isNotEmpty) {
            final batchResult = await _printContentBatchAsImage(
                imageRenderBatch, printerWidth, padding);
            if (!batchResult) overallSuccess = false;
            imageRenderBatch = []; // Clear the batch
          }

          // Print this content directly
          final directResult = await _printContentDirect(content);
          if (!directResult) overallSuccess = false;
        } else if (content.type == ReceiptContentType.barcode) {
          // Handle barcode printing separately (since we have a specialized function)
          if (imageRenderBatch.isNotEmpty) {
            final batchResult = await _printContentBatchAsImage(
                imageRenderBatch, printerWidth, padding);
            if (!batchResult) overallSuccess = false;
            imageRenderBatch = []; // Clear the batch
          }

          // Print barcode using our specialized function
          final barcodeResult =
              await _printBarcodeContent(content, printerWidth);
          if (!barcodeResult) overallSuccess = false;
        } else {
          // Add to the batch for image rendering
          imageRenderBatch.add(content);
        }
      }

      // Print any remaining batch items
      if (imageRenderBatch.isNotEmpty) {
        final batchResult = await _printContentBatchAsImage(
            imageRenderBatch, printerWidth, padding);
        if (!batchResult) overallSuccess = false;
      }

      // Cut paper if requested
      if (overallSuccess && cutPaperAfter) {
        await PrinterCore.cutPaper();
      }

      return overallSuccess;
    }

    // Original non-optimized implementation (render everything as an image)
    // Set up working directory for temporary files
    final directory = await getTemporaryDirectory();
    final tempPath =
        '${directory.path}/complex_receipt_${DateTime.now().millisecondsSinceEpoch}.png';

    // First pass: Calculate total height needed
    double totalHeight = 0;
    List<double> contentHeights = [];

    for (var content in contents) {
      double itemHeight = 0;

      switch (content.type) {
        case ReceiptContentType.text:
          String text = content.data as String;
          final fontSize = content.options?['fontSize'] as double? ?? 20;
          final fontFamily =
              content.options?['fontFamily'] as String? ?? 'Siemreap';
          final lineSpacing =
              content.options?['lineSpacing'] as double? ?? 1.5;

          // Calculate text height
          final textSpan = TextSpan(
            text: text,
            style: TextStyle(
              fontSize: fontSize,
              fontFamily: fontFamily,
            ),
          );

          final textPainter = TextPainter(
            text: textSpan,
            textDirection: TextDirection.ltr,
          );

          textPainter.layout(
              maxWidth: printerWidth.toDouble() - (padding * 2));
          itemHeight = textPainter.height * lineSpacing;
          break;

        case ReceiptContentType.centeredText:
          String text = content.data as String;
          final fontSize = content.options?['fontSize'] as double? ?? 24;
          final fontFamily =
              content.options?['fontFamily'] as String? ?? 'Siemreap';

          // Calculate centered text height
          final textSpan = TextSpan(
            text: text,
            style: TextStyle(
              fontSize: fontSize,
              fontFamily: fontFamily,
            ),
          );

          final textPainter = TextPainter(
            text: textSpan,
            textDirection: TextDirection.ltr,
            textAlign: TextAlign.center,
          );

          textPainter.layout();
          itemHeight = textPainter.height + (padding * 2);
          break;

        case ReceiptContentType.multiColumn:
          final columns = content.data as List<PrintColumn>;
          final lineHeight = content.options?['lineHeight'] as double? ?? 1.5;
          final rowPadding = content.options?['padding'] as double? ?? 10;

          // Calculate multi-column height based on the tallest text
          double maxHeight = 0;
          for (var col in columns) {
            final textSpan = TextSpan(
              text: col.text,
              style: TextStyle(
                fontSize: col.fontSize,
                fontFamily: col.fontFamily,
              ),
            );

            final textPainter = TextPainter(
              text: textSpan,
              textDirection: TextDirection.ltr,
            );

            textPainter.layout();
            if (textPainter.height > maxHeight) {
              maxHeight = textPainter.height;
            }
          }

          itemHeight = maxHeight * lineHeight + (rowPadding * 2);
          break;

        case ReceiptContentType.barcode:
          final type =
              content.options?['type'] as BarcodeType? ?? BarcodeType.code128;
          final height = content.options?['height'] as int? ?? 100;
          final textPosition =
              content.options?['textPosition'] as BarcodeTextPosition? ??
                  BarcodeTextPosition.below;
          final showText = textPosition != BarcodeTextPosition.none;

          // Calculate barcode height
          itemHeight =
              height.toDouble() + (showText ? 30 : 0) + (padding * 2);
          break;

        case ReceiptContentType.image:
          // For images, use the specified height or a default
          itemHeight = (content.options?['height'] as double?) ?? 200;
          break;

        case ReceiptContentType.blankLines:
          final lines = content.data as int;
          itemHeight = lines * 20.0; // Estimate 20 pixels per line
          break;
      }

      contentHeights.add(itemHeight);
      totalHeight += itemHeight;
    }

    // Create recorder and canvas for the combined image
    final recorder = PictureRecorder();
    final canvas = Canvas(recorder);

    // Draw white background
    canvas.drawRect(
      Rect.fromLTWH(0, 0, printerWidth.toDouble(), totalHeight),
      Paint()..color = Colors.white,
    );

    // Current Y position for drawing
    double currentY = 0;

    // Second pass: Draw each element to the canvas
    for (int i = 0; i < contents.length; i++) {
      final content = contents[i];
      final itemHeight = contentHeights[i];

      switch (content.type) {
        case ReceiptContentType.text:
          final text = content.data as String;
          final fontSize = content.options?['fontSize'] as double? ?? 20;
          final fontFamily =
              content.options?['fontFamily'] as String? ?? 'Siemreap';
          final fontWeight = content.options?['fontWeight'] as FontWeight? ??
              FontWeight.normal;
          final color = content.options?['color'] as Color? ?? Colors.black;

          final textSpan = TextSpan(
            text: text,
            style: TextStyle(
              fontSize: fontSize,
              fontFamily: fontFamily,
              fontWeight: fontWeight,
              color: color,
            ),
          );

          final textPainter = TextPainter(
            text: textSpan,
            textDirection: TextDirection.ltr,
          );

          textPainter.layout(
              maxWidth: printerWidth.toDouble() - (padding * 2));
          textPainter.paint(canvas, Offset(padding, currentY + padding));
          break;

        case ReceiptContentType.centeredText:
          final text = content.data as String;
          final fontSize = content.options?['fontSize'] as double? ?? 24;
          final fontFamily =
              content.options?['fontFamily'] as String? ?? 'Siemreap';
          final fontWeight = content.options?['fontWeight'] as FontWeight? ??
              FontWeight.normal;
          final color = content.options?['color'] as Color? ?? Colors.black;

          final textSpan = TextSpan(
            text: text,
            style: TextStyle(
              fontSize: fontSize,
              fontFamily: fontFamily,
              fontWeight: fontWeight,
              color: color,
            ),
          );

          final textPainter = TextPainter(
            text: textSpan,
            textDirection: TextDirection.ltr,
            textAlign: TextAlign.center,
          );

          textPainter.layout(
              maxWidth: printerWidth.toDouble() - (padding * 2));

          // Center text horizontally
          final dx = (printerWidth - textPainter.width) / 2;
          textPainter.paint(
              canvas, Offset(dx > 0 ? dx : padding, currentY + padding));
          break;

        case ReceiptContentType.multiColumn:
          final columns = content.data as List<PrintColumn>;
          final rowPadding = content.options?['padding'] as double? ?? 10;

          for (var col in columns) {
            final textSpan = TextSpan(
              text: col.text,
              style: TextStyle(
                fontSize: col.fontSize,
                fontWeight: col.fontWeight,
                fontFamily: col.fontFamily,
                color: col.color,
              ),
            );

            final textPainter = TextPainter(
              text: textSpan,
              textDirection: TextDirection.ltr,
              textAlign: col.align,
            );

            textPainter.layout(maxWidth: printerWidth.toDouble());

            double dx = col.x;
            if (col.align == TextAlign.center) {
              dx = col.x - textPainter.width / 2;
            } else if (col.align == TextAlign.right) {
              dx = col.x - textPainter.width;
            }

            textPainter.paint(canvas, Offset(dx, currentY + rowPadding));
          }
          break;

        case ReceiptContentType.barcode:
          final data = content.data as String;
          final type =
              content.options?['type'] as BarcodeType? ?? BarcodeType.qrCode;
          final height = content.options?['height'] as int? ?? 100;
          final width = content.options?['width'] as int? ?? 2;
          final textPosition =
              content.options?['textPosition'] as BarcodeTextPosition? ??
                  BarcodeTextPosition.below;

          // For Code128 barcode
          if (type == BarcodeType.code128) {
            // Generate barcode pattern
            final List<int> encodedData =
                BarcodeGenerator.encodeCode128(data);
            final List<String> patterns =
                BarcodeGenerator.generateCode128Patterns(encodedData);
            final String fullPattern = patterns.join();

            // Calculate barcode dimensions
            final double barMinWidth = width.toDouble();
            final double barcodeWidth = fullPattern.length * barMinWidth;
            final startX =
                (printerWidth - barcodeWidth) / 2; // Center the barcode
            double currentX = startX;

            // Draw barcode bars
            for (int j = 0; j < fullPattern.length; j++) {
              if (fullPattern[j] == '1') {
                canvas.drawRect(
                  Rect.fromLTWH(currentX, currentY + padding, barMinWidth,
                      height.toDouble()),
                  Paint()..color = Colors.black,
                );
              }
              currentX += barMinWidth;
            }

            // Draw text under barcode if requested
            if (textPosition != BarcodeTextPosition.none) {
              final textY = currentY + height + padding + 5;

              final textPainter = TextPainter(
                text: TextSpan(
                  text: data,
                  style: const TextStyle(
                    fontSize: 16,
                    color: Colors.black,
                  ),
                ),
                textDirection: TextDirection.ltr,
                textAlign: TextAlign.center,
              );

              textPainter.layout(maxWidth: printerWidth.toDouble());

              // Center text under barcode
              final dx = (printerWidth - textPainter.width) / 2;
              textPainter.paint(canvas, Offset(dx, textY));
            }
          } else {
            // For other barcode types (placeholder implementation)
            final textPainter = TextPainter(
              text: TextSpan(
                text: "Barcode: $data",
                style: const TextStyle(
                  fontSize: 16,
                  color: Colors.black,
                ),
              ),
              textDirection: TextDirection.ltr,
            );

            textPainter.layout(
                maxWidth: printerWidth.toDouble() - (padding * 2));
            textPainter.paint(canvas, Offset(padding, currentY + padding));
          }
          break;

        case ReceiptContentType.image:
          // Image handling (placeholder implementation)
          // In a real implementation, you would load and draw the image here
          break;

        case ReceiptContentType.blankLines:
          // Nothing to draw for blank lines, we just add space
          break;
      }

      // Move to the next position
      currentY += itemHeight;
    }

    // Convert to image
    final picture = recorder.endRecording();
    final img = await picture.toImage(printerWidth, totalHeight.ceil());
    final byteData = await img.toByteData(format: ImageByteFormat.png);
    final buffer = byteData!.buffer.asUint8List();

    // Save to temp file
    await File(tempPath).writeAsBytes(buffer);

    // Print the image as a single receipt
    final result = await PrinterCore.printImage(
      tempPath,
      width: printerWidth,
      alignment: TextFormatter.convertAlignment(Alignment.center),
    );

    // Cut paper if requested
    if (result && cutPaperAfter) {
      await PrinterCore.cutPaper();
    }

    return result;
  } catch (e) {
    print("Error printing complex receipt: $e");
    return false;
  }
}