thermal_unicode_print 0.0.1 copy "thermal_unicode_print: ^0.0.1" to clipboard
thermal_unicode_print: ^0.0.1 copied to clipboard

Prints any Unicode script (Bangla, Arabic, Devanagari, ...) on ESC/POS thermal printers by rasterizing text with dart:ui instead of screenshotting a Flutter widget tree. Renders one bounded line/row a [...]

example/lib/main.dart

import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart';
import 'package:thermal_unicode_print/thermal_unicode_print.dart';

// Bundled locally (see pubspec.yaml) rather than fetched at runtime like
// google_fonts does — a POS device at the counter has to be able to print
// a Bangla receipt with no internet connection.
const String _banglaFont = 'NotoSansBengali';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Pubspec-declared fonts load asynchronously in the background and don't
  // block the first frame, so the very first receipt preview could
  // otherwise render with a fallback font for one frame before the real one
  // swaps in. Preloading here avoids that flash.
  final fontBytes = await rootBundle.load(
    'assets/fonts/NotoSansBengali-Variable.ttf',
  );
  await (FontLoader(_banglaFont)..addFont(Future.value(fontBytes))).load();

  runApp(const DemoApp());
}

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'thermal_unicode_print demo',
      theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
      home: const ReceiptDemoPage(),
    );
  }
}

/// One printable unit of the demo receipt. Implementing both [preview] (PNG,
/// for the on-screen mock-up) and [escPos] (raster bytes for a real printer)
/// keeps the receipt content defined exactly once.
abstract class ReceiptStep {
  Future<Uint8List> preview(ThermalUnicodeRenderer renderer);
  Future<List<int>> escPos(ThermalUnicodeRenderer renderer, Generator gen);
}

class TextStep implements ReceiptStep {
  final String text;
  final TextAlign align;
  final TextStyle style;

  TextStep(
    this.text, {
    this.align = TextAlign.left,
    TextStyle? style,
  }) : style = style ?? const TextStyle(fontFamily: _banglaFont, fontSize: 28);

  @override
  Future<Uint8List> preview(ThermalUnicodeRenderer renderer) =>
      renderer.textLinePng(text, align: align, style: style);

  @override
  Future<List<int>> escPos(ThermalUnicodeRenderer renderer, Generator gen) =>
      renderer.textLine(gen, text, align: align, style: style);
}

class RowStep implements ReceiptStep {
  final List<ThermalCell> cells;
  final TextStyle style;

  RowStep(this.cells, {TextStyle? style})
    : style = style ?? const TextStyle(fontFamily: _banglaFont, fontSize: 22);

  @override
  Future<Uint8List> preview(ThermalUnicodeRenderer renderer) =>
      renderer.rowPng(cells, style: style);

  @override
  Future<List<int>> escPos(ThermalUnicodeRenderer renderer, Generator gen) =>
      renderer.row(gen, cells, style: style);
}

class DividerStep implements ReceiptStep {
  @override
  Future<Uint8List> preview(ThermalUnicodeRenderer renderer) =>
      renderer.dividerPng();

  @override
  Future<List<int>> escPos(ThermalUnicodeRenderer renderer, Generator gen) =>
      renderer.divider(gen);
}

/// Paper widths in dots at the typical 203dpi.
enum PaperWidth {
  mm58(384, PaperSize.mm58),
  mm80(576, PaperSize.mm80);

  final int dots;
  final PaperSize posSize;
  const PaperWidth(this.dots, this.posSize);
}

class ReceiptDemoPage extends StatefulWidget {
  const ReceiptDemoPage({super.key});

  @override
  State<ReceiptDemoPage> createState() => _ReceiptDemoPageState();
}

class _ReceiptDemoPageState extends State<ReceiptDemoPage> {
  PaperWidth _paper = PaperWidth.mm58;
  final TextEditingController _customTextController = TextEditingController(
    text: 'আপনার নিজের বাংলা টেক্সট এখানে লিখুন',
  );

  List<BluetoothInfo> _devices = [];
  bool _connecting = false;
  bool _printing = false;
  String? _status;

  List<ReceiptStep> _buildDemoReceipt() {
    return [
      TextStep(
        'ফ্রেশো শপ',
        align: TextAlign.center,
        style: const TextStyle(
          fontFamily: _banglaFont,
          fontSize: 34,
          fontWeight: FontWeight.bold,
        ),
      ),
      TextStep(
        'ঢাকা, বাংলাদেশ',
        align: TextAlign.center,
        style: const TextStyle(fontFamily: _banglaFont, fontSize: 20),
      ),
      DividerStep(),
      RowStep([
        ThermalCell(
          'পণ্য',
          flex: 5,
          bold: true,
        ),
        ThermalCell('দাম', flex: 3, align: TextAlign.center, bold: true),
        ThermalCell('পরিমাণ', flex: 2, align: TextAlign.center, bold: true),
        ThermalCell('মোট', flex: 3, align: TextAlign.right, bold: true),
      ]),
      DividerStep(),
      RowStep([
        ThermalCell('চাল (মিনিকেট)', flex: 5),
        ThermalCell('৬৫.০০', flex: 3, align: TextAlign.center),
        ThermalCell('২', flex: 2, align: TextAlign.center),
        ThermalCell('১৩০.০০', flex: 3, align: TextAlign.right),
      ]),
      RowStep([
        ThermalCell('সয়াবিন তেল', flex: 5),
        ThermalCell('১৮৫.০০', flex: 3, align: TextAlign.center),
        ThermalCell('১', flex: 2, align: TextAlign.center),
        ThermalCell('১৮৫.০০', flex: 3, align: TextAlign.right),
      ]),
      DividerStep(),
      RowStep([
        ThermalCell('সর্বমোট', flex: 8, bold: true),
        ThermalCell('৩১৫.০০', flex: 5, align: TextAlign.right, bold: true),
      ]),
      DividerStep(),
      TextStep(
        'ধন্যবাদ, আবার আসবেন',
        align: TextAlign.center,
        style: const TextStyle(fontFamily: _banglaFont, fontSize: 20),
      ),
    ];
  }

  // print_bluetooth_thermal never requests Android 12+ runtime Bluetooth
  // permissions itself (its native permission-request code path is dead) —
  // it just silently hangs if they aren't granted. So every entry point that
  // touches the plugin has to secure permissions first.
  Future<bool> _ensureBluetoothPermissions() async {
    final statuses = await [
      Permission.bluetoothScan,
      Permission.bluetoothConnect,
    ].request();
    final granted = statuses.values.every((s) => s.isGranted);
    if (!granted) {
      setState(
        () => _status =
            'Bluetooth permission denied. Enable it in system settings to '
            'find or connect to printers.',
      );
    }
    return granted;
  }

  Future<void> _findPairedDevices() async {
    if (!await _ensureBluetoothPermissions()) return;
    setState(() => _status = 'Scanning paired devices…');
    final devices = await PrintBluetoothThermal.pairedBluetooths;
    setState(() {
      _devices = devices;
      _status = devices.isEmpty
          ? 'No paired Bluetooth printers found.'
          : '${devices.length} paired device(s) found.';
    });
  }

  Future<void> _connect(BluetoothInfo device) async {
    if (!await _ensureBluetoothPermissions()) return;
    setState(() => _connecting = true);
    final ok = await PrintBluetoothThermal.connect(
      macPrinterAddress: device.macAdress,
    );
    setState(() {
      _connecting = false;
      _status = ok
          ? 'Connected to ${device.name}.'
          : 'Failed to connect to ${device.name}.';
    });
  }

  Future<void> _printDemoReceipt() async {
    final bool connected = await PrintBluetoothThermal.connectionStatus;
    if (!connected) {
      setState(() => _status = 'Connect to a printer first.');
      return;
    }

    setState(() {
      _printing = true;
      _status = 'Printing…';
    });

    final renderer = ThermalUnicodeRenderer(dotsWidth: _paper.dots);
    final profile = await CapabilityProfile.load();
    final generator = Generator(_paper.posSize, profile);

    // Each step is rasterized and sent one at a time — output size per step
    // stays constant no matter how long the whole receipt is, unlike
    // screenshotting the entire receipt as one widget.
    final List<int> bytes = [];
    for (final step in _buildDemoReceipt()) {
      bytes.addAll(await step.escPos(renderer, generator));
    }
    bytes.addAll(generator.feed(1));
    bytes.addAll(generator.cut());

    await PrintBluetoothThermal.writeBytes(bytes);

    setState(() {
      _printing = false;
      _status = 'Sent to printer.';
    });
  }

  @override
  Widget build(BuildContext context) {
    final renderer = ThermalUnicodeRenderer(dotsWidth: _paper.dots);

    return Scaffold(
      appBar: AppBar(title: const Text('thermal_unicode_print demo')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          SegmentedButton<PaperWidth>(
            segments: const [
              ButtonSegment(value: PaperWidth.mm58, label: Text('58mm')),
              ButtonSegment(value: PaperWidth.mm80, label: Text('80mm')),
            ],
            selected: {_paper},
            onSelectionChanged: (s) => setState(() => _paper = s.first),
          ),
          const SizedBox(height: 16),
          Text(
            'Live preview — rendered with dart:ui, no widget on screen is '
            'screenshotted to produce this.',
            style: Theme.of(context).textTheme.bodySmall,
          ),
          const SizedBox(height: 8),
          _ReceiptPreview(renderer: renderer, steps: _buildDemoReceipt()),
          const SizedBox(height: 24),
          Text('Try your own text', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 8),
          TextField(
            controller: _customTextController,
            maxLines: 2,
            decoration: const InputDecoration(border: OutlineInputBorder()),
            onChanged: (_) => setState(() {}),
          ),
          const SizedBox(height: 8),
          _PreviewImage(
            future: renderer.textLinePng(
              _customTextController.text,
              style: const TextStyle(fontFamily: _banglaFont, fontSize: 26),
            ),
          ),
          const SizedBox(height: 24),
          Text(
            'Print to a real printer',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              OutlinedButton(
                onPressed: _findPairedDevices,
                child: const Text('Find paired devices'),
              ),
              FilledButton(
                onPressed: _printing ? null : _printDemoReceipt,
                child: Text(_printing ? 'Printing…' : 'Print demo receipt'),
              ),
            ],
          ),
          if (_devices.isNotEmpty)
            ..._devices.map(
              (d) => ListTile(
                title: Text(d.name),
                subtitle: Text(d.macAdress),
                trailing: _connecting
                    ? const SizedBox(
                        width: 16,
                        height: 16,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Icon(Icons.bluetooth),
                onTap: () => _connect(d),
              ),
            ),
          if (_status != null) ...[
            const SizedBox(height: 8),
            Text(_status!, style: Theme.of(context).textTheme.bodyMedium),
          ],
        ],
      ),
    );
  }
}

class _ReceiptPreview extends StatelessWidget {
  final ThermalUnicodeRenderer renderer;
  final List<ReceiptStep> steps;

  const _ReceiptPreview({required this.renderer, required this.steps});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 12),
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border.all(color: Colors.black26),
      ),
      child: Column(
        children: steps
            .map((step) => _PreviewImage(future: step.preview(renderer)))
            .toList(),
      ),
    );
  }
}

class _PreviewImage extends StatelessWidget {
  final Future<Uint8List> future;

  const _PreviewImage({required this.future});

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Uint8List>(
      future: future,
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return const SizedBox(
            height: 24,
            child: Center(
              child: SizedBox(
                width: 14,
                height: 14,
                child: CircularProgressIndicator(strokeWidth: 2),
              ),
            ),
          );
        }
        return Image.memory(snapshot.data!, gaplessPlayback: true);
      },
    );
  }
}
0
likes
150
points
89
downloads

Documentation

API reference

Publisher

verified publisherpritom.me

Weekly Downloads

Prints any Unicode script (Bangla, Arabic, Devanagari, ...) on ESC/POS thermal printers by rasterizing text with dart:ui instead of screenshotting a Flutter widget tree. Renders one bounded line/row at a time, so output size never grows with receipt length.

Repository (GitHub)
View/report issues

Topics

#printing #thermal-printer #bangla #unicode #escpos

License

MIT (license)

Dependencies

esc_pos_utils_plus, flutter, image

More

Packages that depend on thermal_unicode_print