Commit 7e4d35ef authored by Andrey's avatar Andrey

Removed basic ESC/POS classes (moved to esc_pos_utils library). Removed…

Removed basic ESC/POS classes (moved to esc_pos_utils library). Removed bluetooth support (moved to esc_pos_bluetooth library).
parent a9b90937
## [3.0.0]
* Basic ESC/POS classes moved to esc_pos_utils library
* Bluetooth printing moved to esc_pos_bluetooth library
* This library is used to print using a WiFi/Ethernet printer
## [2.1.2] ## [2.1.2]
* Bluetooth printing support for Android * Bluetooth printing support for Android
......
import 'dart:io'; // import 'dart:io';
import 'dart:typed_data'; // import 'dart:typed_data';
import 'package:flutter/services.dart'; // import 'package:flutter/services.dart';
import 'package:image/image.dart'; // import 'package:image/image.dart';
import 'package:esc_pos_printer/esc_pos_printer.dart'; import 'package:esc_pos_printer/esc_pos_printer.dart';
import 'package:esc_pos_utils/esc_pos_utils.dart';
void main() async { void main() async {
final PrinterNetworkManager printerManager = PrinterNetworkManager(); final PrinterNetworkManager printerManager = PrinterNetworkManager();
...@@ -60,10 +61,10 @@ Future<Ticket> testTicket() async { ...@@ -60,10 +61,10 @@ Future<Ticket> testTicket() async {
)); ));
// Print image // Print image
final ByteData data = await rootBundle.load('assets/logo.png'); // final ByteData data = await rootBundle.load('assets/logo.png');
final Uint8List bytes = data.buffer.asUint8List(); // final Uint8List bytes = data.buffer.asUint8List();
final Image image = decodeImage(bytes); // final Image image = decodeImage(bytes);
ticket.image(image); // ticket.image(image);
// Print image using an alternative (obsolette) command // Print image using an alternative (obsolette) command
// ticket.imageRaster(image); // ticket.imageRaster(image);
......
...@@ -7,10 +7,5 @@ ...@@ -7,10 +7,5 @@
*/ */
library esc_pos_printer; library esc_pos_printer;
export './src/barcode.dart';
export './src/enums.dart'; export './src/enums.dart';
export './src/pos_column.dart';
export './src/pos_styles.dart';
export './src/printer_bluetooth_manager.dart';
export './src/printer_network_manager.dart'; export './src/printer_network_manager.dart';
export './src/ticket.dart';
/*
* esc_pos_printer
* Created by Andrey Ushakov
*
* Copyright (c) 2019-2020. All rights reserved.
* See LICENSE for distribution and usage details.
*/
class BarcodeType {
const BarcodeType._internal(this.value);
final int value;
/// UPC-A
static const upcA = BarcodeType._internal(0);
/// UPC-E
static const upcE = BarcodeType._internal(1);
/// JAN13 (EAN13)
static const ean13 = BarcodeType._internal(2);
/// JAN8 (EAN8)
static const ean8 = BarcodeType._internal(3);
/// CODE39
static const code39 = BarcodeType._internal(4);
/// ITF (Interleaved 2 of 5)
static const itf = BarcodeType._internal(5);
/// CODABAR (NW-7)
static const codabar = BarcodeType._internal(6);
}
class BarcodeText {
const BarcodeText._internal(this.value);
final int value;
/// Not printed
static const none = BarcodeText._internal(0);
/// Above the barcode
static const above = BarcodeText._internal(1);
/// Below the barcode
static const below = BarcodeText._internal(2);
/// Both above and below the barcode
static const both = BarcodeText._internal(3);
}
class BarcodeFont {
const BarcodeFont._internal(this.value);
final int value;
static const fontA = BarcodeFont._internal(0);
static const fontB = BarcodeFont._internal(1);
static const fontC = BarcodeFont._internal(2);
static const fontD = BarcodeFont._internal(3);
static const fontE = BarcodeFont._internal(4);
static const specialA = BarcodeFont._internal(97);
static const specialB = BarcodeFont._internal(98);
}
class Barcode {
/// UPC-A
///
/// k = 11, 12
/// d = '0' – '9'
Barcode.upcA(List<dynamic> barcodeData) {
final k = barcodeData.length;
if (![11, 12].contains(k)) {
throw Exception('Barcode: Wrong data range');
}
final numeric = RegExp(r'^[0-9]$');
final bool isDataValid =
barcodeData.every((dynamic d) => numeric.hasMatch(d.toString()));
if (!isDataValid) {
throw Exception('Barcode: Data is not valid');
}
_type = BarcodeType.upcA;
_data = _convertData(barcodeData);
}
/// UPC-E
///
/// k = 6 – 8, 11, 12
/// d = '0' – '9' (However, d0 = '0' when k = 7, 8, 11, 12)
Barcode.upcE(List<dynamic> barcodeData) {
final k = barcodeData.length;
if (![6, 7, 8, 11, 12].contains(k)) {
throw Exception('Barcode: Wrong data range');
}
if ([7, 8, 11, 12].contains(k) && barcodeData[0].toString() != '0') {
throw Exception('Barcode: Data is not valid');
}
final numeric = RegExp(r'^[0-9]$');
final bool isDataValid =
barcodeData.every((dynamic d) => numeric.hasMatch(d.toString()));
if (!isDataValid) {
throw Exception('Barcode: Data is not valid');
}
_type = BarcodeType.upcE;
_data = _convertData(barcodeData);
}
/// JAN13 (EAN13)
///
/// k = 12, 13
/// d = '0' – '9'
Barcode.ean13(List<dynamic> barcodeData) {
final k = barcodeData.length;
if (![12, 13].contains(k)) {
throw Exception('Barcode: Wrong data range');
}
final numeric = RegExp(r'^[0-9]$');
final bool isDataValid =
barcodeData.every((dynamic d) => numeric.hasMatch(d.toString()));
if (!isDataValid) {
throw Exception('Barcode: Data is not valid');
}
_type = BarcodeType.ean13;
_data = _convertData(barcodeData);
}
/// JAN8 (EAN8)
///
/// k = 7, 8
/// d = '0' – '9'
Barcode.ean8(List<dynamic> barcodeData) {
final k = barcodeData.length;
if (![7, 8].contains(k)) {
throw Exception('Barcode: Wrong data range');
}
final numeric = RegExp(r'^[0-9]$');
final bool isDataValid =
barcodeData.every((dynamic d) => numeric.hasMatch(d.toString()));
if (!isDataValid) {
throw Exception('Barcode: Data is not valid');
}
_type = BarcodeType.ean8;
_data = _convertData(barcodeData);
}
/// CODE39
///
/// k >= 1
/// d: '0'–'9', A–Z, SP, $, %, *, +, -, ., /
Barcode.code39(List<dynamic> barcodeData) {
final k = barcodeData.length;
if (k < 1) {
throw Exception('Barcode: Wrong data range');
}
final regex = RegExp(r'^[0-9A-Z \$\%\*\+\-\.\/]$');
final bool isDataValid =
barcodeData.every((dynamic d) => regex.hasMatch(d.toString()));
if (!isDataValid) {
throw Exception('Barcode: Data is not valid');
}
_type = BarcodeType.code39;
_data = _convertData(barcodeData);
}
/// ITF (Interleaved 2 of 5)
///
/// k >= 2 (even number)
/// d = '0'–'9'
Barcode.itf(List<dynamic> barcodeData) {
final k = barcodeData.length;
if (k < 2 || !k.isEven) {
throw Exception('Barcode: Wrong data range');
}
final numeric = RegExp(r'^[0-9]$');
final bool isDataValid =
barcodeData.every((dynamic d) => numeric.hasMatch(d.toString()));
if (!isDataValid) {
throw Exception('Barcode: Data is not valid');
}
_type = BarcodeType.itf;
_data = _convertData(barcodeData);
}
/// CODABAR (NW-7)
///
/// k >= 2
/// d: '0'–'9', A–D, a–d, $, +, −, ., /, :
/// However, d0 = A–D, dk = A–D (65-68)
/// d0 = a-d, dk = a-d (97-100)
Barcode.codabar(List<dynamic> barcodeData) {
final k = barcodeData.length;
if (k < 2) {
throw Exception('Barcode: Wrong data range');
}
final regex = RegExp(r'^[0-9A-Da-d\$\+\-\.\/\:]$');
final bool isDataValid =
barcodeData.every((dynamic d) => regex.hasMatch(d.toString()));
if (!isDataValid) {
throw Exception('Barcode: Data is not valid');
}
if ((_charcode(barcodeData[0]) >= 65 && _charcode(barcodeData[0]) <= 68) &&
!(_charcode(barcodeData[k - 1]) >= 65 &&
_charcode(barcodeData[k - 1]) <= 68)) {
throw Exception('Barcode: Wrong data range');
}
if ((_charcode(barcodeData[0]) >= 97 && _charcode(barcodeData[0]) <= 100) &&
!(_charcode(barcodeData[k - 1]) >= 97 &&
_charcode(barcodeData[k - 1]) <= 100)) {
throw Exception('Barcode: Wrong data range');
}
_type = BarcodeType.codabar;
_data = _convertData(barcodeData);
}
BarcodeType _type;
List<int> _data;
List<int> _convertData(List<dynamic> list) =>
list.map((dynamic d) => d.toString().codeUnitAt(0)).toList();
int _charcode(dynamic ch) => ch.toString().codeUnitAt(0);
BarcodeType get type => _type;
List<int> get data => _data;
}
/*
* esc_pos_printer
* Created by Andrey Ushakov
*
* Copyright (c) 2019-2020. All rights reserved.
* See LICENSE for distribution and usage details.
*/
const esc = '\x1B';
const gs = '\x1D';
const fs = '\x1C';
// Miscellaneous
const cInit = '$esc@'; // Initialize printer
const cBeep = '${esc}B'; // Beeper [count] [duration]
// Mech. Control
const cCutFull = '${gs}V0'; // Full cut
const cCutPart = '${gs}V1'; // Partial cut
// Character
const cReverseOn = '${gs}B1'; // Turn white/black reverse print mode on
const cReverseOff = '${gs}B0'; // Turn white/black reverse print mode off
const cSizeGSn = '$gs!'; // Select character size [N]
const cSizeESCn = '$esc!'; // Select character size [N]
const cUnderlineOff = '$esc-0'; // Turns off underline mode
const cUnderline1dot = '$esc-1'; // Turns on underline mode (1-dot thick)
const cUnderline2dots = '$esc-2'; // Turns on underline mode (2-dots thick)
const cBoldOn = '${esc}E1'; // Turn emphasized mode on
const cBoldOff = '${esc}E0'; // Turn emphasized mode off
const cFontA = '${esc}M0'; // Font A
const cFontB = '${esc}M1'; // Font B
const cTurn90On = '${esc}V1'; // Turn 90° clockwise rotation mode on
const cTurn90Off = '${esc}V0'; // Turn 90° clockwise rotation mode off
const cCodeTable = '${esc}t'; // Select character code table [N]
const cKanjiOn = '$fs&'; // Select Kanji character mode
const cKanjiOff = '$fs.'; // Cancel Kanji character mode
// Print Position
const cAlignLeft = '${esc}a0'; // Left justification
const cAlignCenter = '${esc}a1'; // Centered
const cAlignRight = '${esc}a2'; // Right justification
const cPos = '$esc\$'; // Set absolute print position [nL] [nH]
// Print
const cFeedN = '${esc}d'; // Print and feed n lines [N]
const cReverseFeedN = '${esc}e'; // Print and reverse feed n lines [N]
// Bit Image
const cRasterImg = '${gs}v0'; // Print raster bit image [obsolete command]
const cBitImg = '$esc*'; // Set bit image mode
// Barcode
const cBarcodeSelectPos =
'${gs}H'; // Select print position of HRI characters [N]
const cBarcodeSelectFont = '${gs}f'; // Select font for HRI characters [N]
const cBarcodeSetH = '${gs}h'; // Set barcode height [N]
const cBarcodeSetW = '${gs}w'; // Set barcode width [N]
const cBarcodePrint = '${gs}k'; // Print barcode
...@@ -6,10 +6,6 @@ ...@@ -6,10 +6,6 @@
* See LICENSE for distribution and usage details. * See LICENSE for distribution and usage details.
*/ */
enum PosTextAlign { left, center, right }
enum PosCutMode { full, partial }
enum PosFontType { fontA, fontB }
class PosPrintResult { class PosPrintResult {
const PosPrintResult._internal(this.value); const PosPrintResult._internal(this.value);
final int value; final int value;
...@@ -38,95 +34,3 @@ class PosPrintResult { ...@@ -38,95 +34,3 @@ class PosPrintResult {
} }
} }
} }
class PosTextSize {
const PosTextSize._internal(this.value);
final int value;
static const size1 = PosTextSize._internal(1);
static const size2 = PosTextSize._internal(2);
static const size3 = PosTextSize._internal(3);
static const size4 = PosTextSize._internal(4);
static const size5 = PosTextSize._internal(5);
static const size6 = PosTextSize._internal(6);
static const size7 = PosTextSize._internal(7);
static const size8 = PosTextSize._internal(8);
static int decSize(PosTextSize height, PosTextSize width) =>
16 * (width.value - 1) + (height.value - 1);
}
class PaperSize {
const PaperSize._internal(this.value);
final int value;
static const mm58 = PaperSize._internal(1);
static const mm80 = PaperSize._internal(2);
int get width => value == PaperSize.mm58.value ? 350 : 512;
}
class PosBeepDuration {
const PosBeepDuration._internal(this.value);
final int value;
static const beep50ms = PosBeepDuration._internal(1);
static const beep100ms = PosBeepDuration._internal(2);
static const beep150ms = PosBeepDuration._internal(3);
static const beep200ms = PosBeepDuration._internal(4);
static const beep250ms = PosBeepDuration._internal(5);
static const beep300ms = PosBeepDuration._internal(6);
static const beep350ms = PosBeepDuration._internal(7);
static const beep400ms = PosBeepDuration._internal(8);
static const beep450ms = PosBeepDuration._internal(9);
}
class PosCodeTable {
const PosCodeTable._internal(this.value);
final int value;
/// PC437 - U.S.A., Standard Europe
static const pc437 = PosCodeTable._internal(0);
/// Katakana
static const katakana = PosCodeTable._internal(1);
/// PC850 Multilingual
static const pc850 = PosCodeTable._internal(2);
/// PC860 - Portuguese
static const pc860 = PosCodeTable._internal(3);
/// PC863 - Canadian-French
static const pc863 = PosCodeTable._internal(4);
/// PC865 - Nordic
static const pc865 = PosCodeTable._internal(5);
/// Western Europe
static const westEur = PosCodeTable._internal(6);
/// Greek
static const greek = PosCodeTable._internal(7);
/// PC737 - Greek
static const pc737 = PosCodeTable._internal(64);
/// PC851 - Greek
static const pc851 = PosCodeTable._internal(65);
/// PC869 - Greek
static const pc869 = PosCodeTable._internal(66);
/// PC928 - Greek
static const pc928 = PosCodeTable._internal(67);
/// PC866 - Cyrillic #2
static const pc866 = PosCodeTable._internal(17);
/// PC852 - Latin2
static const pc852 = PosCodeTable._internal(18);
/// WPC1252 - Latin1
static const wpc1252 = PosCodeTable._internal(71);
/// Space page
static const spacePage = PosCodeTable._internal(255);
}
/*
* esc_pos_printer
* Created by Andrey Ushakov
*
* Copyright (c) 2019-2020. All rights reserved.
* See LICENSE for distribution and usage details.
*/
import 'pos_styles.dart';
/// Column contains text, styles and width (an integer in 1..12 range)
class PosColumn {
PosColumn({
this.text = '',
this.containsChinese = false,
this.width = 2,
this.styles = const PosStyles(),
}) {
if (width < 1 || width > 12) {
throw Exception('Column width must be between 1..12');
}
}
String text;
bool containsChinese;
int width;
PosStyles styles;
}
/*
* esc_pos_printer
* Created by Andrey Ushakov
*
* Copyright (c) 2019-2020. All rights reserved.
* See LICENSE for distribution and usage details.
*/
import 'enums.dart';
/// Text styles
class PosStyles {
const PosStyles({
this.bold = false,
this.reverse = false,
this.underline = false,
this.turn90 = false,
this.align = PosTextAlign.left,
this.height = PosTextSize.size1,
this.width = PosTextSize.size1,
this.fontType = PosFontType.fontA,
this.codeTable,
});
final bool bold;
final bool reverse;
final bool underline;
final bool turn90;
final PosTextAlign align;
final PosTextSize height;
final PosTextSize width;
final PosFontType fontType;
final PosCodeTable codeTable;
}
/*
* esc_pos_printer
* Created by Andrey Ushakov
*
* Copyright (c) 2019-2020. All rights reserved.
* See LICENSE for distribution and usage details.
*/
import 'dart:async';
import 'package:esc_pos_printer/esc_pos_printer.dart';
import 'package:rxdart/rxdart.dart';
import 'package:flutter_bluetooth_basic/flutter_bluetooth_basic.dart';
/// Bluetooth printer
class PrinterBluetooth {
PrinterBluetooth(this._device);
final BluetoothDevice _device;
String get name => _device.name;
String get address => _device.address;
int get type => _device.type;
}
/// Printer Bluetooth Manager
class PrinterBluetoothManager {
final BluetoothManager _bluetoothManager = BluetoothManager.instance;
bool _isPrinting = false;
bool _isConnected = false;
StreamSubscription _scanResultsSubscription;
StreamSubscription _isScanningSubscription;
PrinterBluetooth _selectedPrinter;
final BehaviorSubject<bool> _isScanning = BehaviorSubject.seeded(false);
Stream<bool> get isScanningStream => _isScanning.stream;
final BehaviorSubject<List<PrinterBluetooth>> _scanResults =
BehaviorSubject.seeded([]);
Stream<List<PrinterBluetooth>> get scanResults => _scanResults.stream;
Future _runDelayed(int seconds) {
return Future<dynamic>.delayed(Duration(seconds: seconds));
}
void startScan(Duration timeout) async {
_scanResults.add(<PrinterBluetooth>[]);
_bluetoothManager.startScan(timeout: Duration(seconds: 4));
_scanResultsSubscription = _bluetoothManager.scanResults.listen((devices) {
_scanResults.add(devices.map((d) => PrinterBluetooth(d)).toList());
});
_isScanningSubscription =
_bluetoothManager.isScanning.listen((isScanningCurrent) async {
// If isScanning value changed (scan just stopped)
if (_isScanning.value && !isScanningCurrent) {
_scanResultsSubscription.cancel();
_isScanningSubscription.cancel();
}
_isScanning.add(isScanningCurrent);
});
}
void stopScan() async {
await _bluetoothManager.stopScan();
}
void selectPrinter(PrinterBluetooth printer) {
_selectedPrinter = printer;
}
Future<PosPrintResult> writeBytes(List<int> bytes) async {
final Completer<PosPrintResult> completer = Completer();
const int timeout = 5;
if (_selectedPrinter == null) {
return Future<PosPrintResult>.value(PosPrintResult.printerNotSelected);
} else if (_isScanning.value) {
return Future<PosPrintResult>.value(PosPrintResult.scanInProgress);
} else if (_isPrinting) {
return Future<PosPrintResult>.value(PosPrintResult.printInProgress);
}
_isPrinting = true;
// We have to rescan before connecting, otherwise we can connect only once
await _bluetoothManager.startScan(timeout: Duration(seconds: 1));
await _bluetoothManager.stopScan();
// Connect
await _bluetoothManager.connect(_selectedPrinter._device);
// Subscribe to the events
_bluetoothManager.state.listen((state) async {
switch (state) {
case BluetoothManager.CONNECTED:
// To avoid double call
if (!_isConnected) {
await _bluetoothManager.writeData(bytes);
completer.complete(PosPrintResult.success);
}
// TODO sending disconnect signal should be event-based
_runDelayed(3).then((dynamic v) async {
await _bluetoothManager.disconnect();
_isPrinting = false;
});
_isConnected = true;
break;
case BluetoothManager.DISCONNECTED:
_isConnected = false;
break;
default:
break;
}
});
// Printing timeout
_runDelayed(timeout).then((dynamic v) async {
if (_isPrinting) {
_isPrinting = false;
completer.complete(PosPrintResult.timeout);
}
});
return completer.future;
}
Future<PosPrintResult> printTicket(Ticket ticket) async {
if (ticket == null || ticket.bytes.isEmpty) {
return Future<PosPrintResult>.value(PosPrintResult.ticketEmpty);
}
return writeBytes(ticket.bytes);
}
}
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
*/ */
import 'dart:io'; import 'dart:io';
import 'package:esc_pos_utils/esc_pos_utils.dart';
import './enums.dart'; import './enums.dart';
import './ticket.dart';
/// Printer Network Manager /// Printer Network Manager
class PrinterNetworkManager { class PrinterNetworkManager {
......
/*
* esc_pos_printer
* Created by Andrey Ushakov
*
* Copyright (c) 2019-2020. All rights reserved.
* See LICENSE for distribution and usage details.
*/
import 'dart:convert';
import 'dart:typed_data';
import 'package:gbk_codec/gbk_codec.dart';
import 'package:hex/hex.dart';
import 'package:image/image.dart';
import 'barcode.dart';
import 'commands.dart';
import 'enums.dart';
import 'pos_column.dart';
import 'pos_styles.dart';
class Ticket {
Ticket(this._paperSize) {
reset();
}
List<int> bytes = [];
PosCodeTable _codeTable;
final PaperSize _paperSize;
/// Set global code table which will be used instead of the default printer's code table
void setGlobalCodeTable(PosCodeTable codeTable) {
_codeTable = codeTable;
if (codeTable != null) {
bytes += Uint8List.fromList(
List.from(cCodeTable.codeUnits)..add(codeTable.value),
);
}
}
double _colIndToPosition(int colInd) {
final int width = _paperSize.width;
return colInd == 0 ? 0 : (width * colInd / 11 - 1);
}
/// Generic print for internal use
///
/// [colInd] range: 0..11
void _text(
String text, {
PosStyles styles = const PosStyles(),
int colInd = 0,
bool kanjiOff = true,
int colWidth = 12,
}) {
const charLen = 11.625; // 48 symbols per line for 80mm and 32 for 58mm
double fromPos = _colIndToPosition(colInd);
// Align
if (colWidth == 12) {
bytes += latin1.encode(styles.align == PosTextAlign.left
? cAlignLeft
: (styles.align == PosTextAlign.center ? cAlignCenter : cAlignRight));
} else {
final double toPos = _colIndToPosition(colInd + colWidth) - 5;
final double textLen = text.length * charLen;
if (styles.align == PosTextAlign.right) {
fromPos = toPos - textLen;
} else if (styles.align == PosTextAlign.center) {
fromPos = fromPos + (toPos - fromPos) / 2 - textLen / 2;
}
}
final hexStr = fromPos.round().toRadixString(16).padLeft(3, '0');
final hexPair = HEX.decode(hexStr);
bytes += styles.bold ? cBoldOn.codeUnits : cBoldOff.codeUnits;
bytes += styles.turn90 ? cTurn90On.codeUnits : cTurn90Off.codeUnits;
bytes += styles.reverse ? cReverseOn.codeUnits : cReverseOff.codeUnits;
bytes +=
styles.underline ? cUnderline1dot.codeUnits : cUnderlineOff.codeUnits;
bytes += styles.fontType == PosFontType.fontA
? cFontA.codeUnits
: cFontB.codeUnits;
// Text size
bytes += Uint8List.fromList(
List.from(cSizeGSn.codeUnits)
..add(PosTextSize.decSize(styles.height, styles.width)),
);
// Position
bytes += Uint8List.fromList(
List.from(cPos.codeUnits)..addAll([hexPair[1], hexPair[0]]),
);
// Cancel Kanji mode
if (kanjiOff) {
bytes += cKanjiOff.codeUnits;
} else {
bytes += cKanjiOn.codeUnits;
}
// Set local code table
if (styles.codeTable != null) {
bytes += Uint8List.fromList(
List.from(cCodeTable.codeUnits)..add(styles.codeTable.value),
);
}
if (kanjiOff) {
bytes += latin1.encode(text);
} else {
bytes += gbk_bytes.encode(text);
}
}
/// Sens raw command(s)
void rawBytes(List<int> cmd, {bool kanjiOff = true}) {
if (kanjiOff) {
bytes += cKanjiOff.codeUnits;
}
bytes += Uint8List.fromList(cmd);
}
void text(
String text, {
PosStyles styles = const PosStyles(),
int linesAfter = 0,
bool containsChinese = false,
}) {
if (!containsChinese) {
_text(
text,
styles: styles,
kanjiOff: !containsChinese,
);
emptyLines(linesAfter + 1); // at least ine line break after the text
reset();
} else {
_mixedKanji(text, styles: styles, linesAfter: linesAfter);
}
}
/// Break text into chinese/non-chinese lexemes
List _getLexemes(String text) {
bool _isChinese(String ch) {
return ch.codeUnitAt(0) > 255 ? true : false;
}
final List<String> lexemes = [];
final List<bool> isLexemeChinese = [];
int start = 0;
int end = 0;
bool curLexemeChinese = _isChinese(text[0]);
for (var i = 1; i < text.length; ++i) {
if (curLexemeChinese == _isChinese(text[i])) {
end += 1;
} else {
lexemes.add(text.substring(start, end + 1));
isLexemeChinese.add(curLexemeChinese);
start = i;
end = i;
curLexemeChinese = !curLexemeChinese;
}
}
lexemes.add(text.substring(start, end + 1));
isLexemeChinese.add(curLexemeChinese);
return <dynamic>[lexemes, isLexemeChinese];
}
/// Prints one line of styled mixed (chinese and latin symbols) text
void _mixedKanji(
String text, {
PosStyles styles = const PosStyles(),
int linesAfter = 0,
}) {
final list = _getLexemes(text);
final List<String> lexemes = list[0];
final List<bool> isLexemeChinese = list[1];
// Print each lexeme using codetable OR kanji
for (var i = 0; i < lexemes.length; ++i) {
_text(
lexemes[i],
styles: styles,
kanjiOff: !isLexemeChinese[i],
);
}
emptyLines(linesAfter + 1);
reset();
}
/// Print selected code table.
///
/// If [codeTable] is null, global code table is used.
/// If global code table is null, default printer code table is used.
void codeTable({PosCodeTable codeTable}) {
bytes += cKanjiOff.codeUnits;
if (codeTable != null) {
bytes += Uint8List.fromList(
List.from(cCodeTable.codeUnits)..add(codeTable.value),
);
}
final List<int> list = [];
for (int i = 0; i < 256; i++) {
list.add(i);
}
bytes += Uint8List.fromList(list);
// Back to initial code table
setGlobalCodeTable(_codeTable);
}
/// Print a row.
///
/// A row contains up to 12 columns. A column has a width between 1 and 12.
/// Total width of columns in one row must be equal 12.
void row(List<PosColumn> cols) {
final validSum = cols.fold(0, (int sum, col) => sum + col.width) == 12;
if (!validSum) {
throw Exception('Total columns width must be equal to 12');
}
for (int i = 0; i < cols.length; ++i) {
final colInd =
cols.sublist(0, i).fold(0, (int sum, col) => sum + col.width);
if (!cols[i].containsChinese) {
_text(
cols[i].text,
styles: cols[i].styles,
colInd: colInd,
colWidth: cols[i].width,
);
} else {
final list = _getLexemes(cols[i].text);
final List<String> lexemes = list[0];
final List<bool> isLexemeChinese = list[1];
// Print each lexeme using codetable OR kanji
for (var j = 0; j < lexemes.length; ++j) {
_text(
lexemes[j],
styles: cols[i].styles,
colInd: colInd,
colWidth: cols[i].width,
kanjiOff: !isLexemeChinese[j],
);
}
}
}
emptyLines(1);
reset();
}
/// Beeps [n] times
///
/// Beep [duration] could be between 50 and 450 ms.
void beep({int n = 3, PosBeepDuration duration = PosBeepDuration.beep450ms}) {
if (n <= 0) {
return;
}
int beepCount = n;
if (beepCount > 9) {
beepCount = 9;
}
bytes += Uint8List.fromList(
List.from(cBeep.codeUnits)..addAll([beepCount, duration.value]),
);
beep(n: n - 9, duration: duration);
}
/// Clear the buffer and reset text styles
void reset() {
bytes += cInit.codeUnits;
setGlobalCodeTable(_codeTable);
}
/// Skips [n] lines
///
/// Similar to [feed] but uses an alternative command
void emptyLines(int n) {
if (n > 0) {
bytes += List.filled(n, '\n').join().codeUnits;
}
}
/// Skips [n] lines
///
/// Similar to [emptyLines] but uses an alternative command
void feed(int n) {
if (n >= 0 && n <= 255) {
bytes += Uint8List.fromList(
List.from(cFeedN.codeUnits)..add(n),
);
}
}
/// Reverse feed for [n] lines (if supported by the priner)
void reverseFeed(int n) {
bytes += Uint8List.fromList(
List.from(cReverseFeedN.codeUnits)..add(n),
);
}
/// Cut the paper
///
/// [mode] is used to define the full or partial cut (if supported by the priner)
void cut({PosCutMode mode = PosCutMode.full}) {
emptyLines(5);
if (mode == PosCutMode.partial) {
bytes += cCutPart.codeUnits;
} else {
bytes += cCutFull.codeUnits;
}
}
/// Generate multiple bytes for a number: In lower and higher parts, or more parts as needed.
///
/// [value] Input number
/// [bytesNb] The number of bytes to output (1 - 4)
List<int> _intLowHigh(int value, int bytesNb) {
final dynamic maxInput = 256 << (bytesNb * 8) - 1;
if (bytesNb < 1 || bytesNb > 4) {
throw Exception('Can only output 1-4 bytes');
}
if (value < 0 || value > maxInput) {
throw Exception(
'Number too large. Can only output up to $maxInput in $bytesNb bytes');
}
final List<int> res = <int>[];
int buf = value;
for (int i = 0; i < bytesNb; ++i) {
res.add(buf % 256);
buf = buf ~/ 256;
}
return res;
}
/// Replaces a single bit in a 32-bit unsigned integer.
int _transformUint32Bool(int uint32, int shift, bool newValue) {
return ((0xFFFFFFFF ^ (0x1 << shift)) & uint32) |
((newValue ? 1 : 0) << shift);
}
/// Merges each 8 values (bits) into one byte
List<int> _packBitsIntoBytes(List<int> bytes) {
const pxPerLine = 8;
final List<int> res = <int>[];
const threshold = 127; // set the greyscale -> b/w threshold here
for (int i = 0; i < bytes.length; i += pxPerLine) {
int newVal = 0;
for (int j = 0; j < pxPerLine; j++) {
newVal = _transformUint32Bool(
newVal,
pxPerLine - j,
bytes[i + j] > threshold,
);
}
res.add(newVal ~/ 2);
}
return res;
}
/// Print image using (ESC *) command
///
/// [image] is an instanse of class from [Image library](https://pub.dev/packages/image)
void image(Image imgSrc) {
final Image image = Image.from(imgSrc); // make a copy
const bool highDensityHorizontal = true;
const bool highDensityVertical = true;
invert(image);
flip(image, Flip.horizontal);
final Image imageRotated = copyRotate(image, 270);
const int lineHeight = highDensityVertical ? 3 : 1;
final List<List<int>> blobs = _toColumnFormat(imageRotated, lineHeight * 8);
// Compress according to line density
// Line height contains 8 or 24 pixels of src image
// Each blobs[i] contains greyscale bytes [0-255]
// const int pxPerLine = 24 ~/ lineHeight;
for (int blobInd = 0; blobInd < blobs.length; blobInd++) {
blobs[blobInd] = _packBitsIntoBytes(blobs[blobInd]);
}
final int heightPx = imageRotated.height;
const int densityByte =
(highDensityHorizontal ? 1 : 0) + (highDensityVertical ? 32 : 0);
final List<int> header = List.from(cBitImg.codeUnits);
header.add(densityByte);
header.addAll(_intLowHigh(heightPx, 2));
// Adjust line spacing (for 16-unit line feeds): ESC 3 0x10 (HEX: 0x1b 0x33 0x10)
rawBytes([27, 51, 16]);
for (int i = 0; i < blobs.length; ++i) {
rawBytes(List.from(header)..addAll(blobs[i])..addAll('\n'.codeUnits));
}
// Reset line spacing: ESC 2 (HEX: 0x1b 0x32)
rawBytes([27, 50]);
}
/// Extract slices of an image as equal-sized blobs of column-format data.
///
/// [image] Image to extract from
/// [lineHeight] Printed line height in dots
List<List<int>> _toColumnFormat(Image imgSrc, int lineHeight) {
final Image image = Image.from(imgSrc); // make a copy
// Determine new width: closest integer that is divisible by lineHeight
final int widthPx = (image.width + lineHeight) - (image.width % lineHeight);
final int heightPx = image.height;
// Create a black bottom layer
final biggerImage = copyResize(image, width: widthPx, height: heightPx);
fill(biggerImage, 0);
// Insert source image into bigger one
drawImage(biggerImage, image, dstX: 0, dstY: 0);
int left = 0;
final List<List<int>> blobs = [];
while (left < widthPx) {
final Image slice = copyCrop(biggerImage, left, 0, lineHeight, heightPx);
final Uint8List bytes = slice.getBytes(format: Format.luminance);
blobs.add(bytes);
left += lineHeight;
}
return blobs;
}
/// Print image using (GS v 0) obsolete command
///
/// [image] is an instanse of class from [Image library](https://pub.dev/packages/image)
void imageRaster(
Image imgSrc, {
bool highDensityHorizontal = true,
bool highDensityVertical = true,
}) {
final Image image = Image.from(imgSrc); // make a copy
final int widthPx = image.width;
final int heightPx = image.height;
final int widthBytes = (widthPx + 7) ~/ 8;
final int densityByte =
(highDensityVertical ? 0 : 1) + (highDensityHorizontal ? 0 : 2);
final List<int> header = List.from(cRasterImg.codeUnits);
header.add(densityByte);
header.addAll(_intLowHigh(widthBytes, 2));
header.addAll(_intLowHigh(heightPx, 2));
grayscale(image);
invert(image);
// R/G/B channels are same -> keep only one channel
final List<int> oneChannelBytes = [];
final List<int> buffer = image.getBytes(format: Format.rgba);
for (int i = 0; i < buffer.length; i += 4) {
oneChannelBytes.add(buffer[i]);
}
// Add some empty pixels at the end of each line (to make the width divisible by 8)
final targetWidth = (widthPx + 8) - (widthPx % 8);
final missingPx = targetWidth - widthPx;
final extra = Uint8List(missingPx);
for (int i = 0; i < heightPx; i++) {
final pos = (i * widthPx + widthPx) + i * missingPx;
oneChannelBytes.insertAll(pos, extra);
}
// Pack bits into bytes
final List<int> res = _packBitsIntoBytes(oneChannelBytes);
rawBytes(List.from(header)..addAll(res));
}
/// Print barcode
///
/// [width] range and units are different depending on the printer model.
/// [height] range: 1 - 255. The units depend on the printer model.
/// Width, height, font, text position settings are effective until performing of ESC @, reset or power-off.
void barcode(
Barcode barcode, {
int width,
int height,
BarcodeFont font,
BarcodeText textPos = BarcodeText.below,
}) {
// Set text position
rawBytes(cBarcodeSelectPos.codeUnits + [textPos.value]);
// Set font
if (font != null) {
rawBytes(cBarcodeSelectFont.codeUnits + [font.value]);
}
// Set width
if (width != null && width >= 0) {
rawBytes(cBarcodeSetW.codeUnits + [width]);
}
// Set height
if (height != null && height >= 1 && height <= 255) {
rawBytes(cBarcodeSetH.codeUnits + [height]);
}
// Print barcode
final header = cBarcodePrint.codeUnits + [barcode.type.value];
rawBytes(header + barcode.data + [0]);
}
}
name: esc_pos_printer name: esc_pos_printer
description: The library allows to print receipts using an ESC/POS thermal WiFi/Bluetooth printer. description: The library allows to print receipts using an ESC/POS thermal WiFi printer.
version: 2.1.2 version: 3.0.0
homepage: https://github.com/andrey-ushakov/esc_pos_printer homepage: https://github.com/andrey-ushakov/esc_pos_printer
environment: environment:
...@@ -9,14 +9,11 @@ environment: ...@@ -9,14 +9,11 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
hex: ^0.1.2
image: ^2.1.4
gbk_codec: ^0.3.1+3
rxdart: ^0.23.1 rxdart: ^0.23.1
flutter_bluetooth_basic: ^0.1.2 esc_pos_utils: ^0.1.1
# flutter_bluetooth_basic: # esc_pos_utils:
# git: # git:
# url: git://github.com/andrey-ushakov/flutter_bluetooth_basic # url: git://github.com/andrey-ushakov/esc_pos_utils
# ref: master # ref: master
dev_dependencies: dev_dependencies:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment