diff --git a/core-services/egov-notification-sms/src/main/java/org/egov/web/notification/sms/config/SMSProperties.java b/core-services/egov-notification-sms/src/main/java/org/egov/web/notification/sms/config/SMSProperties.java index 223b9e1a3..e039f20e9 100644 --- a/core-services/egov-notification-sms/src/main/java/org/egov/web/notification/sms/config/SMSProperties.java +++ b/core-services/egov-notification-sms/src/main/java/org/egov/web/notification/sms/config/SMSProperties.java @@ -93,6 +93,9 @@ public class SMSProperties { @Value("${save.sms.entity.enabled}") private boolean isSaveSmsEnable; + @Value("#{'${sms.error.codes}'.split(',')}") + protected List smsDisabledTenantList; + @Setter(AccessLevel.PROTECTED) private List whitelistPatterns; @Setter(AccessLevel.PROTECTED) private List blacklistPatterns; diff --git a/core-services/egov-notification-sms/src/main/java/org/egov/web/notification/sms/consumer/SmsNotificationListener.java b/core-services/egov-notification-sms/src/main/java/org/egov/web/notification/sms/consumer/SmsNotificationListener.java index 703ea3b48..2a1bbba68 100644 --- a/core-services/egov-notification-sms/src/main/java/org/egov/web/notification/sms/consumer/SmsNotificationListener.java +++ b/core-services/egov-notification-sms/src/main/java/org/egov/web/notification/sms/consumer/SmsNotificationListener.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.consumer.*; import org.egov.tracer.kafka.*; +import org.egov.web.notification.sms.config.SMSProperties; import org.egov.web.notification.sms.consumer.contract.SMSRequest; import org.egov.web.notification.sms.models.Category; import org.egov.web.notification.sms.models.RequestContext; @@ -44,6 +45,9 @@ public class SmsNotificationListener { @Value("${kafka.topics.error.sms}") String errorSmsTopic; + @Autowired + protected SMSProperties smsProperties; + @Autowired public SmsNotificationListener( @@ -63,18 +67,20 @@ public void process(HashMap consumerRecord) { SMSRequest request = null; try { request = objectMapper.convertValue(consumerRecord, SMSRequest.class); - if (request.getExpiryTime() != null && request.getCategory() == Category.OTP) { - Long expiryTime = request.getExpiryTime(); - Long currentTime = System.currentTimeMillis(); - if (expiryTime < currentTime) { - log.info("OTP Expired"); - if (!StringUtils.isEmpty(expiredSmsTopic)) - kafkaTemplate.send(expiredSmsTopic, request); + if(!ObjectUtils.isEmpty(request.getTenantId()) && !smsProperties.getSmsDisabledTenantList().contains(request.getTenantId())) { + if (request.getExpiryTime() != null && request.getCategory() == Category.OTP) { + Long expiryTime = request.getExpiryTime(); + Long currentTime = System.currentTimeMillis(); + if (expiryTime < currentTime) { + log.info("OTP Expired"); + if (!StringUtils.isEmpty(expiredSmsTopic)) + kafkaTemplate.send(expiredSmsTopic, request); + } else { + smsService.sendSMS(request.toDomain()); + } } else { smsService.sendSMS(request.toDomain()); } - } else { - smsService.sendSMS(request.toDomain()); } } catch (RestClientException rx) { log.info("Going to backup SMS Service", rx); diff --git a/core-services/egov-notification-sms/src/main/resources/application.properties b/core-services/egov-notification-sms/src/main/resources/application.properties index 25849120a..37e8e8ca6 100644 --- a/core-services/egov-notification-sms/src/main/resources/application.properties +++ b/core-services/egov-notification-sms/src/main/resources/application.properties @@ -108,4 +108,7 @@ spring.kafka.listener.missing-topics-fatal=false #persister topic of sms save.sms.entity.topic = save-sms-entity-application -save.sms.entity.enabled = true \ No newline at end of file +save.sms.entity.enabled = true + +#Disable tenants SMS +sms.disabled.tenants.list=testing \ No newline at end of file diff --git a/frontend/mgramseva/android/gradle/wrapper/gradle-wrapper.properties b/frontend/mgramseva/android/gradle/wrapper/gradle-wrapper.properties index 48072d380..c9ad23990 100644 --- a/frontend/mgramseva/android/gradle/wrapper/gradle-wrapper.properties +++ b/frontend/mgramseva/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip diff --git a/frontend/mgramseva/assets/json/capabilities.json b/frontend/mgramseva/assets/json/capabilities.json new file mode 100644 index 000000000..1a9ec0d34 --- /dev/null +++ b/frontend/mgramseva/assets/json/capabilities.json @@ -0,0 +1,225 @@ +{ + "profiles": { + "default": { + "codePages": { + "0": "CP437", + "1": "CP932", + "2": "CP850", + "3": "CP860", + "4": "CP863", + "5": "CP865", + "6": "Unknown", + "7": "Unknown", + "8": "Unknown", + "11": "CP851", + "12": "CP853", + "13": "CP857", + "14": "CP737", + "15": "ISO_8859-7", + "16": "CP1252", + "17": "CP866", + "18": "CP852", + "19": "CP858", + "20": "Unknown", + "21": "CP874", + "22": "Unknown", + "23": "Unknown", + "24": "Unknown", + "25": "Unknown", + "26": "Unknown", + "30": "TCVN-3-1", + "31": "TCVN-3-2", + "32": "CP720", + "33": "CP775", + "34": "CP855", + "35": "CP861", + "36": "CP862", + "37": "CP864", + "38": "CP869", + "39": "ISO_8859-2", + "40": "ISO_8859-15", + "41": "CP1098", + "42": "CP774", + "43": "CP772", + "44": "CP1125", + "45": "CP1250", + "46": "CP1251", + "47": "CP1253", + "48": "CP1254", + "49": "CP1255", + "50": "CP1256", + "51": "CP1257", + "52": "CP1258", + "53": "RK1048", + "66": "Unknown", + "67": "Unknown", + "68": "Unknown", + "69": "Unknown", + "70": "Unknown", + "71": "Unknown", + "72": "Unknown", + "73": "Unknown", + "74": "Unknown", + "75": "Unknown", + "82": "Unknown", + "254": "Unknown", + "255": "Unknown" + }, + "vendor": "Generic", + "model": "Default", + "description": "Default ESC/POS profile" + }, + + "XP-N160I": { + "codePages": { + "0": "CP437", + "1": "CP932", + "2": "CP850", + "3": "CP860", + "4": "CP863", + "5": "CP865", + "6": "CP1252", + "7": "CP737", + "8": "CP862", + "9": "Unknown", + "10": "Unknown", + "16": "CP1252", + "17": "CP866", + "18": "CP852", + "19": "CP858", + "20": "Unknown", + "21": "Unknown", + "22": "Unknown", + "23": "Unknown", + "24": "CP747", + "25": "CP1257", + "27": "CP1258", + "28": "CP864", + "29": "CP1001", + "30": "Unknown", + "31": "Unknown", + "32": "CP1255", + "50": "CP437", + "51": "CP932", + "52": "CP437", + "53": "CP858", + "54": "CP858", + "55": "CP860", + "56": "CP861", + "57": "CP863", + "58": "CP865", + "59": "CP866", + "60": "CP855", + "61": "CP857", + "62": "CP862", + "63": "CP864", + "64": "CP737", + "65": "CP851", + "66": "CP869", + "67": "CP928", + "68": "CP772", + "69": "CP774", + "70": "CP874", + "71": "CP1252", + "72": "CP1250", + "73": "CP1251", + "74": "CP3840", + "75": "CP3841", + "76": "CP3843", + "77": "CP3844", + "78": "CP3845", + "79": "CP3846", + "80": "CP3847", + "81": "CP3848", + "82": "CP1001", + "83": "CP2001", + "84": "CP3001", + "85": "CP3002", + "86": "CP3011", + "87": "CP3012", + "88": "CP3021", + "89": "CP3041" + }, + "vendor": "Xprinter", + "model": "XP-N160I", + "description": "" + }, + + "RP80USE": { + "codePages": { + "0": "CP437", + "1": "CP932", + "2": "CP850", + "3": "CP860", + "4": "CP863", + "5": "CP865", + "6": "CP1251", + "7": "CP866", + "8": "Unknown", + "9": "Unknown", + "10": "Unknown", + "15": "CP862", + "16": "CP1252", + "17": "CP1253", + "18": "CP852", + "19": "CP858", + "20": "Unknown", + "21": "Unknown", + "22": "CP864", + "23": "ISO_8859-1", + "24": "CP737", + "25": "CP1257", + "26": "Unknown", + "27": "CP720", + "28": "CP855", + "29": "CP857", + "30": "CP1250", + "31": "CP775", + "32": "CP1254", + "34": "CP1256", + "35": "CP1258", + "36": "ISO_8859-2", + "37": "ISO_8859-3", + "38": "ISO_8859-4", + "39": "ISO_8859-5", + "40": "ISO_8859-6", + "41": "ISO_8859-7", + "42": "ISO_8859-8", + "43": "ISO_8859-9", + "44": "ISO_8859-15", + "45": "Unknown", + "46": "CP856", + "47": "CP874" + }, + "vendor": "Rongta", + "model": "RP80USE", + "description": "" + }, + + "TP806L": { + "codePages": { + "0": "PC437", + "1": "Katakana", + "2": "PC850", + "3": "PC860", + "4": "PC863", + "5": "PC865", + "13": "PC857", + "14": "PC737", + "15": "ISO8859-7", + "16": "WPC1252", + "17": "PC866", + "18": "PC852", + "19": "PC858", + "20": "KU42", + "32": "PC720", + "37": "PC864", + "50": "WPC1256", + "63": "ISO-8859-6" + }, + "vendor": "HPRT", + "model": "TP806L", + "description": "" + } + } +} diff --git a/frontend/mgramseva/lib/components/house_connection_and_bill/consumer_bill_payment.dart b/frontend/mgramseva/lib/components/house_connection_and_bill/consumer_bill_payment.dart index 5a4334602..6800b26dd 100644 --- a/frontend/mgramseva/lib/components/house_connection_and_bill/consumer_bill_payment.dart +++ b/frontend/mgramseva/lib/components/house_connection_and_bill/consumer_bill_payment.dart @@ -123,7 +123,7 @@ class ConsumerBillPaymentsState extends State { navigatorKey.currentContext!) .translate(i18.consumerReciepts .GRAM_PANCHAYAT_WATER_SUPPLY_AND_SANITATION), - textScaler: TextScaler.linear(kIsWeb ? 3 : 1), + textScaleFactor: kIsWeb ? 3 : 1, maxLines: 3, style: TextStyle( color: Colors.black, @@ -146,7 +146,7 @@ class ConsumerBillPaymentsState extends State { ApplicationLocalizations.of( navigatorKey.currentContext!) .translate(i18.consumerReciepts.WATER_RECEIPT), - textScaler: TextScaler.linear(kIsWeb ? 3 : 1), + textScaleFactor: kIsWeb ? 3 : 1, style: TextStyle( color: Colors.black, fontSize: 10, @@ -238,7 +238,7 @@ class ConsumerBillPaymentsState extends State { height: 8, ), Text('- - *** - -', - textScaler: TextScaler.linear(kIsWeb ? 3 : 1), + textScaleFactor: kIsWeb ? 3 : 1, textAlign: TextAlign.start, style: TextStyle( color: Colors.black, @@ -246,7 +246,7 @@ class ConsumerBillPaymentsState extends State { fontWeight: FontWeight.bold)), Text( "${ApplicationLocalizations.of(navigatorKey.currentContext!).translate(i18.common.RECEIPT_FOOTER)}", - textScaler: TextScaler.linear(kIsWeb ? 3 : 1), + textScaleFactor: kIsWeb ? 3 : 1, textAlign: TextAlign.start, style: TextStyle( color: Colors.black, diff --git a/frontend/mgramseva/lib/providers/collect_payment_provider.dart b/frontend/mgramseva/lib/providers/collect_payment_provider.dart index 1661c785c..2e8f00870 100644 --- a/frontend/mgramseva/lib/providers/collect_payment_provider.dart +++ b/frontend/mgramseva/lib/providers/collect_payment_provider.dart @@ -406,7 +406,7 @@ class CollectPaymentProvider with ChangeNotifier { height: 8, ), Text('- - *** - -', - textScaler: TextScaler.linear(kIsWeb ? 3 : 1), + textScaleFactor: kIsWeb ? 3 : 1, textAlign: TextAlign.start, style: TextStyle( color: Colors.black, @@ -414,7 +414,7 @@ class CollectPaymentProvider with ChangeNotifier { fontWeight: FontWeight.bold)), Text( "${ApplicationLocalizations.of(navigatorKey.currentContext!).translate(i18.common.RECEIPT_FOOTER)}", - textScaler: TextScaler.linear(kIsWeb ? 3 : 1), + textScaleFactor: kIsWeb ? 3 : 1, textAlign: TextAlign.start, style: TextStyle( color: Colors.black, diff --git a/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/esc_pos_utils_platform.dart b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/esc_pos_utils_platform.dart new file mode 100644 index 000000000..b10b6cbe1 --- /dev/null +++ b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/esc_pos_utils_platform.dart @@ -0,0 +1,15 @@ +// You have generated a new plugin project without specifying the `--platforms` +// flag. A plugin project with no platform support was generated. To add a +// platform, run `flutter create -t plugin --platforms .` under the +// same directory. You can also find a detailed instruction on how to add +// platforms in the `pubspec.yaml` at +// https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. +library esc_pos_utils_platform; + +export './src/barcode.dart'; +export './src/capability_profile.dart'; +export './src/enums.dart'; +export './src/pos_column.dart'; +export './src/pos_styles.dart'; +export './src/qrcode.dart'; +export './src/generator.dart'; diff --git a/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/barcode.dart b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/barcode.dart new file mode 100644 index 000000000..5114b3603 --- /dev/null +++ b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/barcode.dart @@ -0,0 +1,270 @@ +/* + * esc_pos_utils + * Created by Andrey U. + * + * 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); + + /// CODE128 + static const code128 = BarcodeType._internal(73); +} + +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 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 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 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 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 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 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 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); + } + + /// CODE128 + /// + /// k >= 2 + /// d: '{A'/'{B'/'{C' => '0'–'9', A–D, a–d, $, +, −, ., /, : + /// usage: + /// {A = QRCode type A + /// {B = QRCode type B + /// {C = QRCode type C + /// barcodeData ex.: "{A978020137962".split(""); + Barcode.code128(List barcodeData) { + final k = barcodeData.length; + if (k < 2) { + throw Exception('Barcode: Wrong data range'); + } + + final regex = RegExp(r'^\{[A-C][\x00-\x7F]+$'); + final bool isDataValid = regex.hasMatch(barcodeData.join()); + + if (!isDataValid) { + throw Exception('Barcode: Data is not valid'); + } + + _type = BarcodeType.code128; + _data = _convertData(barcodeData); + } + + BarcodeType? _type; + List? _data; + + List _convertData(List list) => + list.map((dynamic d) => d.toString().codeUnitAt(0)).toList(); + + int _charcode(dynamic ch) => ch.toString().codeUnitAt(0); + + BarcodeType? get type => _type; + List? get data => _data; +} diff --git a/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/capability_profile.dart b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/capability_profile.dart new file mode 100644 index 000000000..2c1631ed9 --- /dev/null +++ b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/capability_profile.dart @@ -0,0 +1,73 @@ +/* + * esc_pos_utils + * Created by Andrey U. + * + * Copyright (c) 2019-2020. All rights reserved. + * See LICENSE for distribution and usage details. + */ + +import 'dart:convert' show json; +import 'package:flutter/services.dart' show rootBundle; + +class CodePage { + CodePage(this.id, this.name); + int id; + String name; +} + +class CapabilityProfile { + CapabilityProfile._internal(this.name, this.codePages); + + /// Public factory + static Future load({String name = 'default'}) async { + final content = await rootBundle.loadString('assets/json/capabilities.json'); + Map capabilities = json.decode(content); + + var profile = capabilities['profiles'][name]; + + if (profile == null) { + throw Exception("The CapabilityProfile '$name' does not exist"); + } + + List list = []; + profile['codePages'].forEach((k, v) { + list.add(CodePage(int.parse(k), v)); + }); + + // Call the private constructor + return CapabilityProfile._internal(name, list); + } + + String name; + List codePages; + + int getCodePageId(String? codePage) { + if (codePages.isEmpty) { + throw Exception("The CapabilityProfile isn't initialized"); + } + + return codePages + .firstWhere((cp) => cp.name == codePage, orElse: () => throw Exception("Code Page '$codePage' isn't defined for this profile")) + .id; + } + + static Future> getAvailableProfiles() async { + final content = await rootBundle.loadString('packages/thermal_printer/resources/capabilities.json'); + Map capabilities = json.decode(content); + + var profiles = capabilities['profiles']; + + List res = []; + + profiles.forEach((k, v) { + res.add({ + 'key': k, + 'vendor': v['vendor'] is String ? v['vendor'] : '', + 'model': v['model'] is String ? v['model'] : '', + 'description': v['description'] is String ? v['description'] : '', + }); + }); + + return res; + } +} diff --git a/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/commands.dart b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/commands.dart new file mode 100644 index 000000000..34d999433 --- /dev/null +++ b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/commands.dart @@ -0,0 +1,68 @@ +/* + * esc_pos_utils + * Created by Andrey U. + * + * 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}B\x01'; // Turn white/black reverse print mode on +const cReverseOff = '${gs}B\x00'; // Turn white/black reverse print mode off +const cSizeGSn = '$gs!'; // Select character size [N] +const cSizeESCn = '$esc!'; // Select character size [N] +const cUnderlineOff = '$esc-\x00'; // Turns off underline mode +const cUnderline1dot = '$esc-\x01'; // Turns on underline mode (1-dot thick) +const cUnderline2dots = '$esc-\x02'; // Turns on underline mode (2-dots thick) +const cBoldOn = '${esc}E\x01'; // Turn emphasized mode on +const cBoldOff = '${esc}E\x00'; // Turn emphasized mode off +const cFontA = '${esc}M\x00'; // Font A +const cFontB = '${esc}M\x01'; // Font B +const cTurn90On = '${esc}V\x01'; // Turn 90° clockwise rotation mode on +const cTurn90Off = '${esc}V\x00'; // 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(L'; // Print image - raster bit format (graphics) +const cRasterImg2 = + '${gs}v0'; // Print image - raster bit format (bitImageRaster) [obsolete] +const cBitImg = '$esc*'; // Print image - column format + +// 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 + +// Cash Drawer Open +const cCashDrawerPin2 = '${esc}p030'; +const cCashDrawerPin5 = '${esc}p130'; + +// QR Code +const cQrHeader = '$gs(k'; diff --git a/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/enums.dart b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/enums.dart new file mode 100644 index 000000000..ac2f60200 --- /dev/null +++ b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/enums.dart @@ -0,0 +1,56 @@ +/* + * esc_pos_utils + * Created by Andrey U. + * + * Copyright (c) 2019-2020. All rights reserved. + * See LICENSE for distribution and usage details. + */ + +enum PosAlign { left, center, right } +enum PosCutMode { full, partial } +enum PosFontType { fontA, fontB } +enum PosDrawer { pin2, pin5 } + +/// Choose image printing function +/// bitImageRaster: GS v 0 (obsolete) +/// graphics: GS ( L +enum PosImageFn { bitImageRaster, graphics } + +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 ? 372 : 558; +} + +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); +} diff --git a/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/generator.dart b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/generator.dart new file mode 100644 index 000000000..82662fb40 --- /dev/null +++ b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/generator.dart @@ -0,0 +1,987 @@ +/* + * esc_pos_utils + * Created by Andrey U. + * + * Copyright (c) 2019-2020. All rights reserved. + * See LICENSE for distribution and usage details. + */ + +import 'dart:convert'; +import 'package:flutter/services.dart'; +import 'package:image/image.dart' as img; +import 'package:gbk_codec/gbk_codec.dart'; +import "package:hex/hex.dart"; +import '../esc_pos_utils_platform.dart'; +import 'commands.dart'; + +class Generator { + Generator(this._paperSize, this._profile, {this.spaceBetweenRows = 5}); + + // Ticket config + final PaperSize _paperSize; + CapabilityProfile _profile; + int? _maxCharsPerLine; + // Global styles + String? _codeTable; + PosFontType? _font; + // Current styles + PosStyles _styles = PosStyles(); + int spaceBetweenRows; + + // ************************ Internal helpers ************************ + int _getMaxCharsPerLine(PosFontType? font) { + if (_paperSize == PaperSize.mm58) { + return (font == null || font == PosFontType.fontA) ? 32 : 42; + } else { + return (font == null || font == PosFontType.fontA) ? 48 : 64; + } + } + + // charWidth = default width * text size multiplier + double _getCharWidth(PosStyles styles, {int? maxCharsPerLine}) { + int charsPerLine = _getCharsPerLine(styles, maxCharsPerLine); + double charWidth = (_paperSize.width / charsPerLine) * styles.width.value; + return charWidth; + } + + double _colIndToPosition(int colInd) { + final int width = _paperSize.width; + return colInd == 0 ? 0 : (width * colInd / 12 - 1); + } + + int _getCharsPerLine(PosStyles styles, int? maxCharsPerLine) { + int charsPerLine; + if (maxCharsPerLine != null) { + charsPerLine = maxCharsPerLine; + } else { + if (styles.fontType != null) { + charsPerLine = _getMaxCharsPerLine(styles.fontType); + } else { + charsPerLine = _maxCharsPerLine ?? _getMaxCharsPerLine(_styles.fontType); + } + } + return charsPerLine; + } + + Uint8List _encode(String text, {bool isKanji = false}) { + // replace some non-ascii characters + text = text.replaceAll("’", "'").replaceAll("´", "'").replaceAll("»", '"').replaceAll(" ", ' ').replaceAll("•", '.').replaceAll("・", '.'); + if (!isKanji) { + return latin1.encode(text); + } else { + return Uint8List.fromList(gbk_bytes.encode(text)); + } + } + + List _getLexemes(String text) { + final List lexemes = []; + final List 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 [lexemes, isLexemeChinese]; + } + + /// Break text into chinese/non-chinese lexemes + bool _isChinese(String ch) { + return ch.codeUnitAt(0) > 255; + } + + /// 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 _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 is too large. Can only output up to $maxInput in $bytesNb bytes'); + } + + final List res = []; + int buf = value; + for (int i = 0; i < bytesNb; ++i) { + res.add(buf % 256); + buf = buf ~/ 256; + } + return res; + } + + /// 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> _toColumnFormat(img.Image imgSrc, int lineHeight) { + final img.Image image = img.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 = img.copyResize(image, width: widthPx, height: heightPx); + img.fill(biggerImage, img.getColor(255,255, 255)); + // Insert source image into bigger one + drawImage(biggerImage, image, dstX: 0, dstY: 0); + + int left = 0; + final List> blobs = []; + + while (left < widthPx) { + final img.Image slice = img.copyCrop(biggerImage, left, 0, lineHeight, heightPx); + final Uint8List bytes = slice.getBytes(); // slice.getBytes(format: luminance) + blobs.add(bytes); + left += lineHeight; + } + + return blobs; + } + + /// Image rasterization + List _toRasterFormat(img.Image imgSrc) { + final img.Image image = img.Image.from(imgSrc); // make a copy + final int widthPx = image.width; + final int heightPx = image.height; + + img.grayscale(image); + img.invert(image); + + // R/G/B channels are same -> keep only one channel + final List oneChannelBytes = []; + final List buffer = image.getBytes(); + 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) + if (widthPx % 8 != 0) { + 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 + return _packBitsIntoBytes(oneChannelBytes); + } + + /// Merges each 8 values (bits) into one byte + List _packBitsIntoBytes(List bytes) { + const pxPerLine = 8; + final List res = []; + 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; + } + + /// 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); + } + // ************************ (end) Internal helpers ************************ + + //**************************** Public command generators ************************ + /// Clear the buffer and reset text styles + List reset() { + List bytes = []; + bytes += cInit.codeUnits; + _styles = PosStyles(); + bytes += setGlobalCodeTable(_codeTable); + bytes += setGlobalFont(_font); + return bytes; + } + + /// Set global code table which will be used instead of the default printer's code table + /// (even after resetting) + List setGlobalCodeTable(String? codeTable) { + List bytes = []; + _codeTable = codeTable; + if (codeTable != null) { + bytes += Uint8List.fromList( + List.from(cCodeTable.codeUnits)..add(_profile.getCodePageId(codeTable)), + ); + _styles = _styles.copyWith(codeTable: codeTable); + } + return bytes; + } + + /// Set global font which will be used instead of the default printer's font + /// (even after resetting) + List setGlobalFont(PosFontType? font, {int? maxCharsPerLine}) { + List bytes = []; + _font = font; + if (font != null) { + _maxCharsPerLine = maxCharsPerLine ?? _getMaxCharsPerLine(font); + bytes += font == PosFontType.fontB ? cFontB.codeUnits : cFontA.codeUnits; + _styles = _styles.copyWith(fontType: font); + } + return bytes; + } + + List setStyles(PosStyles styles, {bool isKanji = false}) { + List bytes = []; + // if (styles.align != _styles.align) { + bytes += latin1.encode(styles.align == PosAlign.left ? cAlignLeft : (styles.align == PosAlign.center ? cAlignCenter : cAlignRight)); + _styles = _styles.copyWith(align: styles.align); + // } + + // if (styles.bold != _styles.bold) { + bytes += styles.bold ? cBoldOn.codeUnits : cBoldOff.codeUnits; + _styles = _styles.copyWith(bold: styles.bold); + // } + + if (styles.turn90 != _styles.turn90) { + bytes += styles.turn90 ? cTurn90On.codeUnits : cTurn90Off.codeUnits; + _styles = _styles.copyWith(turn90: styles.turn90); + } + if (styles.reverse != _styles.reverse) { + bytes += styles.reverse ? cReverseOn.codeUnits : cReverseOff.codeUnits; + _styles = _styles.copyWith(reverse: styles.reverse); + } + + // if (styles.underline != _styles.underline) { + bytes += styles.underline ? cUnderline1dot.codeUnits : cUnderlineOff.codeUnits; + _styles = _styles.copyWith(underline: styles.underline); + // } + + // Set font + if (styles.fontType != null) { + bytes += styles.fontType == PosFontType.fontB ? cFontB.codeUnits : cFontA.codeUnits; + _styles = _styles.copyWith(fontType: styles.fontType); + } else if (_font != null) { + bytes += _font == PosFontType.fontB ? cFontB.codeUnits : cFontA.codeUnits; + _styles = _styles.copyWith(fontType: _font); + } + + // Characters size + if (styles.height.value != _styles.height.value || styles.width.value != _styles.width.value) { + bytes += Uint8List.fromList( + List.from(cSizeGSn.codeUnits)..add(PosTextSize.decSize(styles.height, styles.width)), + ); + _styles = _styles.copyWith(height: styles.height, width: styles.width); + } + + // Set Kanji mode + if (isKanji) { + bytes += cKanjiOn.codeUnits; + } else { + bytes += cKanjiOff.codeUnits; + } + + // Set local code table + if (styles.codeTable != null && _styles.codeTable != styles.codeTable) { + bytes += Uint8List.fromList( + List.from(cCodeTable.codeUnits)..add(_profile.getCodePageId(styles.codeTable)), + ); + _styles = _styles.copyWith(align: styles.align, codeTable: styles.codeTable); + } else if (_codeTable != null && _styles.codeTable != _codeTable) { + bytes += Uint8List.fromList( + List.from(cCodeTable.codeUnits)..add(_profile.getCodePageId(_codeTable)), + ); + _styles = _styles.copyWith(align: styles.align, codeTable: _codeTable); + } + + return bytes; + } + + /// Sens raw command(s) + List rawBytes(List cmd, {bool isKanji = false}) { + List bytes = []; + if (!isKanji) { + bytes += cKanjiOff.codeUnits; + } + bytes += Uint8List.fromList(cmd); + return bytes; + } + + List text( + String text, { + PosStyles styles = const PosStyles(), + int linesAfter = 0, + bool containsChinese = false, + int? maxCharsPerLine, + }) { + List bytes = []; + if (!containsChinese) { + bytes += _text( + _encode(text, isKanji: containsChinese), + styles: styles, + isKanji: containsChinese, + maxCharsPerLine: maxCharsPerLine, + ); + // Ensure at least one line break after the text + bytes += emptyLines(linesAfter + 1); + } else { + bytes += _mixedKanji(text, styles: styles, linesAfter: linesAfter); + } + return bytes; + } + + /// Skips [n] lines + /// + /// Similar to [feed] but uses an alternative command + List emptyLines(int n) { + List bytes = []; + if (n > 0) { + bytes += List.filled(n, '\n').join().codeUnits; + } + return bytes; + } + + /// Skips [n] lines + /// + /// Similar to [emptyLines] but uses an alternative command + List feed(int n) { + List bytes = []; + if (n >= 0 && n <= 255) { + bytes += Uint8List.fromList( + List.from(cFeedN.codeUnits)..add(n), + ); + } + return bytes; + } + + /// Cut the paper + /// + /// [mode] is used to define the full or partial cut (if supported by the priner) + List cut({PosCutMode mode = PosCutMode.full}) { + List bytes = []; + bytes += emptyLines(5); + if (mode == PosCutMode.partial) { + bytes += cCutPart.codeUnits; + } else { + bytes += cCutFull.codeUnits; + } + return bytes; + } + + /// 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. + List printCodeTable({String? codeTable}) { + List bytes = []; + bytes += cKanjiOff.codeUnits; + + if (codeTable != null) { + bytes += Uint8List.fromList( + List.from(cCodeTable.codeUnits)..add(_profile.getCodePageId(codeTable)), + ); + } + + bytes += Uint8List.fromList(List.generate(256, (i) => i)); + + // Back to initial code table + setGlobalCodeTable(_codeTable); + return bytes; + } + + /// Beeps [n] times + /// + /// Beep [duration] could be between 50 and 450 ms. + List beep({int n = 3, PosBeepDuration duration = PosBeepDuration.beep450ms}) { + List bytes = []; + 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); + return bytes; + } + + /// Reverse feed for [n] lines (if supported by the priner) + List reverseFeed(int n) { + List bytes = []; + bytes += Uint8List.fromList( + List.from(cReverseFeedN.codeUnits)..add(n), + ); + return bytes; + } + + /// 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. + List oldRrow(List cols) { + List bytes = []; + final isSumValid = cols.fold(0, (int sum, col) => sum + col.width) == 12; + if (!isSumValid) { + throw Exception('Total columns width must be equal to 12'); + } + bool isNextRow = false; + List nextRow = []; + + for (int i = 0; i < cols.length; ++i) { + int colInd = cols.sublist(0, i).fold(0, (int sum, col) => sum + col.width); + double charWidth = _getCharWidth(cols[i].styles); + double fromPos = _colIndToPosition(colInd); + double toPos = _colIndToPosition(colInd + cols[i].width) - spaceBetweenRows; + int maxCharactersNb = ((toPos - fromPos) / charWidth).floor(); + + var containsChinese = cols[i].containsChinese; + + // CASE 1: containsChinese = false + if (!containsChinese) { + Uint8List encodedToPrint = cols[i].textEncoded != null ? cols[i].textEncoded! : _encode(cols[i].text); + + // If the col's content is too long, split it to the next row + int realCharactersNb = encodedToPrint.length; + if (realCharactersNb > maxCharactersNb) { + // Print max possible and split to the next row + Uint8List encodedToPrintNextRow = encodedToPrint.sublist(maxCharactersNb); + encodedToPrint = encodedToPrint.sublist(0, maxCharactersNb); + isNextRow = true; + nextRow.add(PosColumn(textEncoded: encodedToPrintNextRow, width: cols[i].width, styles: cols[i].styles)); + } else { + // Insert an empty col + nextRow.add(PosColumn(text: '', width: cols[i].width, styles: cols[i].styles)); + } + // end rows splitting + bytes += _text( + encodedToPrint, + styles: cols[i].styles, + colInd: colInd, + colWidth: cols[i].width, + ); + } else { + // CASE 1: containsChinese = true + // Split text into multiple lines if it too long + int counter = 0; + int splitPos = 0; + for (int p = 0; p < cols[i].text.length; ++p) { + final int w = _isChinese(cols[i].text[p]) ? 2 : 1; + if (counter + w >= maxCharactersNb) { + break; + } + counter += w; + splitPos += 1; + } + String toPrintNextRow = cols[i].text.substring(splitPos); + String toPrint = cols[i].text.substring(0, splitPos); + + if (toPrintNextRow.isNotEmpty) { + isNextRow = true; + nextRow.add(PosColumn(text: toPrintNextRow, containsChinese: true, width: cols[i].width, styles: cols[i].styles)); + } else { + // Insert an empty col + nextRow.add(PosColumn(text: '', width: cols[i].width, styles: cols[i].styles)); + } + + // Print current row + final list = _getLexemes(toPrint); + final List lexemes = list[0]; + final List isLexemeChinese = list[1]; + + // Print each lexeme using codetable OR kanji + for (var j = 0; j < lexemes.length; ++j) { + bytes += _text( + _encode(lexemes[j], isKanji: isLexemeChinese[j]), + styles: cols[i].styles, + colInd: colInd, + colWidth: cols[i].width, + isKanji: isLexemeChinese[j], + ); + // Define the absolute position only once (we print one line only) + // colInd = null; + } + } + } + + bytes += emptyLines(1); + + if (isNextRow) { + row(nextRow); + } + + return bytes; + } + + List textLeftRight(String leftText, String rightText, {bool containsChinese = false}) { + /// Portuguese + // var encTxt1 = await CharsetConverter.encode("UTF-8", leftText); + return row([ + PosColumn( + text: leftText, + // textEncoded: encTxt1, + width: 8, + containsChinese: containsChinese, + styles: const PosStyles( + align: PosAlign.left, + )), + PosColumn( + text: rightText, + width: 4, + containsChinese: containsChinese, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + } + + /// 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. + List row(List cols) { + List bytes = []; + final isSumValid = cols.fold(0, (int sum, col) => sum + col.width) == 12; + if (!isSumValid) { + throw Exception('Total columns width must be equal to 12'); + } + bool isNextRow = false; + List nextRow = []; + + for (int i = 0; i < cols.length; ++i) { + int colInd = cols.sublist(0, i).fold(0, (int sum, col) => sum + col.width); + // print(' ---------- colInd $colInd col.width ${cols[0].width}'); + double charWidth = _getCharWidth(cols[i].styles); + double fromPos = _colIndToPosition(colInd); + double toPos = _colIndToPosition(colInd + cols[i].width) - spaceBetweenRows; + int maxCharactersNb = ((toPos - fromPos) / charWidth).floor(); + var containsChinese = cols[i].containsChinese; + if (!containsChinese) { + // CASE 1: containsChinese = false + Uint8List encodedToPrint = cols[i].textEncoded != null ? cols[i].textEncoded! : _encode(cols[i].text); + + // If the col's content is too long, split it to the next row + int realCharactersNb = encodedToPrint.length; + if (realCharactersNb > maxCharactersNb) { + // Print max possible and split to the next row + Uint8List encodedToPrintNextRow = realCharactersNb < maxCharactersNb ? encodedToPrint : encodedToPrint.sublist(maxCharactersNb); + encodedToPrint = realCharactersNb < maxCharactersNb ? encodedToPrint : encodedToPrint.sublist(0, maxCharactersNb); + isNextRow = realCharactersNb < maxCharactersNb ? false : true; + + if (isNextRow) { + nextRow.add(PosColumn(text: String.fromCharCodes(encodedToPrintNextRow).trim(), width: cols[i].width, styles: cols[i].styles)); + } else { + nextRow.add(PosColumn(text: '', width: cols[i].width, styles: cols[i].styles)); + } + + // end rows splitting + bytes += _text( + encodedToPrint, + styles: cols[i].styles, + colInd: colInd, + colWidth: cols[i].width, + // maxCharsPerLine: maxCharactersNb, + ); + isNextRow = true; + } else { + bytes += _text( + _encode(cols[i].text), + styles: cols[i].styles, + colInd: colInd, + colWidth: cols[i].width, + // maxCharsPerLine: maxCharactersNb, + ); + // Insert an empty col + nextRow.add(PosColumn(text: '', width: cols[i].width, styles: cols[i].styles)); + } + } else { + // CASE 1: containsChinese = true + var originalText = cols[i].text; + var splitPos = _spltChineseCharacters(maxCharactersNb, originalText); + + String toPrint = originalText.substring(0, splitPos); + String toPrintNextRow = originalText.substring(splitPos); + // '豚肉・木耳と玉子炒め弁当' + if (toPrintNextRow.isNotEmpty) { + isNextRow = true; + nextRow.add(PosColumn(text: toPrintNextRow, containsChinese: true, width: cols[i].width, styles: cols[i].styles)); + } else { + // Insert an empty col + nextRow.add(PosColumn(text: '', width: cols[i].width, styles: cols[i].styles)); + } + if (toPrint.isNotEmpty) { + // Print current row + bytes += _text( + _encode(toPrint, isKanji: true), + styles: cols[i].styles, + colInd: colInd, + colWidth: cols[i].width, + isKanji: true, + ); + } + } + } + + bytes += emptyLines(1); + + if (isNextRow) { + bytes += row(nextRow); + } + + return bytes; + } + + int _spltChineseCharacters(int maxCharactersNb, String text) { + // Split text into multiple lines if it too long + int counter = 0; + int splitPos = 0; + for (int p = 0; p < text.length; ++p) { + final int w = _isChinese(text[p]) ? 2 : 1; + if (counter + w >= maxCharactersNb) { + break; + } + counter += w; + splitPos += 1; + } + + return splitPos; + } + + /// Print an image using (ESC *) command + /// + /// [image] is an instanse of class from [Image library](https://pub.dev/packages/image) + List image(img.Image imgSrc, {PosAlign align = PosAlign.center}) { + List bytes = []; + // Image alignment + bytes += setStyles(const PosStyles().copyWith(align: align)); + + final img.Image image = img.Image.from(imgSrc); // make a copy + const bool highDensityHorizontal = true; + const bool highDensityVertical = true; + + img.invert(image); + img.flip(image, img.Flip.horizontal); + final img.Image imageRotated = img.copyRotate(image, 270); + + // ignore: dead_code + const int lineHeight = highDensityVertical ? 3 : 1; + final List> 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; + // ignore: dead_code + const int densityByte = (highDensityHorizontal ? 1 : 0) + (highDensityVertical ? 32 : 0); + + final List 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) + bytes += [27, 51, 16]; + for (int i = 0; i < blobs.length; ++i) { + bytes += List.from(header) + ..addAll(blobs[i]) + ..addAll('\n'.codeUnits); + } + // Reset line spacing: ESC 2 (HEX: 0x1b 0x32) + bytes += [27, 50]; + return bytes; + } + + /// Print an image using (GS v 0) obsolete command + /// + /// [image] is an instanse of class from [Image library](https://pub.dev/packages/image) + List imageRaster( + img.Image image, { + PosAlign align = PosAlign.center, + bool highDensityHorizontal = true, + bool highDensityVertical = true, + PosImageFn imageFn = PosImageFn.bitImageRaster, + }) { + List bytes = []; + // Image alignment + bytes += setStyles(PosStyles().copyWith(align: align)); + + final int widthPx = image.width; + final int heightPx = image.height; + final int widthBytes = (widthPx + 7) ~/ 8; + final List resterizedData = _toRasterFormat(image); + + if (imageFn == PosImageFn.bitImageRaster) { + // GS v 0 + final int densityByte = (highDensityVertical ? 0 : 1) + (highDensityHorizontal ? 0 : 2); + + final List header = List.from(cRasterImg2.codeUnits); + header.add(densityByte); // m + header.addAll(_intLowHigh(widthBytes, 2)); // xL xH + header.addAll(_intLowHigh(heightPx, 2)); // yL yH + bytes += List.from(header)..addAll(resterizedData); + } else if (imageFn == PosImageFn.graphics) { + // 'GS ( L' - FN_112 (Image data) + final List header1 = List.from(cRasterImg.codeUnits); + header1.addAll(_intLowHigh(widthBytes * heightPx + 10, 2)); // pL pH + header1.addAll([48, 112, 48]); // m=48, fn=112, a=48 + header1.addAll([1, 1]); // bx=1, by=1 + header1.addAll([49]); // c=49 + header1.addAll(_intLowHigh(widthBytes, 2)); // xL xH + header1.addAll(_intLowHigh(heightPx, 2)); // yL yH + bytes += List.from(header1)..addAll(resterizedData); + + // 'GS ( L' - FN_50 (Run print) + final List header2 = List.from(cRasterImg.codeUnits); + header2.addAll([2, 0]); // pL pH + header2.addAll([48, 50]); // m fn[2,50] + bytes += List.from(header2); + } + return bytes; + } + + /// Print a barcode + /// + /// [width] range and units are different depending on the printer model (some printers use 1..5). + /// [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. + List barcode( + Barcode barcode, { + int? width, + int? height, + BarcodeFont? font, + BarcodeText textPos = BarcodeText.below, + PosAlign align = PosAlign.center, + }) { + List bytes = []; + // Set alignment + bytes += setStyles(PosStyles().copyWith(align: align)); + + // Set text position + bytes += cBarcodeSelectPos.codeUnits + [textPos.value]; + + // Set font + if (font != null) { + bytes += cBarcodeSelectFont.codeUnits + [font.value]; + } + + // Set width + if (width != null && width >= 0) { + bytes += cBarcodeSetW.codeUnits + [width]; + } + // Set height + if (height != null && height >= 1 && height <= 255) { + bytes += cBarcodeSetH.codeUnits + [height]; + } + + // Print barcode + final header = cBarcodePrint.codeUnits + [barcode.type!.value]; + if (barcode.type!.value <= 6) { + // Function A + bytes += header + barcode.data! + [0]; + } else { + // Function B + bytes += header + [barcode.data!.length] + barcode.data!; + } + return bytes; + } + + /// Print a QR Code + List qrcode( + String text, { + PosAlign align = PosAlign.center, + QRSize size = QRSize.Size4, + QRCorrection cor = QRCorrection.L, + }) { + List bytes = []; + // Set alignment + bytes += setStyles(PosStyles().copyWith(align: align)); + QRCode qr = QRCode(text, size, cor); + bytes += qr.bytes; + return bytes; + } + + /// Open cash drawer + List drawer({PosDrawer pin = PosDrawer.pin2}) { + List bytes = []; + if (pin == PosDrawer.pin2) { + bytes += cCashDrawerPin2.codeUnits; + } else { + bytes += cCashDrawerPin5.codeUnits; + } + return bytes; + } + + /// Print horizontal full width separator + /// If [len] is null, then it will be defined according to the paper width + List hr({String ch = '-', int? len, int linesAfter = 0}) { + List bytes = []; + int n = len ?? _maxCharsPerLine ?? _getMaxCharsPerLine(_styles.fontType); + String ch1 = ch.length == 1 ? ch : ch[0]; + bytes += text(List.filled(n, ch1).join(), linesAfter: linesAfter); + return bytes; + } + + List textEncoded( + Uint8List textBytes, { + PosStyles styles = const PosStyles(), + int linesAfter = 0, + int? maxCharsPerLine, + }) { + List bytes = []; + bytes += _text(textBytes, styles: styles, maxCharsPerLine: maxCharsPerLine); + // Ensure at least one line break after the text + bytes += emptyLines(linesAfter + 1); + return bytes; + } + // ************************ (end) Public command generators ************************ + + // ************************ (end) Internal command generators ************************ + /// Generic print for internal use + /// + /// [colInd] range: 0..11. If null: do not define the position + List _text( + Uint8List textBytes, { + PosStyles styles = const PosStyles(), + int? colInd = 0, + bool isKanji = false, + int colWidth = 12, + int? maxCharsPerLine, + }) { + List bytes = []; + if (colInd != null) { + double charWidth = _getCharWidth(styles, maxCharsPerLine: maxCharsPerLine); + double fromPos = _colIndToPosition(colInd); + + // Align + if (colWidth != 12) { + // Update fromPos + final double toPos = _colIndToPosition(colInd + colWidth) - spaceBetweenRows; + final double textLen = textBytes.length * charWidth; + + if (styles.align == PosAlign.right) { + fromPos = toPos - textLen; + } else if (styles.align == PosAlign.center) { + fromPos = fromPos + (toPos - fromPos) / 2 - textLen / 2; + } + if (fromPos < 0) { + fromPos = 0; + } + } + + final hexStr = fromPos.round().toRadixString(16).padLeft(3, '0'); + final hexPair = HEX.decode(hexStr); + + // Position + bytes += Uint8List.fromList( + List.from(cPos.codeUnits)..addAll([hexPair[1], hexPair[0]]), + ); + } + + bytes += setStyles(styles, isKanji: isKanji); + + bytes += textBytes; + return bytes; + } + + /// Prints one line of styled mixed (chinese and latin symbols) text + List _mixedKanji( + String text, { + PosStyles styles = const PosStyles(), + int linesAfter = 0, + int? maxCharsPerLine, + }) { + List bytes = []; + final list = _getLexemes(text); + final List lexemes = list[0]; + final List isLexemeChinese = list[1]; + + // Print each lexeme using codetable OR kanji + int? colInd = 0; + for (var i = 0; i < lexemes.length; ++i) { + bytes += _text( + _encode(lexemes[i], isKanji: isLexemeChinese[i]), + styles: styles, + colInd: colInd, + isKanji: isLexemeChinese[i], + maxCharsPerLine: maxCharsPerLine, + ); + // Define the absolute position only once (we print one line only) + colInd = null; + } + + bytes += emptyLines(linesAfter + 1); + return bytes; + } + // ************************ (end) Internal command generators ************************ + + /// Draw the image [src] onto the image [dst]. + /// + /// In other words, drawImage will take an rectangular area from src of + /// width [src_w] and height [src_h] at position ([src_x],[src_y]) and place it + /// in a rectangular area of [dst] of width [dst_w] and height [dst_h] at + /// position ([dst_x],[dst_y]). + /// + /// If the source and destination coordinates and width and heights differ, + /// appropriate stretching or shrinking of the image fragment will be performed. + /// The coordinates refer to the upper left corner. This function can be used to + /// copy regions within the same image (if [dst] is the same as [src]) + /// but if the regions overlap the results will be unpredictable. + img.Image drawImage(img.Image dst, img.Image src, + {int? dstX, int? dstY, int? dstW, int? dstH, int? srcX, int? srcY, int? srcW, int? srcH, bool blend = true}) { + dstX ??= 0; + dstY ??= 0; + srcX ??= 0; + srcY ??= 0; + srcW ??= src.width; + srcH ??= src.height; + dstW ??= (dst.width < src.width) ? dstW = dst.width : src.width; + dstH ??= (dst.height < src.height) ? dst.height : src.height; + + if (blend) { + for (var y = 0; y < dstH; ++y) { + for (var x = 0; x < dstW; ++x) { + final stepX = (x * (srcW / dstW)).toInt(); + final stepY = (y * (srcH / dstH)).toInt(); + final srcPixel = src.getPixel(srcX + stepX, srcY + stepY); + img.drawPixel(dst, dstX + x, dstY + y, srcPixel); + } + } + } else { + for (var y = 0; y < dstH; ++y) { + for (var x = 0; x < dstW; ++x) { + final stepX = (x * (srcW / dstW)).toInt(); + final stepY = (y * (srcH / dstH)).toInt(); + final srcPixel = src.getPixel(srcX + stepX, srcY + stepY); + dst.setPixel(dstX + x, dstY + y, srcPixel); + } + } + } + + return dst; + } +} diff --git a/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/pos_column.dart b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/pos_column.dart new file mode 100644 index 000000000..ba33c238e --- /dev/null +++ b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/pos_column.dart @@ -0,0 +1,36 @@ +/* + * esc_pos_utils + * Created by Andrey U. + * + * Copyright (c) 2019-2020. All rights reserved. + * See LICENSE for distribution and usage details. + */ + +import 'dart:typed_data' show Uint8List; + +import 'pos_styles.dart'; + +/// Column contains text, styles and width (an integer in 1..12 range) +/// [containsChinese] not used if the text passed as textEncoded +class PosColumn { + PosColumn({ + this.text = '', + this.textEncoded, + this.containsChinese = false, + this.width = 2, + this.styles = const PosStyles(), + }) { + if (width < 1 || width > 12) { + throw Exception('Column width must be between 1..12'); + } + if (text.length > 0 && textEncoded != null && textEncoded!.length > 0) { + throw Exception('Only one parameter - text or textEncoded - should be passed'); + } + } + + String text; + Uint8List? textEncoded; + bool containsChinese; + int width; + PosStyles styles; +} diff --git a/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/pos_styles.dart b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/pos_styles.dart new file mode 100644 index 000000000..b69231397 --- /dev/null +++ b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/pos_styles.dart @@ -0,0 +1,71 @@ +/* + * esc_pos_utils + * Created by Andrey U. + * + * 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 = PosAlign.left, + this.height = PosTextSize.size1, + this.width = PosTextSize.size1, + this.fontType, + this.codeTable, + }); + + // Init all fields with default values + const PosStyles.defaults({ + this.bold = false, + this.reverse = false, + this.underline = false, + this.turn90 = false, + this.align = PosAlign.left, + this.height = PosTextSize.size1, + this.width = PosTextSize.size1, + this.fontType = PosFontType.fontA, + this.codeTable = "CP437", + }); + + final bool bold; + final bool reverse; + final bool underline; + final bool turn90; + final PosAlign align; + final PosTextSize height; + final PosTextSize width; + final PosFontType? fontType; + final String? codeTable; + + PosStyles copyWith({ + bool? bold, + bool? reverse, + bool? underline, + bool? turn90, + PosAlign? align, + PosTextSize? height, + PosTextSize? width, + PosFontType? fontType, + String? codeTable, + }) { + return PosStyles( + bold: bold ?? this.bold, + reverse: reverse ?? this.reverse, + underline: underline ?? this.underline, + turn90: turn90 ?? this.turn90, + align: align ?? this.align, + height: height ?? this.height, + width: width ?? this.width, + fontType: fontType ?? this.fontType, + codeTable: codeTable ?? this.codeTable, + ); + } +} diff --git a/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/qrcode.dart b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/qrcode.dart new file mode 100644 index 000000000..9501d686d --- /dev/null +++ b/frontend/mgramseva/lib/utils/printer/esc_pos_utils_platform/src/qrcode.dart @@ -0,0 +1,71 @@ +/* + * esc_pos_utils + * Created by Andrey U. + * + * Copyright (c) 2019-2020. All rights reserved. + * See LICENSE for distribution and usage details. + */ + +import 'dart:convert'; + +import 'commands.dart'; + +class QRSize { + const QRSize(this.value); + final int value; + + static const Size1 = QRSize(0x01); + static const Size2 = QRSize(0x02); + static const Size3 = QRSize(0x03); + static const Size4 = QRSize(0x04); + static const Size5 = QRSize(0x05); + static const Size6 = QRSize(0x06); + static const Size7 = QRSize(0x07); + static const Size8 = QRSize(0x08); +} + +/// QR Correction level +class QRCorrection { + const QRCorrection._internal(this.value); + final int value; + + /// Level L: Recovery Capacity 7% + static const L = QRCorrection._internal(48); + + /// Level M: Recovery Capacity 15% + static const M = QRCorrection._internal(49); + + /// Level Q: Recovery Capacity 25% + static const Q = QRCorrection._internal(50); + + /// Level H: Recovery Capacity 30% + static const H = QRCorrection._internal(51); +} + +class QRCode { + List bytes = []; + + QRCode(String text, QRSize size, QRCorrection level) { + // FN 167. QR Code: Set the size of module + // pL pH cn fn n + bytes += cQrHeader.codeUnits + [0x03, 0x00, 0x31, 0x43] + [size.value]; + + // FN 169. QR Code: Select the error correction level + // pL pH cn fn n + bytes += cQrHeader.codeUnits + [0x03, 0x00, 0x31, 0x45] + [level.value]; + + // FN 180. QR Code: Store the data in the symbol storage area + List textBytes = latin1.encode(text); + // pL pH cn fn m + bytes += cQrHeader.codeUnits + [textBytes.length + 3, 0x00, 0x31, 0x50, 0x30]; + bytes += textBytes; + + // FN 182. QR Code: Transmit the size information of the symbol data in the symbol storage area + // pL pH cn fn m + bytes += cQrHeader.codeUnits + [0x03, 0x00, 0x31, 0x52, 0x30]; + + // FN 181. QR Code: Print the symbol data in the symbol storage area + // pL pH cn fn m + bytes += cQrHeader.codeUnits + [0x03, 0x00, 0x31, 0x51, 0x30]; + } +} diff --git a/frontend/mgramseva/pubspec.yaml b/frontend/mgramseva/pubspec.yaml index 9a1a67d36..d00e786bf 100644 --- a/frontend/mgramseva/pubspec.yaml +++ b/frontend/mgramseva/pubspec.yaml @@ -40,7 +40,6 @@ dependencies: flutter_spinkit: ^5.0.0 json_annotation: ^4.8.0 url_strategy: ^0.2.0 - charset_converter: ^2.1.1 screenshot: ^2.1.0 number_to_words: ^1.0.0 flutter_localizations: @@ -112,6 +111,7 @@ flutter: - assets/icons/ - assets/svg/ - assets/.env + # To add assets to your application, add an assets section, like this: # - images/a_dot_ham.jpeg