newDevices, Context context) {
+ this.pairedDevices = pairedDevices;
+ this.newDevices = newDevices;
+ this.mContext = context;
+ }
+
+ @Override
+ public int getCount() {
+ return pairedDevices.size() + newDevices.size() + 2;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return position;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ switch (getItemViewType(position)) {
+ case TITLE:
+ convertView = LayoutInflater.from(mContext).inflate(R.layout.text_item, parent, false);
+ TextView tv_title = (TextView) convertView.findViewById(R.id.text);
+ tv_title.setTextColor(mContext.getResources().getColor(R.color.colorAccent));
+ tv_title.setGravity(Gravity.LEFT);
+ if (position == 0) {
+ tv_title.setText(mContext.getResources().getString(R.string.paired));
+ } else {
+ tv_title.setText(mContext.getResources().getString(R.string.unpaired));
+ }
+ break;
+ case CONTENT:
+ BluetoothParameter bluetoothParameter = null;
+ if (position < pairedDevices.size() + 1) {
+ bluetoothParameter = pairedDevices.get(position - 1);
+ }
+ if (position > pairedDevices.size()+1 && newDevices.size() > 0) {
+ bluetoothParameter = newDevices.get(position - pairedDevices.size() - 2);
+ }
+
+ if (bluetoothParameter!=null) {
+ convertView = LayoutInflater.from(mContext).inflate(R.layout.bluetooth_list_item, parent, false);
+ TextView tvName = (TextView) convertView.findViewById(R.id.b_name);
+ TextView tvMac = (TextView) convertView.findViewById(R.id.b_mac);
+ TextView tvStrength = (TextView) convertView.findViewById(R.id.b_info);
+ tvName.setText(bluetoothParameter.getBluetoothName());
+ tvMac.setText(bluetoothParameter.getBluetoothMac());
+ tvStrength.setText(bluetoothParameter.getBluetoothStrength());
+ }
+ break;
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if ((position == pairedDevices.size()+1) || (position == 0)) {
+ return TITLE;
+ } else {
+ return CONTENT;
+ }
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/MainActivity.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/MainActivity.java
new file mode 100755
index 0000000..bee1bcd
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/MainActivity.java
@@ -0,0 +1,494 @@
+package com.printer.tscdemo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.gprinter.bean.PrinterDevices;
+import com.gprinter.command.LabelCommand;
+import com.gprinter.utils.CallbackListener;
+import com.gprinter.utils.Command;
+import com.gprinter.utils.ConnMethod;
+import com.gprinter.utils.LogUtils;
+import com.gprinter.utils.PDFUtils;
+import com.gprinter.utils.SDKUtils;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Vector;
+
+public class MainActivity extends AppCompatActivity implements CallbackListener {
+ TextView tvState;
+ CheckBox swState;
+ Printer printer=null;
+ Context context;
+ Spinner sp_gap;
+ String TAG=MainActivity.class.getSimpleName();
+ PermissionUtils permissionUtils;
+ Handler handler=new Handler(Looper.getMainLooper()){
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ switch (msg.what){
+ case 0x00:
+ String tip=(String)msg.obj;
+ Toast.makeText(context,tip,Toast.LENGTH_SHORT).show();
+ break;
+ case 0x01:
+ int status=msg.arg1;
+ if (status==-1){//获取状态失败
+ AlertDialog alertDialog = new AlertDialog.Builder(context)
+ .setTitle(getString(R.string.tip))
+ .setMessage(getString(R.string.status_fail))
+ .setIcon(R.mipmap.ic_launcher)
+ .setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() {//添加"Yes"按钮
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+
+ }
+ })
+ .create();
+ alertDialog.show();
+ return;
+ }else if (status==1){
+ Toast.makeText(context,getString(R.string.status_feed),Toast.LENGTH_SHORT).show();
+ return;
+ }else if (status==0){//状态正常
+ Toast.makeText(context,getString(R.string.status_normal),Toast.LENGTH_SHORT).show();
+ return;
+ }else if (status==-2){//状态缺纸
+ Toast.makeText(context,getString(R.string.status_out_of_paper),Toast.LENGTH_SHORT).show();
+ return;
+ }else if (status==-3){//状态开盖
+ Toast.makeText(context,getString(R.string.status_open),Toast.LENGTH_SHORT).show();
+ return;
+ }else if (status==-4){
+ Toast.makeText(context,getString(R.string.status_overheated),Toast.LENGTH_SHORT).show();
+ return;
+ }
+ break;
+ case 0x02://关闭连接
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (printer.getPortManager()!=null){
+ printer.close();
+ }
+ }
+ }).start();
+
+ tvState.setText(getString(R.string.not_connected));
+ break;
+ case 0x03:
+ String message=(String)msg.obj;
+ AlertDialog alertDialog = new AlertDialog.Builder(context)
+ .setTitle(getString(R.string.tip))
+ .setMessage(message)
+ .setIcon(R.mipmap.ic_launcher)
+ .setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() {//添加"Yes"按钮
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+
+ }
+ })
+ .create();
+ alertDialog.show();
+ break;
+ }
+ }
+ };
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ initView();
+ initPermission();
+ }
+
+ /**
+ * 初始化权限
+ */
+ private void initPermission() {
+ permissionUtils.requestPermissions(getString(R.string.permission),
+ new PermissionUtils.PermissionListener(){
+ @Override
+ public void doAfterGrand(String... permission) {
+
+ }
+ @Override
+ public void doAfterDenied(String... permission) {
+ for (String p:permission) {
+ switch (p){
+ case Manifest.permission.READ_EXTERNAL_STORAGE:
+ Utils.shortToast(context,getString(R.string.no_read));
+ break;
+ case Manifest.permission.ACCESS_FINE_LOCATION:
+ Utils.shortToast(context,getString(R.string.no_permission));
+ break;
+ }
+ }
+ }
+ }, Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.ACCESS_FINE_LOCATION);
+ }
+
+ private void initView() {
+ context=MainActivity.this;
+ permissionUtils=new PermissionUtils(context);
+ setTitle(getString(R.string.app_name)+"V"+Utils.getVersionName(context));
+ tvState=(TextView)findViewById(R.id.tvState);
+ swState=(CheckBox)findViewById(R.id.swState);
+ sp_gap=(Spinner)findViewById(R.id.sp_gap) ;
+ printer=Printer.getInstance();//获取管理对象
+ }
+ /**
+ * 断开连接
+ * @param view
+ */
+ public void disconnect(View view) {
+ handler.obtainMessage(0x02).sendToTarget();
+ }
+
+ /**
+ * 蓝牙设备
+ * @param view
+ */
+ public void blueToothDevices(View view) {
+ startActivityForResult(new Intent(context, BlueToothDeviceActivity.class),0x00);
+ }
+ /**
+ * usb设备
+ * @param view
+ */
+ public void usbDevices(View view) {
+ startActivityForResult(new Intent(context, UsbDeviceActivity.class),0x01);
+ }
+ /**
+ * wifi接口
+ * @param view
+ */
+ public void wifiDevices(View view) {
+ startActivityForResult(new Intent(context, WifiDeviceActivity.class),0x02);
+ }
+ /**
+ * 串口接口
+ * @param view
+ */
+ public void serialPortDevices(View view) {
+ startActivityForResult(new Intent(context, SerialPortDeviceActivity.class),0x03);
+ }
+ /**
+ * 打印案例
+ * @param view
+ */
+ public void print(View view) {
+ ThreadPoolManager.getInstance().addTask(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (printer.getPortManager()==null){
+ tipsToast(getString(R.string.conn_first));
+ return;
+ }
+ //打印前后查询打印机状态,部分老款打印机不支持查询请去除下面查询代码
+ //****************** 查询状态 ***************************
+ if (swState.isChecked()) {
+ Command command = printer.getPortManager().getCommand();
+ int status = printer.getPrinterState(command,2000);
+ if (status != 0) {//打印机处于不正常状态,则不发送打印任务
+ Message msg = new Message();
+ msg.what = 0x01;
+ msg.arg1 = status;
+ handler.sendMessage(msg);
+ return;
+ }
+ }
+ //***************************************************************
+ boolean result=printer.getPortManager().writeDataImmediately(PrintContent.getLabel(context,sp_gap.getSelectedItemPosition()));
+ if (result) {
+ tipsDialog(getString(R.string.send_success));
+ }else {
+ tipsDialog(getString(R.string.send_fail));
+ }
+ LogUtils.e("send result",result);
+ } catch (IOException e) {
+ tipsDialog(getString(R.string.print_fail)+e.getMessage());
+ }catch (Exception e){
+ tipsDialog(getString(R.string.print_fail)+e.getMessage());
+ }finally {
+ if (printer.getPortManager()==null) {
+ printer.close();
+ }
+ }
+ }
+ });
+
+ }
+
+ /**
+ * 打印xml 布局
+ * @param view
+ */
+ public void xml(View view) {
+ ThreadPoolManager.getInstance().addTask(new Runnable() {
+ @Override
+ public void run() {
+ if (printer.getPortManager()==null){
+ tipsToast(getString(R.string.conn_first));
+ return;
+ }
+ try {
+
+ printer.getPortManager().writeDataImmediately(PrintContent.getXmlBitmap(context));
+ } catch (IOException e) {
+ tipsDialog(getString(R.string.status_error)+e.getMessage());
+ }catch (Exception e){
+ tipsDialog(getString(R.string.status_error)+e.getMessage());
+ }
+ }
+ });
+ }
+ /**
+ * 打印PDF
+ * @param view
+ */
+ public void printPDF(View view) {
+ if (!permissionUtils.hasPermissions(context,Manifest.permission.READ_EXTERNAL_STORAGE)){
+ Utils.shortToast(context,getString(R.string.no_read));
+ return;
+ }
+ ThreadPoolManager.getInstance().addTask(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (printer.getPortManager()==null){
+ tipsToast(getString(R.string.conn_first));
+ return;
+ }
+ //打印前后查询打印机状态,部分老款打印机不支持查询请去除下面查询代码
+ //****************** 查询状态 ***************************
+ if (swState.isChecked()) {
+ Command command = printer.getPortManager().getCommand();
+ int status = printer.getPrinterState(command,2000);
+ if (status != 0) {//打印机处于不正常状态、则不发送打印
+ Message msg = new Message();
+ msg.what = 0x01;
+ msg.arg1 = status;
+ handler.sendMessage(msg);
+ return;
+ }
+ }
+ //***************************************************************
+ File file = null;
+ try {
+ file= new File(context.getExternalCacheDir(), "WalmartFile.pdf");
+ if (!file.exists()) {
+ // Since PdfRenderer cannot handle the compressed asset file directly, we copy it into
+ // the cache directory.
+ InputStream asset = context.getAssets().open("WalmartFile.pdf");
+ FileOutputStream output = new FileOutputStream(file);
+ final byte[] buffer = new byte[1024];
+ int size;
+ while ((size = asset.read(buffer)) != -1) {
+ output.write(buffer, 0, size);
+ }
+ asset.close();
+ output.close();
+ }
+ }catch (IOException e){
+ tipsToast(getString(R.string.pdf_error));
+ return;
+ }
+ boolean result= printer.getPortManager().writePDFToTsc(file,576,0,true,true,false,160);
+ if (result) {
+ tipsDialog(getString(R.string.send_success));
+ }else {
+ tipsDialog(getString(R.string.send_fail));
+ }
+ LogUtils.e("send result",result);
+ } catch (IOException e) {
+ tipsDialog(getString(R.string.disconnect)+"\n"+getString(R.string.print_fail)+e.getMessage());
+ }catch (Exception e){
+ tipsDialog(getString(R.string.print_fail)+e.getMessage());
+ }
+ }
+ });
+ }
+
+ /**
+ * 检查标签打印机状态
+ * @param view
+ */
+ public void checkState(View view) {
+ ThreadPoolManager.getInstance().addTask(new Runnable() {
+ @Override
+ public void run() {
+ if (printer.getPortManager()==null){
+ tipsToast(getString(R.string.conn_first));
+ return;
+ }
+ try {
+ Command command=printer.getPortManager().getCommand();
+ int status=printer.getPrinterState(command,2000);
+ Message msg=new Message();
+ msg.what=0x01;
+ msg.arg1=status;
+ handler.sendMessage(msg);
+ } catch (IOException e) {
+ tipsDialog(getString(R.string.status_error)+e.getMessage());
+ }catch (Exception e){
+ tipsDialog(getString(R.string.status_error)+e.getMessage());
+ }
+ }
+ });
+ }
+ /**
+ * 提示弹框
+ * @param message
+ */
+ private void tipsToast(String message){
+ Message msg =new Message();
+ msg.what=0x00;
+ msg.obj=message;
+ handler.sendMessage(msg);
+ }
+ /**
+ * 提示弹框
+ * @param message
+ */
+ private void tipsDialog(String message){
+ Message msg =new Message();
+ msg.what=0x03;
+ msg.obj=message;
+ handler.sendMessage(msg);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (resultCode== Activity.RESULT_OK){
+ switch (requestCode){
+ case 0x00://蓝牙返回mac地址
+ String mac =data.getStringExtra(BlueToothDeviceActivity.EXTRA_DEVICE_ADDRESS);
+ Log.e(TAG, SDKUtils.bytesToHexString(mac.getBytes()));
+ PrinterDevices blueTooth=new PrinterDevices.Build()
+ .setContext(context)
+ .setConnMethod(ConnMethod.BLUETOOTH)
+ .setMacAddress(mac)
+ .setCommand(Command.TSC)
+ .setCallbackListener(this)
+ .build();
+ printer.connect(blueTooth);
+ break;
+ case 0x01://usb返回USB名称
+ String name =data.getStringExtra(UsbDeviceActivity.USB_NAME);
+ UsbDevice usbDevice = Utils.getUsbDeviceFromName(context, name);
+ PrinterDevices usb=new PrinterDevices.Build()
+ .setContext(context)
+ .setConnMethod(ConnMethod.USB)
+ .setUsbDevice(usbDevice)
+ .setCommand(Command.TSC)
+ .setCallbackListener(this)
+ .build();
+ printer.connect(usb);
+ break;
+ case 0x02://WIFI返回ip
+ String ip =data.getStringExtra(WifiDeviceActivity.IP);
+ PrinterDevices wifi=new PrinterDevices.Build()
+ .setContext(context)
+ .setConnMethod(ConnMethod.WIFI)
+ .setIp(ip)
+ .setPort(9100)//打印唯一端口9100
+ .setCommand(Command.TSC)
+ .setCallbackListener(this)
+ .build();
+ printer.connect(wifi);
+ break;
+ case 0x03://串口返回路径、波特率
+ int baudRate = data.getIntExtra(SerialPortDeviceActivity.SERIALPORT_BAUDRATE, 9600);
+ String path = data.getStringExtra(SerialPortDeviceActivity.SERIALPORT_PATH);
+ PrinterDevices serialPort=new PrinterDevices.Build()
+ .setContext(context)
+ .setConnMethod(ConnMethod.SERIALPORT)
+ .setSerialPort(path)
+ .setBaudrate(baudRate)
+ .setCommand(Command.TSC)
+ .setCallbackListener(this)
+ .build();
+ printer.connect(serialPort);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onConnecting() {//连接打印机中
+ tvState.setText(getString(R.string.conning));
+ }
+
+ @Override
+ public void onCheckCommand() {//查询打印机指令
+ tvState.setText(getString(R.string.checking));
+ }
+
+ @Override
+ public void onSuccess(PrinterDevices devices) {//连接成功
+ Toast.makeText(context,getString(R.string.conn_success),Toast.LENGTH_SHORT).show();
+ tvState.setText(devices.toString());
+ }
+
+ @Override
+ public void onReceive(byte[] bytes) {
+
+ }
+
+ @Override
+ public void onFailure() {//连接失败
+ Toast.makeText(context,getString(R.string.conn_fail),Toast.LENGTH_SHORT).show();
+ handler.obtainMessage(0x02).sendToTarget();
+ }
+
+ @Override
+ public void onDisconnect() {//断开连接
+ Toast.makeText(context,getString(R.string.disconnect),Toast.LENGTH_SHORT).show();
+ handler.obtainMessage(0x02).sendToTarget();
+ }
+ //申请权限返回
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ permissionUtils.handleRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (printer.getPortManager()!=null){
+ printer.close();
+ }
+ }
+
+
+
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/PermissionUtils.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/PermissionUtils.java
new file mode 100755
index 0000000..81318a8
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/PermissionUtils.java
@@ -0,0 +1,236 @@
+package com.printer.tscdemo;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author deadline
+ * @time 2016-10-28
+ * @usage android >=M 的权限申请统一处理
+ *
+ * notice:
+ * 很多手机对原生系统做了修改,比如小米4的6.0的shouldShowRequestPermissionRationale
+ * 就一直返回false,而且在申请权限时,如果用户选择了拒绝,则不会再弹出对话框了, 因此有了
+ * void doAfterDenied(String... permission);
+ */
+
+public class PermissionUtils {
+
+ private static final int REQUEST_PERMISSION_CODE = 1000;
+
+ private Object mContext;
+
+ private PermissionListener mListener;
+
+ private List mPermissionList;
+
+ public PermissionUtils(@NonNull Object object) {
+ checkCallingObjectSuitability(object);
+ this.mContext = object;
+
+ }
+
+
+ /**
+ * 权限授权申请
+ *
+ * @param hintMessage 要申请的权限的提示
+ * @param permissions 要申请的权限
+ * @param listener 申请成功之后的callback
+ */
+ public void requestPermissions(@NonNull CharSequence hintMessage,
+ @Nullable PermissionListener listener,
+ @NonNull final String... permissions) {
+ if (listener != null) {
+ mListener = listener;
+ }
+ mPermissionList = Arrays.asList(permissions);
+ //没全部权限
+ if (!hasPermissions(mContext, permissions)) {
+ //需要向用户解释为什么申请这个权限
+ boolean shouldShowRationale = false;
+ for (String perm : permissions) {
+ shouldShowRationale =
+ shouldShowRationale || shouldShowRequestPermissionRationale(mContext, perm);
+ }
+
+ if (shouldShowRationale) {
+ showMessageOKCancel(hintMessage, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ executePermissionsRequest(mContext, permissions,
+ REQUEST_PERMISSION_CODE);
+
+ }
+ });
+ } else {
+ executePermissionsRequest(mContext, permissions,
+ REQUEST_PERMISSION_CODE);
+ }
+ } else if (mListener != null) { //有全部权限
+ mListener.doAfterGrand(permissions);
+ }
+ }
+
+ /**
+ * 处理onRequestPermissionsResult
+ *
+ * @param requestCode
+ * @param permissions
+ * @param grantResults
+ */
+ public void handleRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case REQUEST_PERMISSION_CODE:
+ boolean allGranted = true;
+ for (int grant : grantResults) {
+ if (grant != PackageManager.PERMISSION_GRANTED) {
+ allGranted = false;
+ break;
+ }
+ }
+
+ if (allGranted && mListener != null) {
+
+ mListener.doAfterGrand((String[]) mPermissionList.toArray());
+
+ } else if (!allGranted && mListener != null) {
+ mListener.doAfterDenied((String[]) mPermissionList.toArray());
+ }
+ break;
+ }
+ }
+
+ /**
+ * 判断是否具有某权限
+ *
+ * @param object
+ * @param perms
+ * @return
+ */
+ public static boolean hasPermissions(@NonNull Object object, @NonNull String... perms) {
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return true;
+ }
+ for (String perm : perms) {
+ boolean hasPerm = (ContextCompat.checkSelfPermission(getActivity(object), perm) ==
+ PackageManager.PERMISSION_GRANTED);
+ if (!hasPerm) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+
+ /**
+ * 兼容fragment
+ *
+ * @param object
+ * @param perm
+ * @return
+ */
+ @TargetApi(23)
+ private static boolean shouldShowRequestPermissionRationale(@NonNull Object object, @NonNull String perm) {
+ if (object instanceof Activity) {
+ return ActivityCompat.shouldShowRequestPermissionRationale((Activity) object, perm);
+ } else if (object instanceof Fragment) {
+ return ((Fragment) object).shouldShowRequestPermissionRationale(perm);
+ } else if (object instanceof Fragment) {
+ return ((Fragment) object).shouldShowRequestPermissionRationale(perm);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * 执行申请,兼容fragment
+ *
+ * @param object
+ * @param perms
+ * @param requestCode
+ */
+ @TargetApi(23)
+ private void executePermissionsRequest(@NonNull Object object, @NonNull String[] perms, int requestCode) {
+ if (object instanceof Activity) {
+ ActivityCompat.requestPermissions((Activity) object, perms, requestCode);
+ } else if (object instanceof Fragment) {
+ ((Fragment) object).requestPermissions(perms, requestCode);
+ } else if (object instanceof Fragment) {
+ ((Fragment) object).requestPermissions(perms, requestCode);
+ }
+ }
+
+ /**
+ * 检查传递Context是否合法
+ *
+ * @param object
+ */
+ private void checkCallingObjectSuitability(@Nullable Object object) {
+ if (object == null) {
+ throw new NullPointerException("Activity or Fragment should not be null");
+ }
+
+ boolean isActivity = object instanceof Activity;
+ boolean isSupportFragment = object instanceof Fragment;
+ boolean isAppFragment = object instanceof Fragment;
+ if (!(isSupportFragment || isActivity || (isAppFragment && isNeedRequest()))) {
+ if (isAppFragment) {
+ throw new IllegalArgumentException(
+ "Target SDK needs to be greater than 23 if caller is android.app.Fragment");
+ } else {
+ throw new IllegalArgumentException("Caller must be an Activity or a Fragment.");
+ }
+ }
+ }
+
+
+ @TargetApi(11)
+ private static Activity getActivity(@NonNull Object object) {
+ if (object instanceof Activity) {
+ return ((Activity) object);
+ } else if (object instanceof Fragment) {
+ return ((Fragment) object).getActivity();
+ } else if (object instanceof Fragment) {
+ return ((Fragment) object).getActivity();
+ } else {
+ return null;
+ }
+ }
+
+ public static boolean isNeedRequest() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
+ }
+
+ public void showMessageOKCancel(CharSequence message, DialogInterface.OnClickListener okListener) {
+ new AlertDialog.Builder(getActivity(mContext))
+ .setMessage(message)
+ .setPositiveButton(getActivity(mContext).getString(android.R.string.ok), okListener)
+ .setNegativeButton(getActivity(mContext).getString(android.R.string.cancel), null)
+ .setCancelable(false)
+ .create()
+ .show();
+ }
+
+ public interface PermissionListener {
+
+ void doAfterGrand(String... permission);
+
+ void doAfterDenied(String... permission);
+ }
+}
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/PrintContent.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/PrintContent.java
new file mode 100755
index 0000000..8148145
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/PrintContent.java
@@ -0,0 +1,196 @@
+package com.printer.tscdemo;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.view.View;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+import com.gprinter.command.LabelCommand;
+import com.gprinter.utils.GpUtils;
+import com.gprinter.utils.PDFUtils;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.security.PublicKey;
+import java.util.Vector;
+
+/**
+ * Copyright (C), 2012-2020, 珠海佳博科技股份有限公司
+ * FileName: PrintConntent
+ * Author: Circle
+ * Date: 2020/7/20 10:04
+ * Description: 打印内容
+ */
+public class PrintContent {
+ /**
+ * 标签打印测试页
+ *
+ * @return
+ */
+ public static Vector getLabel(Context context,int gap) {
+ LabelCommand tsc = new LabelCommand();
+ // 设置标签尺寸宽高,按照实际尺寸设置 单位mm
+ tsc.addUserCommand("\r\n");
+ tsc.addSize(58, 70);
+ // 设置标签间隙,按照实际尺寸设置,如果为无间隙纸则设置为0 单位mm
+ tsc.addGap(gap);
+ //设置纸张类型为黑标,发送BLINE 指令不能同时发送GAP指令
+// tsc.addBline(2);
+ // 设置打印方向
+ tsc.addDirection(LabelCommand.DIRECTION.FORWARD, LabelCommand.MIRROR.NORMAL);
+ // 设置原点坐标
+ tsc.addReference(0, 0);
+ //设置浓度
+ tsc.addDensity(LabelCommand.DENSITY.DNESITY4);
+ // 撕纸模式开启
+ tsc.addTear(LabelCommand.RESPONSE_MODE.ON);
+ // 清除打印缓冲区
+ tsc.addCls();
+ // 绘制简体中文
+ tsc.addText(30, 20, LabelCommand.FONTTYPE.SIMPLIFIED_24_CHINESE, LabelCommand.ROTATION.ROTATION_0, LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,
+ "欢迎使用Printer");
+ //打印繁体
+ tsc.addUnicodeText(30,50, LabelCommand.FONTTYPE.TRADITIONAL_CHINESE, LabelCommand.ROTATION.ROTATION_0, LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"BIG5碼繁體中文","BIG5");
+ //打印韩文
+ tsc.addUnicodeText(30,80, LabelCommand.FONTTYPE.KOREAN, LabelCommand.ROTATION.ROTATION_0, LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"Korean 지아보 하성","EUC_KR");
+ //英数字
+ tsc.addText(240,20, LabelCommand.FONTTYPE.FONT_1, LabelCommand.ROTATION.ROTATION_0,LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"1");
+ tsc.addText(250,20, LabelCommand.FONTTYPE.FONT_2, LabelCommand.ROTATION.ROTATION_0,LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"2");
+ tsc.addText(270,20, LabelCommand.FONTTYPE.FONT_3, LabelCommand.ROTATION.ROTATION_0,LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"3");
+ tsc.addText(300,20, LabelCommand.FONTTYPE.FONT_4, LabelCommand.ROTATION.ROTATION_0,LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"4");
+ tsc.addText(330,20, LabelCommand.FONTTYPE.FONT_5, LabelCommand.ROTATION.ROTATION_0,LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"5");
+ tsc.addText(240,40, LabelCommand.FONTTYPE.FONT_6, LabelCommand.ROTATION.ROTATION_0,LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"6");
+ tsc.addText(250,40, LabelCommand.FONTTYPE.FONT_7, LabelCommand.ROTATION.ROTATION_0,LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"7");
+ tsc.addText(270,40, LabelCommand.FONTTYPE.FONT_8, LabelCommand.ROTATION.ROTATION_0,LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"8");
+ tsc.addText(300,60, LabelCommand.FONTTYPE.FONT_9, LabelCommand.ROTATION.ROTATION_0,LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"9");
+ tsc.addText(330,80, LabelCommand.FONTTYPE.FONT_10, LabelCommand.ROTATION.ROTATION_0,LabelCommand.FONTMUL.MUL_1, LabelCommand.FONTMUL.MUL_1,"10");
+ Bitmap b = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_priter);
+ // 绘制图片
+ tsc.drawImage(30, 100, 300, b);
+ Bitmap b2= BitmapFactory.decodeResource(context.getResources(), R.drawable.flower);
+ tsc.drawJPGImage(200,250,200,b2);
+ //绘制二维码
+ tsc.addQRCode(30,250, LabelCommand.EEC.LEVEL_L, 5, LabelCommand.ROTATION.ROTATION_0, " www.smarnet.cc");
+ // 绘制一维条码
+ tsc.add1DBarcode(30, 380, LabelCommand.BARCODETYPE.CODE128, 80, LabelCommand.READABEL.EANBEL, LabelCommand.ROTATION.ROTATION_0, "12345678");
+ // 打印标签
+ tsc.addPrint(1, 1);
+ // 打印标签后 蜂鸣器响
+ tsc.addSound(2, 100);
+ //开启钱箱
+ tsc.addCashdrwer(LabelCommand.FOOT.F5, 255, 255);
+ Vector datas = tsc.getCommand();
+ // 发送数据
+ return datas;
+ }
+
+
+ /**
+ * 获取图片
+ * @param context
+ * @return
+ */
+ public static Bitmap getBitmap(Context context) {
+ View v = View.inflate(context, R.layout.page, null);
+ TableLayout tableLayout = (TableLayout) v.findViewById(R.id.line);
+ TextView total = (TextView) v.findViewById(R.id.total);
+ TextView cashier = (TextView) v.findViewById(R.id.cashier);
+ tableLayout.addView(ctv(context, "红茶\n加热\n加糖", 3, 8));
+ tableLayout.addView(ctv(context, "绿茶", 899, 109));
+ tableLayout.addView(ctv(context, "咖啡", 4, 15));
+ tableLayout.addView(ctv(context, "红茶", 3, 8));
+ tableLayout.addView(ctv(context, "绿茶", 8, 10));
+ tableLayout.addView(ctv(context, "咖啡", 4, 15));
+ total.setText("998");
+ cashier.setText("张三");
+ final Bitmap bitmap = convertViewToBitmap(v);
+ return bitmap;
+ }
+ /**
+ * mxl转bitmap图片
+ * @return
+ */
+ public static Bitmap convertViewToBitmap(View view){
+ view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+ view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
+ view.buildDrawingCache();
+ Bitmap bitmap = view.getDrawingCache();
+ return bitmap;
+ }
+
+ public static TableRow ctv(Context context, String name, int k, int n){
+ TableRow tb=new TableRow(context);
+ tb.setLayoutParams(new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT ,TableLayout.LayoutParams.WRAP_CONTENT));
+ TextView tv1=new TextView(context);
+ tv1.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT ,TableRow.LayoutParams.WRAP_CONTENT));
+ tv1.setText(name);
+ tv1.setTextColor(Color.BLACK);
+ tb.addView(tv1);
+ TextView tv2=new TextView(context);
+ tv2.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT ,TableRow.LayoutParams.WRAP_CONTENT));
+ tv2.setText(k+"");
+ tv2.setTextColor(Color.BLACK);
+ tb.addView(tv2);
+ TextView tv3=new TextView(context);
+ tv3.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT ,TableRow.LayoutParams.WRAP_CONTENT));
+ tv3.setText(n+"");
+ tv3.setTextColor(Color.BLACK);
+ tb.addView(tv3);
+ return tb;
+ }
+ /**
+ * 获取Assets文件
+ * @param fileName
+ * @return
+ */
+ public static String getFromAssets(Context context,String fileName) {
+ String result = "";
+ try {
+ InputStreamReader inputReader = new InputStreamReader(context.getResources().getAssets().open(fileName));
+ BufferedReader bufReader = new BufferedReader(inputReader);
+ String line = "";
+ while ((line = bufReader.readLine()) != null)
+ result += line+"\r\n";
+ return result;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return result;
+ }
+ public static Vector getXmlBitmap(Context context){
+ LabelCommand tsc = new LabelCommand();
+ // 设置标签尺寸宽高,按照实际尺寸设置 单位mm
+ tsc.addUserCommand("\r\n");
+ tsc.addSize(58, 100);
+ // 设置标签间隙,按照实际尺寸设置,如果为无间隙纸则设置为0 单位mm
+ tsc.addGap(0);
+ // 设置打印方向
+ tsc.addDirection(LabelCommand.DIRECTION.FORWARD, LabelCommand.MIRROR.NORMAL);
+ // 设置原点坐标
+ tsc.addReference(0, 0);
+ //设置浓度
+ tsc.addDensity(LabelCommand.DENSITY.DNESITY4);
+ // 撕纸模式开启
+ tsc.addTear(LabelCommand.RESPONSE_MODE.ON);
+ // 清除打印缓冲区
+ tsc.addCls();
+ Bitmap bitmap=getBitmap(context);
+ // 绘制图片
+ /**
+ * x:打印起始横坐标
+ * y:打印起始纵坐标
+ * mWidth:打印宽度以dot为单位
+ * nbitmap:源图
+ */
+ tsc.drawXmlImage(10,10,bitmap.getWidth(),bitmap);
+ // 打印标签
+ tsc.addPrint(1, 1);
+ return tsc.getCommand();
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/PrintContent2.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/PrintContent2.java
new file mode 100755
index 0000000..c17ea6f
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/PrintContent2.java
@@ -0,0 +1,413 @@
+package com.printer.tscdemo;
+
+import static com.gprinter.bean.BarCodeType.BARCODE_CODE128;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.view.View;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+import com.gprinter.command.EscCommand;
+import com.gprinter.command.EscForDotCommand;
+import com.gprinter.command.LabelCommand;
+import com.gprinter.utils.Menu58Utils;
+import com.gprinter.utils.Menu80Utils;
+
+import java.util.Vector;
+
+/**
+ * Copyright (C), 2012-2020, 打印机有限公司
+ * FileName: PrintConntent
+ * Author: Circle
+ * Date: 2020/7/20 10:04
+ * Description: 打印内容
+ */
+public class PrintContent2 {
+ /**
+ * 小票案例
+ * @param context
+ *
+ * @return
+ */
+ public static Vector getReceiptChinese(Context context,int width) {
+ EscCommand esc = new EscCommand();
+ //初始化打印机
+ esc.addInitializePrinter();
+ // 设置打印居中
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.CENTER);
+ //字体放大两倍
+ esc.addSetCharcterSize(EscCommand.WIDTH_ZOOM.MUL_2, EscCommand.HEIGHT_ZOOM.MUL_2);
+ // 打印文字
+ esc.addText("票据测试\n");
+
+ //字体正常
+ esc.addSetCharcterSize(EscCommand.WIDTH_ZOOM.MUL_1, EscCommand.HEIGHT_ZOOM.MUL_1);
+ //打印并换行
+ esc.addPrintAndLineFeed();
+ // 设置打印左对齐
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.LEFT);
+ // 打印文字
+ esc.addText("打印文字测试:\n");
+
+// esc.addSetKanjiUnderLine(EscCommand.UNDERLINE_MODE.UNDERLINE_2DOT);//汉字下划线
+ esc.addTurnUnderlineModeOnOrOff(EscCommand.UNDERLINE_MODE.UNDERLINE_2DOT);//非汉字下划线
+ esc.addText("125545AA测试下划线\n");
+ esc.addTurnUnderlineModeOnOrOff(EscCommand.UNDERLINE_MODE.OFF);//非汉字取消下划线
+
+ // 打印文字
+ esc.addText("欢迎使用打印机!\n");
+ esc.addPrintAndLineFeed();
+ //对齐方式
+ esc.addText("打印对齐方式测试:\n");
+ //设置居左
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.LEFT);
+ esc.addText("居左");
+ esc.addPrintAndLineFeed();
+ //设置居中
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.CENTER);
+ esc.addText("居中");
+ esc.addPrintAndLineFeed();
+ //设置居右
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.RIGHT);
+ esc.addText("居右");
+ esc.addPrintAndLineFeed();
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.LEFT);
+ // 打印图片
+ esc.addText("打印Bitmap图测试:\n");
+ Bitmap b = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_priter);//二维码图片,图片类型bitmap
+ // 打印图片,58打印机图片宽度最大为384dot 1mm=8dot 用尺子量取图片的宽度单位为Xmm 传入宽度值为 X*8
+ //打印图片,80打印机图片宽度最大为576dot 1mm=8dot 用尺子量取图片的宽度单位为Xmm 传入宽度值为 X*8
+ esc.drawImage(b, width);
+ esc.addPrintAndLineFeed();
+ //打印条码
+ esc.addText("打印条码测试:\n");
+ // 设置条码可识别字符位置在条码下方
+ esc.addSelectPrintingPositionForHRICharacters(EscCommand.HRI_POSITION.BELOW);
+ // 设置条码高度为60点
+ esc.addSetBarcodeHeight((byte) 60);
+ // 设置条码单元宽度为1
+ esc.addSetBarcodeWidth((byte) 1);
+ // 打印Code128码内容
+ esc.addCODE128(esc.genCodeB("barcode128"));
+ esc.addPrintAndLineFeed();
+ // 打印二维码
+ esc.addText("打印二维码测试:\n");
+ // 设置纠错等级
+ esc.addSelectErrorCorrectionLevelForQRCode((byte) 0x31);
+ // 设置qrcode模块大小
+ esc.addSelectSizeOfModuleForQRCode((byte) 4);
+ // 设置qrcode内容
+ esc.addStoreQRCodeData("www.baidu.com");
+ // 打印QRCode
+ esc.addPrintQRCode();
+ esc.addPrintAndLineFeed();
+
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.CENTER);
+ //打印文字
+ esc.addSelectCharacterFont(EscCommand.FONT.FONTB);
+ esc.addText("测试完成!\r\n");
+ // 设置打印居左
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.LEFT);
+ esc.addPrintAndLineFeed();
+ esc.addPrintAndFeedLines((byte) 4);
+ //切纸(带切刀打印机才可用)
+ esc.addCutPaper();
+ // 开钱箱
+ esc.addGeneratePlus(LabelCommand.FOOT.F2, (byte) 255, (byte) 255);
+ esc.addInitializePrinter();
+ //返回指令集
+ return esc.getCommand();
+ }
+
+
+ public static Vector getImgRes(Context context,int res) {
+ EscCommand esc = new EscCommand();
+ //初始化打印机
+ esc.addInitializePrinter();
+ // 设置打印居中
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.CENTER);
+ Bitmap b = BitmapFactory.decodeResource(context.getResources(), res);
+ // 打印图片,图片宽度为384dot 1mm=8dot 用尺子量取图片的宽度单位为Xmm 传入宽度值为 X*8
+ esc.drawJpgImage(b, 200);
+ esc.addText("\n\n\n\n");
+ esc.addPrintAndLineFeed();
+ //切纸(带切刀打印机才可用)
+ esc.addCutPaper();
+ // 开钱箱
+ esc.addGeneratePlus(LabelCommand.FOOT.F2, (byte) 255, (byte) 255);
+ esc.addInitializePrinter();
+ return esc.getCommand();
+ }
+
+
+ /**
+ * 针式打印指令
+ * @param qrCode
+ * @param barCode
+ * @return
+ */
+ public static Vector getDotPrintCommand(String qrCode,String barCode) {
+ EscForDotCommand esc = new EscForDotCommand();
+ //初始化打印机
+ esc.addInitializePrinter();
+ // 设置打印居中
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.CENTER);
+ esc.addText("二维码打印");
+ esc.addText("\n\n");
+ esc.addQrcode(100,100,qrCode);
+ // 打印图片,图片宽度为384dot 1mm=8dot 用尺子量取图片的宽度单位为Xmm 传入宽度值为 X*8
+ esc.addText("\n\n");
+ esc.addText("条形码打印");
+ esc.addText("\n\n");
+ esc.addBarcode(BARCODE_CODE128,200,50,true,12,barCode);
+ esc.addText("\n\n\n\n");
+ esc.addPrintAndLineFeed();
+ //切纸(带切刀打印机才可用)
+ esc.addCutPaper();
+ // 开钱箱
+ esc.addGeneratePlus(LabelCommand.FOOT.F2, (byte) 255, (byte) 255);
+ esc.addInitializePrinter();
+ return esc.getCommand();
+ }
+
+
+ /**
+ * 菜单样例
+ * @param context
+ * @return
+ */
+ public static Vector get58Menu(Context context) {
+ EscCommand esc = new EscCommand();
+ //初始化打印机
+ esc.addInitializePrinter();
+ // 设置打印居中
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.CENTER);
+ Bitmap b = BitmapFactory.decodeResource(context.getResources(), R.drawable.flower);//二维码图片,图片类型bitmap
+ // 打印图片,图片宽度为384dot 1mm=8dot 用尺子量取图片的宽度单位为Xmm 传入宽度值为 X*8
+ esc.drawJpgImage(b, 200);
+ // 设置为倍高倍宽
+ //字体放大两倍
+ esc.addSetCharcterSize(EscCommand.WIDTH_ZOOM.MUL_2, EscCommand.HEIGHT_ZOOM.MUL_2);
+ // 打印文字
+ esc.addText("爱情餐厅\n\n");
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.LEFT);
+ esc.addText("520号桌\n\n");
+ //字体正常
+ esc.addSetCharcterSize(EscCommand.WIDTH_ZOOM.MUL_1, EscCommand.HEIGHT_ZOOM.MUL_1);
+ esc.addText("点菜时间 2020-05-20 5:20\n");
+ esc.addText("上菜时间 2020-05-20 13:14\n");
+ esc.addText("人数:2人 点菜员:新疆包工头\n");
+ esc.addText("--------------------------------\n");
+ //开启加粗
+ esc.addTurnEmphasizedModeOnOrOff(EscCommand.ENABLE.ON);
+ esc.addText(Menu58Utils.printThreeData("菜名", "数量", "金额"));
+ esc.addTurnEmphasizedModeOnOrOff(EscCommand.ENABLE.OFF);
+ esc.addText(Menu58Utils.printThreeData("北京烤鸭", "1", "99.99"));
+ esc.addText(Menu58Utils.printThreeData("四川麻婆豆腐", "1", "39.99"));
+ esc.addText(Menu58Utils.printThreeData("西湖醋鱼", "1", "59.99"));
+ esc.addText(Menu58Utils.printThreeData("飞龙汤", "1", "66.66"));
+ esc.addText(Menu58Utils.printThreeData("无为熏鸭", "1", "88.88"));
+ esc.addText(Menu58Utils.printThreeData("东坡肉", "1", "39.99"));
+ esc.addText(Menu58Utils.printThreeData("辣子鸡", "1", "66.66"));
+ esc.addText(Menu58Utils.printThreeData("腊味合蒸", "1", "108.00"));
+ esc.addText(Menu58Utils.printThreeData("东安子鸡", "1", "119.00"));
+ esc.addText(Menu58Utils.printThreeData("清蒸武昌鱼", "1", "88.88"));
+ esc.addText(Menu58Utils.printThreeData("再来两瓶82年的快乐肥宅水(去冰)", "1", "9.99"));
+ esc.addText(Menu58Utils.printThreeData("老干妈拌饭(加辣、加香菜)", "1", "6.66"));
+ esc.addText("--------------------------------\n\n");
+ esc.addTurnEmphasizedModeOnOrOff(EscCommand.ENABLE.ON);
+ esc.addText(Menu58Utils.printTwoData("合计:", "1314.00"));
+ esc.addText(Menu58Utils.printTwoData("抹零:", "14.00"));
+ esc.addText(Menu58Utils.printTwoData("应收:", "1300.00"));
+ esc.addTurnEmphasizedModeOnOrOff(EscCommand.ENABLE.OFF);
+ esc.addText("--------------------------------\n");
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.RIGHT);
+ esc.addText("收银员:广东包租公\n");
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.LEFT);
+ esc.addText("宣言:我点个鸡蛋都是爱你的形状哦");
+ esc.addPrintAndLineFeed();
+ esc.addPrintAndLineFeed();
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.CENTER);
+ // 设置纠错等级
+ esc.addSelectErrorCorrectionLevelForQRCode((byte) 0x31);
+ // 设置qrcode模块大小
+ esc.addSelectSizeOfModuleForQRCode((byte) 4);
+ // 设置qrcode内容
+ esc.addStoreQRCodeData("https://www.baidu.com");
+ // 打印QRCode
+ esc.addPrintQRCode();
+ esc.addText("\n(扫二维码送手机)\n");
+ esc.addText("\n\n\n\n");
+ esc.addPrintAndLineFeed();
+ //切纸(带切刀打印机才可用)
+ esc.addCutPaper();
+ // 开钱箱
+ esc.addGeneratePlus(LabelCommand.FOOT.F2, (byte) 255, (byte) 255);
+ esc.addInitializePrinter();
+ //返回指令集
+ return esc.getCommand();
+ }
+ /**
+ * 菜单样例
+ * @param context
+ * @return
+ */
+ public static Vector get80Menu(Context context) {
+ EscCommand esc = new EscCommand();
+ //初始化打印机
+ esc.addInitializePrinter();
+ // 设置打印居中
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.CENTER);
+ Bitmap b = BitmapFactory.decodeResource(context.getResources(), R.drawable.flower);//二维码图片,图片类型bitmap
+ // 打印图片,图片宽度为384dot 1mm=8dot 用尺子量取图片的宽度单位为Xmm 传入宽度值为 X*8
+ esc.drawJpgImage(b, 200);
+ // 设置为倍高倍宽
+ //字体放大两倍
+ esc.addSetCharcterSize(EscCommand.WIDTH_ZOOM.MUL_2, EscCommand.HEIGHT_ZOOM.MUL_2);
+ // 打印文字
+ esc.addText("爱情餐厅\n\n");
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.LEFT);
+ esc.addText("520号桌\n\n");
+ //字体正常
+ esc.addSetCharcterSize(EscCommand.WIDTH_ZOOM.MUL_1, EscCommand.HEIGHT_ZOOM.MUL_1);
+ esc.addText("点菜时间 2020-05-20 5:20\n");
+ esc.addText("上菜时间 2020-05-20 13:14\n");
+ esc.addText("人数:2人 点菜员:新疆包工头\n");
+ esc.addText("------------------三行菜单案例------------------\n");
+ esc.addText(Menu80Utils.printThreeData("菜名", "数量", "金额"));
+ esc.addTurnEmphasizedModeOnOrOff(EscCommand.ENABLE.OFF);
+ esc.addText(Menu80Utils.printThreeData("北京烤鸭", "1", "99.99"));
+ esc.addText(Menu80Utils.printThreeData("四川麻婆豆腐", "1", "39.99"));
+ esc.addText(Menu80Utils.printThreeData("西湖醋鱼", "1", "59.99"));
+ esc.addText(Menu80Utils.printThreeData("飞龙汤", "1", "66.66"));
+ esc.addText(Menu80Utils.printThreeData("无为熏鸭", "1", "88.88"));
+ esc.addText(Menu80Utils.printThreeData("东坡肉", "1", "39.99"));
+ esc.addText("------------------四行菜单案例------------------\n");
+ //开启加粗
+ esc.addTurnEmphasizedModeOnOrOff(EscCommand.ENABLE.ON);
+ esc.addText(Menu80Utils.printFourData("菜名", "单价","数量", "金额"));
+ esc.addTurnEmphasizedModeOnOrOff(EscCommand.ENABLE.OFF);
+ esc.addText(Menu80Utils.printFourData("北京烤鸭", "99.99","1", "99.99"));
+ esc.addText(Menu80Utils.printFourData("四川麻婆豆腐", "39.99","1", "39.99"));
+ esc.addText(Menu80Utils.printFourData("西湖醋鱼", "59.99","1", "59.99"));
+ esc.addText(Menu80Utils.printFourData("飞龙汤", "66.66","1", "66.66"));
+ esc.addText(Menu80Utils.printFourData("无为熏鸭", "88.88","1", "88.88"));
+ esc.addText(Menu80Utils.printFourData("东坡肉", "39.99","1", "39.99"));
+ esc.addText(Menu80Utils.printFourData("辣子鸡", "66.66","1", "66.66"));
+ esc.addText(Menu80Utils.printFourData("腊味合蒸", "108.00","1", "108.00"));
+ esc.addText(Menu80Utils.printFourData("东安子鸡", "119.00","1", "119.00"));
+ esc.addText(Menu80Utils.printFourData("清蒸武昌鱼", "88.88","1", "88.88"));
+ esc.addText(Menu80Utils.printFourData("再来两瓶82年的快乐肥宅水(去冰)", "9.00","11", "99.00"));
+ esc.addText(Menu80Utils.printFourData("老干妈拌饭", "6.66","1", "6.66"));
+ esc.addText("------------------------------------------------\n");
+ esc.addText(Menu80Utils.printTwoData("合计:", "1314.00"));
+ esc.addText(Menu80Utils.printTwoData("抹零:", "14.00"));
+ esc.addText(Menu80Utils.printTwoData("应收:", "1300.00"));
+ esc.addText("------------------------------------------------\n");
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.RIGHT);
+ esc.addText("收银员:广东包租公\n");
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.LEFT);
+ esc.addText("宣言:我点个鸡蛋都是爱你的形状哦\n");
+
+ esc.addSelectJustification(EscCommand.JUSTIFICATION.CENTER);
+ // 设置纠错等级
+ esc.addSelectErrorCorrectionLevelForQRCode((byte) 0x31);
+ // 设置qrcode模块大小
+ esc.addSelectSizeOfModuleForQRCode((byte) 4);
+ // 设置qrcode内容
+ esc.addStoreQRCodeData("www.baidu.com");
+ // 打印QRCode
+ esc.addPrintQRCode();
+ esc.addText("\n(扫二维码送手机)\n");
+ esc.addText("\n\n\n\n\n");
+ esc.addPrintAndLineFeed();
+ //切纸(带切刀打印机才可用)
+ esc.addCutPaper();
+ // 开钱箱
+ esc.addGeneratePlus(LabelCommand.FOOT.F2, (byte) 255, (byte) 255);
+ esc.addInitializePrinter();
+ //返回指令集
+ return esc.getCommand();
+ }
+
+ /**
+ * 打印自检页
+ * @return
+ */
+ public static Vector getSelfTest() {
+ EscCommand esc = new EscCommand();
+ byte[] escSelfTestCommand = {0x1f, 0x1b, 0x1f, (byte) 0x93, 0x10, 0x11, 0x12, 0x15, 0x16, 0x17, 0x10, 0x00};
+ esc.addUserCommand(escSelfTestCommand);
+ return esc.getCommand();
+ }
+ /**
+ * 获取图片
+ * @param context
+ * @return
+ */
+ public static Bitmap getBitmap(Context context) {
+ View v = View.inflate(context, R.layout.page, null);
+ TableLayout tableLayout = (TableLayout) v.findViewById(R.id.line);
+ TextView total = (TextView) v.findViewById(R.id.total);
+ TextView cashier = (TextView) v.findViewById(R.id.cashier);
+ tableLayout.addView(ctv(context, "红茶\n加热\n加糖", 3, 8));
+ tableLayout.addView(ctv(context, "绿茶", 899, 109));
+ tableLayout.addView(ctv(context, "咖啡", 4, 15));
+ tableLayout.addView(ctv(context, "红茶", 3, 8));
+ tableLayout.addView(ctv(context, "绿茶", 8, 10));
+ tableLayout.addView(ctv(context, "咖啡", 4, 15));
+ tableLayout.addView(ctv(context, "红茶", 3, 8));
+ tableLayout.addView(ctv(context, "绿茶", 8, 10));
+ tableLayout.addView(ctv(context, "咖啡", 4, 15));
+ tableLayout.addView(ctv(context, "红茶", 3, 8));
+ tableLayout.addView(ctv(context, "绿茶", 8, 10));
+ tableLayout.addView(ctv(context, "咖啡", 4, 15));
+ tableLayout.addView(ctv(context, "红茶", 3, 8));
+ tableLayout.addView(ctv(context, "绿茶", 8, 10));
+ tableLayout.addView(ctv(context, "咖啡", 4, 15));
+ total.setText("998");
+ cashier.setText("张三");
+ final Bitmap bitmap = convertViewToBitmap(v);
+ return bitmap;
+ }
+ /**
+ * mxl转bitmap图片
+ * @param view
+ * @return
+ */
+ public static Bitmap convertViewToBitmap(View view){
+ view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+ view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
+ view.buildDrawingCache();
+ Bitmap bitmap = view.getDrawingCache();
+ return bitmap;
+ }
+
+ public static TableRow ctv(Context context, String name, int k, int n){
+ TableRow tb=new TableRow(context);
+ tb.setLayoutParams(new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT ,TableLayout.LayoutParams.WRAP_CONTENT));
+ TextView tv1=new TextView(context);
+ tv1.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT ,TableRow.LayoutParams.WRAP_CONTENT));
+ tv1.setText(name);
+ tv1.setTextColor(Color.BLACK);
+ tv1.setTextSize(30);
+ tb.addView(tv1);
+ TextView tv2=new TextView(context);
+ tv2.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT ,TableRow.LayoutParams.WRAP_CONTENT));
+ tv2.setText(k+"");
+ tv2.setTextColor(Color.BLACK);
+ tv2.setTextSize(30);
+ tb.addView(tv2);
+ TextView tv3=new TextView(context);
+ tv3.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT ,TableRow.LayoutParams.WRAP_CONTENT));
+ tv3.setText(n+"");
+ tv3.setTextColor(Color.BLACK);
+ tv3.setTextSize(30);
+ tb.addView(tv3);
+ return tb;
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/Printer.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/Printer.java
new file mode 100755
index 0000000..efca8a3
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/Printer.java
@@ -0,0 +1,166 @@
+package com.printer.tscdemo;
+
+import com.gprinter.bean.PrinterDevices;
+import com.gprinter.io.BluetoothPort;
+import com.gprinter.io.EthernetPort;
+import com.gprinter.io.PortManager;
+import com.gprinter.io.SerialPort;
+import com.gprinter.io.UsbPort;
+import com.gprinter.utils.Command;
+
+import java.io.IOException;
+import java.util.Vector;
+
+/**
+ * Copyright (C), 2012-2019, 打印机有限公司
+ * FileName: Printer
+ * Author: Circle
+ * Date: 2019/12/25 19:46
+ * Description: 打印机使用单例
+ */
+public class Printer {
+ public static Printer printer=null;
+ public static PortManager portManager=null;
+ public final PrinterDevices devices=null;
+ public Printer(){
+ }
+ /**
+ * 单例
+ * @return
+ */
+ public static Printer getInstance(){
+ if (printer==null){
+ printer=new Printer();
+ }
+ return printer;
+ }
+
+ /**
+ * 获取打印机管理类
+ * @return
+ */
+ public static PortManager getPortManager(){
+ return portManager;
+ }
+
+ /**
+ * 获取连接状态
+ * @return
+ */
+ public static boolean getConnectState(){
+ return portManager.getConnectStatus();
+ }
+
+ /**
+ * 连接
+ * @param devices
+ */
+ public static void connect(final PrinterDevices devices){
+ ThreadPoolManager.getInstance().addTask(new Runnable() {
+ @Override
+ public void run() {
+ if (portManager!=null) {//先close上次连接
+ portManager.closePort();
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ }
+ }
+ if (devices!=null) {
+ switch (devices.getConnMethod()) {
+ case BLUETOOTH://蓝牙
+ portManager = new BluetoothPort(devices);
+ portManager.openPort();
+ break;
+ case USB://USB
+ portManager = new UsbPort(devices);
+ portManager.openPort();
+ break;
+ case WIFI://WIFI
+ portManager = new EthernetPort(devices);
+ portManager.openPort();
+ break;
+ case SERIALPORT://串口
+ portManager=new SerialPort(devices);
+ portManager.openPort();
+ break;
+ default:
+ break;
+ }
+ }
+
+ }
+ });
+ }
+ /**
+ * 发送数据到打印机 字节数据
+ * @param vector
+ * @return true发送成功 false 发送失败
+ * 打印机连接异常或断开发送时会抛异常,可以捕获异常进行处理
+ */
+ public static boolean sendDataToPrinter(byte [] vector) throws IOException {
+ if (portManager==null){
+ return false;
+ }
+ return portManager.writeDataImmediately(vector);
+ }
+
+ /**
+ * 获取打印机状态
+ * @param printerCommand 打印机命令 ESC为小票,TSC为标签 ,CPCL为面单
+ * @return 返回值常见文档说明
+ * @throws IOException
+ */
+ public static int getPrinterState(Command printerCommand, long delayMillis)throws IOException {
+ return portManager.getPrinterStatus(printerCommand);
+ }
+
+ /**
+ * 获取打印机电量
+ * @return
+ * @throws IOException
+ */
+ public static int getPower() throws IOException {
+ return portManager.getPower();
+ }
+ /**
+ * 获取打印机指令
+ * @return
+ */
+ public static Command getPrinterCommand(){
+ return portManager.getCommand();
+ }
+
+ /**
+ * 设置使用指令
+ * @param printerCommand
+ */
+ public static void setPrinterCommand(Command printerCommand){
+ if (portManager==null){
+ return;
+ }
+ portManager.setCommand(printerCommand);
+ }
+ /**
+ * 发送数据到打印机 指令集合内容
+ * @param vector
+ * @return true发送成功 false 发送失败
+ * 打印机连接异常或断开发送时会抛异常,可以捕获异常进行处理
+ */
+ public static boolean sendDataToPrinter(Vector vector) throws IOException {
+ if (portManager==null){
+ return false;
+ }
+ return portManager.writeDataImmediately(vector);
+ }
+ /**
+ * 关闭连接
+ * @return
+ */
+ public static void close(){
+ if (portManager!=null){
+ portManager.closePort();
+ portManager=null;
+ }
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/SerialPortDeviceActivity.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/SerialPortDeviceActivity.java
new file mode 100755
index 0000000..2f16bfa
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/SerialPortDeviceActivity.java
@@ -0,0 +1,109 @@
+package com.printer.tscdemo;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.Spinner;
+
+import com.gprinter.utils.SerialPortFinder;
+
+
+/**
+ * Copyright (C), 2012-2019, 打印机有限公司
+ * FileName: Printer
+ * Author: Circle
+ * Date: 2019/12/25 19:46
+ * Description: 打印机使用单例
+ */
+public class SerialPortDeviceActivity extends Activity {
+ public static String SERIALPORT_PATH="SERIAL_PORT_PATH";
+ public static String SERIALPORT_BAUDRATE="SERIAL_PORT_BAUD_RATE";
+ private SerialPortFinder mSerialPortFinder;
+ private String[] entries;
+ private String[] entryValues;
+ private Spinner spSerialPortPath;
+ private Spinner spBaudrate;
+ private String path;
+ private int selectBaudrate;
+ private String[] baudrates;
+ private Button btnConfirm;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_serial_port);
+ baudrates = this.getResources().getStringArray(R.array.baudrate);
+ mSerialPortFinder = new SerialPortFinder();
+ entries = mSerialPortFinder.getAllDevices();
+ //获取串口路径
+ entryValues = mSerialPortFinder.getAllDevicesPath();
+ initView();
+ initListener();
+ }
+
+ private void initView() {
+ //串口路径初始化
+ spSerialPortPath = (Spinner) findViewById(R.id.sp_serialport_path);
+ ArrayAdapter arrayAdapter;
+ if (entries != null) {
+ arrayAdapter = new ArrayAdapter(this, R.layout.text_item, entries);
+ } else {
+ arrayAdapter = new ArrayAdapter(this, R.layout.text_item, new String[]{this.getString(R.string.str_no_serialport)});
+ }
+ spSerialPortPath.setAdapter(arrayAdapter);
+ //波特率数据初始化
+ spBaudrate = (Spinner) findViewById(R.id.sp_baudrate);
+ ArrayAdapter portAdapter = new ArrayAdapter(this, R.layout.text_item, this.getResources().getStringArray(R.array.baudrate));
+ spBaudrate.setAdapter(portAdapter);
+ btnConfirm = (Button) findViewById(R.id.btn_confirm);
+ }
+
+ private void initListener() {
+ spBaudrate.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ //保存当前选择的波特率
+ selectBaudrate = Integer.parseInt(baudrates[position]);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+
+ }
+ });
+
+ spSerialPortPath.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ if (entryValues != null) {
+ path = entryValues[position];
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+
+ }
+ });
+
+ btnConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!TextUtils.isEmpty(path)) {
+ Intent intent = new Intent();
+ Bundle bundle = new Bundle();
+ bundle.putString(SERIALPORT_PATH, path);
+ bundle.putInt(SERIALPORT_BAUDRATE, selectBaudrate);
+ intent.putExtras(bundle);
+ setResult(RESULT_OK, intent);
+ }
+ finish();
+ }
+ });
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/SharedPreferencesUtil.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/SharedPreferencesUtil.java
new file mode 100755
index 0000000..668f3b9
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/SharedPreferencesUtil.java
@@ -0,0 +1,69 @@
+package com.printer.tscdemo;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/**
+ * Created by Administrator
+ */
+public class SharedPreferencesUtil {
+ private static final String NAME = "Configs";
+ private static SharedPreferences sharedPreferences;
+ private static SharedPreferencesUtil sharedPreferencesUtil;
+
+ private SharedPreferencesUtil(){
+
+ }
+
+ public static SharedPreferencesUtil getInstantiation(Context context) {
+ if (sharedPreferences == null) {
+ sharedPreferencesUtil = new SharedPreferencesUtil();
+ getSharedPreferences(context);
+ }
+ return sharedPreferencesUtil;
+ }
+
+ private static void getSharedPreferences(Context context) {
+ sharedPreferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE);
+ }
+
+ public void putInt(int value, String key) {
+ sharedPreferences.edit().putInt(key, value).apply();
+ }
+
+ public void putString(String value, String key) {
+ sharedPreferences.edit().putString(key, value).apply();
+ }
+
+ public void putBoolean(boolean value, String key) {
+ sharedPreferences.edit().putBoolean(key, value).apply();
+ }
+
+ public void putFloat(float value, String key) {
+ sharedPreferences.edit().putFloat(key, value).apply();
+ }
+
+ public void putLong(long value, String key) {
+ sharedPreferences.edit().putLong(key, value).apply();
+ }
+
+ public int getInt(int defaultValue, String key) {
+ return sharedPreferences.getInt(key, defaultValue);
+ }
+
+ public String getString(String defaultValue, String key) {
+ return sharedPreferences.getString(key, defaultValue);
+ }
+
+ public boolean getBoolean(boolean defaultValue, String key) {
+ return sharedPreferences.getBoolean(key, defaultValue);
+ }
+
+ public float getFloat(float defaultValue, String key) {
+ return sharedPreferences.getFloat(key, defaultValue);
+ }
+
+ public void clear() {
+ sharedPreferences.edit().clear().apply();
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/ThreadFactoryBuilder.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/ThreadFactoryBuilder.java
new file mode 100755
index 0000000..9a175c9
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/ThreadFactoryBuilder.java
@@ -0,0 +1,30 @@
+package com.printer.tscdemo;
+
+
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Created by Circle on 2018/5/24.
+ */
+
+/**
+ * 作者: Circle
+ * 创造于 2018/5/24.
+ */
+public class ThreadFactoryBuilder implements ThreadFactory {
+
+ private String name;
+ private int counter;
+
+ public ThreadFactoryBuilder(String name) {
+ this.name = name;
+ counter = 1;
+ }
+
+ @Override
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable, name);
+ thread.setName("ThreadFactoryBuilder_" + name + "_" + counter);
+ return thread;
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/ThreadPoolManager.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/ThreadPoolManager.java
new file mode 100755
index 0000000..0f61795
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/ThreadPoolManager.java
@@ -0,0 +1,92 @@
+package com.printer.tscdemo;
+
+import android.util.Log;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Created by Circle on 2019/9/17.
+ * 线程池管理类
+ */
+
+public class ThreadPoolManager {
+ String TAG=ThreadPoolManager.class.getSimpleName();
+ private static ThreadPoolManager threadPoolManager=null;
+
+ public static ThreadPoolManager getInstance(){
+ if (threadPoolManager == null) {
+ threadPoolManager =new ThreadPoolManager();
+ }
+ return threadPoolManager;
+ }
+ //线程安全
+ private LinkedBlockingDeque mQueue=new LinkedBlockingDeque<>();
+
+ private ThreadPoolExecutor mThreadPoolExecutor;
+ //创建线程池
+ private ThreadPoolManager(){
+ mThreadPoolExecutor=new ThreadPoolExecutor(1, 100, 15, TimeUnit.SECONDS, new ArrayBlockingQueue(100), new RejectedExecutionHandler() {
+ @Override
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+ addTask(r);
+ }
+ });
+ new Thread(coreTread).start();
+// mThreadPoolExecutor.execute(coreTread);//执行核心线程池
+ }
+
+
+ //将请求添加到队列中
+ public void addTask(Runnable runnable){
+ Log.e(TAG,runnable.getClass().getSimpleName());
+ if (runnable!=null){
+ try {
+ mQueue.put(runnable);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ public void addTopTask(Runnable runnable){
+ if (runnable!=null){
+ try {
+ mQueue.putFirst(runnable);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ //创建核心线程
+ public Runnable coreTread=new Runnable() {
+ Runnable runn=null;
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ runn = mQueue.take();
+ synchronized(this){
+ mThreadPoolExecutor.execute(runn);//执行线程
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ };
+
+ /**
+ * 结束线程池
+ */
+ public void stopThreadPool() {
+ if (mThreadPoolExecutor != null) {
+ mThreadPoolExecutor.shutdown();
+ mQueue.clear();
+ mThreadPoolExecutor = null;
+ threadPoolManager = null;
+ }
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/UsbDeviceActivity.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/UsbDeviceActivity.java
new file mode 100755
index 0000000..b5bb629
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/UsbDeviceActivity.java
@@ -0,0 +1,124 @@
+package com.printer.tscdemo;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ * Copyright (C), 2012-2020, 打印机有限公司
+ * FileName: UsbDeviceListActivity
+ * Author: Circle
+ * Date: 2020/7/18 16:17
+ * Description: 获取USB设备列表
+ */
+public class UsbDeviceActivity extends AppCompatActivity {
+
+ private static final String TAG = UsbDeviceActivity.class.getSimpleName();
+ /**
+ * Member fields
+ */
+ private ListView lvDevices = null;
+ private ArrayAdapter adapter;
+ public static final String USB_NAME = "usb_name";
+ UsbManager manager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_usb);
+ setTitle(getString(R.string.usb_label));
+ initView();
+ getUsbDeviceList();
+ }
+ /**
+ * 初始化视图、控件
+ */
+ private void initView() {
+ lvDevices = (ListView) findViewById(R.id.lv_usb);
+ adapter = new ArrayAdapter(this,R.layout.text_item);
+ lvDevices.setOnItemClickListener(mDeviceClickListener);
+ lvDevices.setAdapter(adapter);
+ }
+
+ /**
+ * 检查USB设备的PID与VID
+ * @param dev
+ * @return
+ */
+ boolean checkUsbDevicePidVid(UsbDevice dev) {
+ int pid = dev.getProductId();
+ int vid = dev.getVendorId();
+ return ((vid == 34918 && pid == 256) || (vid == 1137 && pid == 85)
+ || (vid == 6790 && pid == 30084)
+ || (vid == 26728 && pid == 256) || (vid == 26728 && pid == 512)
+ || (vid == 26728 && pid == 256) || (vid == 26728 && pid == 768)
+ || (vid == 26728 && pid == 1024) || (vid == 26728 && pid == 1280)
+ || (vid == 26728 && pid == 1536));
+ }
+
+ /**
+ * 获取USB设备列表
+ */
+ public void getUsbDeviceList() {
+ manager = (UsbManager) getSystemService(Context.USB_SERVICE);
+ // Get the list of attached devices
+ HashMap devices = manager.getDeviceList();
+ Iterator deviceIterator = devices.values().iterator();
+ int count = devices.size();
+ Log.d(TAG, "count " + count);
+ if (count > 0) {
+ while (deviceIterator.hasNext()) {
+ UsbDevice device = deviceIterator.next();
+ String devicename = device.getDeviceName();
+ if (checkUsbDevicePidVid(device)) {
+ adapter.add(devicename);
+ }
+ }
+ } else {
+ String noDevices = getResources().getText(R.string.none_usb_device).toString();
+ Log.d(TAG, "noDevices " + noDevices);
+ adapter.add(noDevices);
+ }
+ }
+
+ private AdapterView.OnItemClickListener mDeviceClickListener = new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> av, View v, int arg2, long arg3) {
+ // Cancel discovery because it's costly and we're about to connect
+ // Get the device MAC address, which is the last 17 chars in the View
+ String info = ((TextView) v).getText().toString();
+ String noDevices = getResources().getText(R.string.none_usb_device).toString();
+ if (!info.equals(noDevices)) {
+ String address = info;
+ // Create the result Intent and include the MAC address
+ Intent intent = new Intent();
+ intent.putExtra(USB_NAME, address);
+ // Set result and finish this Activity
+ setResult(Activity.RESULT_OK, intent);
+ }
+ finish();
+ }
+ };
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (manager!=null){
+ manager=null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/Utils.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/Utils.java
new file mode 100755
index 0000000..a151f9f
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/Utils.java
@@ -0,0 +1,146 @@
+package com.printer.tscdemo;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.widget.Toast;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 作者: Circle
+ * 创造于 2018/5/24.
+ */
+public class Utils {
+ /**
+ * 通过USB名称获取设备
+ * @param context
+ * @param usbName
+ * @return
+ */
+ public static UsbDevice getUsbDeviceFromName(Context context, String usbName) {
+ UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
+ HashMap usbDeviceList = usbManager.getDeviceList();
+ return usbDeviceList.get(usbName);
+ }
+ /**
+ * 短时间吐司
+ * @param context
+ * @param msg
+ */
+ public static void shortToast(Context context,String msg){
+ Toast.makeText(context,msg,Toast.LENGTH_SHORT).show();
+ }
+ /**
+ * 合拼两个数组
+ * @param first
+ * @param second
+ * @param
+ * @return
+ */
+ public static byte[] concat(byte[] first, byte[] second) {
+ byte[] result = Arrays.copyOf(first, first.length + second.length);
+ System.arraycopy(second, 0, result, first.length, second.length);
+ return result;
+ }
+ /**
+ * 检测WiFi的IP是否输入正确
+ * @param addr
+ * @return
+ */
+ public static boolean checkIP(String addr)
+
+ {
+ if(addr.length() < 7 || addr.length() > 15 || "".equals(addr))
+ {
+ return false;
+ }
+ /**
+ * 判断IP格式和范围
+ */
+ String rexp = "([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}";
+ Pattern pat = Pattern.compile(rexp);
+ Matcher mat = pat.matcher(addr);
+ boolean ipAddress = mat.find();
+ //============对之前的ip判断的bug在进行判断
+ if (ipAddress==true){
+ String ips[] = addr.split("\\.");
+ if(ips.length==4){
+ try{
+ for(String ip : ips){
+ if(Integer.parseInt(ip)<0||Integer.parseInt(ip)>255){
+ return false;
+ }
+ }
+ }catch (Exception e){
+ return false;
+ }
+ return true;
+ }else{
+ return false;
+ }
+ }
+ return ipAddress;
+ }
+ /**
+ * 获取指定包名的版本号
+ *
+ * @param context
+ * 本应用程序上下文
+ * @return
+ * @throws Exception
+ */
+ public static String getVersionName(Context context) {
+ // 获取packagemanager的实例
+ PackageManager packageManager = context.getPackageManager();
+ try {
+ PackageInfo packInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
+ String version = packInfo.versionName;
+ return version;
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ return "";
+
+ }
+ public static byte [] getAssetsFile(Context context,String fileName) {
+ InputStream in = null;
+ ByteArrayOutputStream outStream = null;
+ byte [] data=null;
+ try {
+ in = context.getResources().getAssets().open(fileName);
+ outStream = new ByteArrayOutputStream();
+ //创建byte数组
+ byte[] buffer = new byte[1024*1024];
+ //将文件中的数据读到byte数组中
+ int len = 0;
+ while ((len = in.read(buffer)) != -1) {
+ outStream.write(buffer, 0, len);
+ }
+ data = outStream.toByteArray();
+ return data;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }finally {
+ try {
+ if (in!=null){
+ in.close();
+ }
+ if (outStream!=null){
+ outStream.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return data;
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/WifiDeviceActivity.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/WifiDeviceActivity.java
new file mode 100755
index 0000000..6f8be74
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/WifiDeviceActivity.java
@@ -0,0 +1,68 @@
+package com.printer.tscdemo;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+
+/**
+ * Copyright (C), 2012-2019, 打印机有限公司
+ * FileName: Printer
+ * Author: Circle
+ * Date: 2019/12/25 19:46
+ * Description: WIFI打印机设备
+ */
+public class WifiDeviceActivity extends Activity {
+ public static String IP="IP";
+ public Context context;
+ public EditText edWifi;
+ public Button btnOK;
+ SharedPreferencesUtil sharedPreferencesUtil;
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_wifi);
+ context=WifiDeviceActivity.this;
+ sharedPreferencesUtil= SharedPreferencesUtil.getInstantiation(context);
+ initView();
+ }
+
+ private void initView() {
+ //串口路径初始化
+ edWifi = (EditText)findViewById(R.id.et_wifi_ip);
+ btnOK=(Button)findViewById(R.id.btn_confirm) ;
+ String ip = sharedPreferencesUtil.getString("192.168.123.100", IP);
+ edWifi.setText(ip);
+ initListener();
+ }
+
+ private void initListener() {
+ btnOK.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String ip=edWifi.getText().toString().trim();
+ if (TextUtils.isEmpty(ip)&&!Utils.checkIP(ip)){//ip不合法
+ Utils.shortToast(context,context.getString(R.string.ip_is_illegal));
+ return;
+ }
+ sharedPreferencesUtil.putString(ip,IP);
+ Intent intent = new Intent();
+ Bundle bundle = new Bundle();
+ bundle.putString(IP, ip);
+ intent.putExtras(bundle);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/bean/BluetoothParameter.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/bean/BluetoothParameter.java
new file mode 100755
index 0000000..986e2fc
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/java/com/printer/tscdemo/bean/BluetoothParameter.java
@@ -0,0 +1,30 @@
+package com.printer.tscdemo.bean;
+
+public class BluetoothParameter {
+ private String bluetoothName;
+ private String bluetoothMac;
+ private String bluetoothStrength;/*蓝牙强度*/
+ public String getBluetoothName() {
+ return bluetoothName;
+ }
+
+ public void setBluetoothName(String bluetoothName) {
+ this.bluetoothName = bluetoothName;
+ }
+
+ public String getBluetoothMac() {
+ return bluetoothMac;
+ }
+
+ public void setBluetoothMac(String bluetoothMac) {
+ this.bluetoothMac = bluetoothMac;
+ }
+
+ public String getBluetoothStrength() {
+ return bluetoothStrength;
+ }
+
+ public void setBluetoothStrength(String bluetoothStrength) {
+ this.bluetoothStrength = bluetoothStrength;
+ }
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/jniLibs/arm64-v8a/libserial_port.so b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/jniLibs/arm64-v8a/libserial_port.so
new file mode 100755
index 0000000..e8a1f48
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/jniLibs/arm64-v8a/libserial_port.so differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/jniLibs/armeabi-v7a/libserial_port.so b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/jniLibs/armeabi-v7a/libserial_port.so
new file mode 100755
index 0000000..78df841
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/jniLibs/armeabi-v7a/libserial_port.so differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/jniLibs/armeabi/libserial_port.so b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/jniLibs/armeabi/libserial_port.so
new file mode 100755
index 0000000..3b03715
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/jniLibs/armeabi/libserial_port.so differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100755
index 0000000..1f6bb29
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/flower.jpg b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/flower.jpg
new file mode 100755
index 0000000..cf1a061
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/flower.jpg differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/head.jpg b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/head.jpg
new file mode 100755
index 0000000..ab0d26c
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/head.jpg differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/ic_launcher_background.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100755
index 0000000..0d025f9
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/test.bmp b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/test.bmp
new file mode 100755
index 0000000..ad920bc
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/drawable/test.bmp differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_blue_tooth_device_list.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_blue_tooth_device_list.xml
new file mode 100755
index 0000000..6c51a59
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_blue_tooth_device_list.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_bluetooth.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_bluetooth.xml
new file mode 100755
index 0000000..d86ef45
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_bluetooth.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_main.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_main.xml
new file mode 100755
index 0000000..e058d5b
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_serial_port.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_serial_port.xml
new file mode 100755
index 0000000..dde99b8
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_serial_port.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_usb.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_usb.xml
new file mode 100755
index 0000000..4394aee
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_usb.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_usb_list.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_usb_list.xml
new file mode 100755
index 0000000..4394aee
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_usb_list.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_wifi.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_wifi.xml
new file mode 100755
index 0000000..4582838
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/activity_wifi.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/bluetooth_list_item.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/bluetooth_list_item.xml
new file mode 100755
index 0000000..3590959
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/bluetooth_list_item.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/dialog_wifi_config.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/dialog_wifi_config.xml
new file mode 100755
index 0000000..9e93b6e
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/dialog_wifi_config.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/page.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/page.xml
new file mode 100755
index 0000000..f89c55c
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/page.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/text_item.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/text_item.xml
new file mode 100755
index 0000000..f95eb30
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/layout/text_item.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100755
index 0000000..eca70cf
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100755
index 0000000..eca70cf
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-hdpi/ic_launcher.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100755
index 0000000..898f3ed
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100755
index 0000000..dffca36
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-mdpi/ic_launcher.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100755
index 0000000..64ba76f
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100755
index 0000000..dae5e08
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755
index 0000000..e5ed465
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100755
index 0000000..14ed0af
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xhdpi/ic_priter.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xhdpi/ic_priter.png
new file mode 100755
index 0000000..2c289ab
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xhdpi/ic_priter.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755
index 0000000..b0907ca
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100755
index 0000000..d8ae031
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755
index 0000000..2c18de9
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100755
index 0000000..beed3cd
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values-zh-rCN/strings.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values-zh-rCN/strings.xml
new file mode 100755
index 0000000..e7afc8f
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,74 @@
+
+ 标签Demo
+ 蓝牙设备
+ USB设备
+ 以太网设备
+ 串口设备
+ 扫描连接
+ 断开连接
+ 打印前查询状态
+ 打印案例(58mm*70mm)
+ 打印PDF
+ 打印菜单
+ 打印XML
+ 查询状态
+ 无USB设备
+ 已配对
+ 未配对
+ 蓝牙未开启
+ 搜索中...
+ 搜索完成
+ 搜索设备
+ 权限
+ 无定位权限
+ 获取PDF失败
+ 无读取文件权限
+ 扫描需要摄像头权限
+ 安卓8.0系统搜索蓝牙需开启GPS定位功能!\n请选择是否开启
+ 获取权限失败,无法使用扫描功能
+ 提示
+ 确定
+ 取消
+ 状态获取异常
+ 打印机状态获取失败,请检查打印机是否缺纸或开盖
+ 状态正常
+ 状态走纸、打印
+ 状态缺纸
+ 状态开盖
+ 状态过热
+ 请先连接打印机
+ 未连接
+ 连接失败
+ 连接成功
+ 连接中...
+ 查询中...
+ 已连接
+ 打印成功
+ 打印失败
+ 发送成功
+ 发送失败
+ 波特率
+ 串口地址
+
+ - 9600
+ - 19200
+ - 38400
+ - 115200
+
+ 无串口设备
+ IP不合法
+ 订单
+ 名称
+ 数量
+ 价格
+ 总金额:
+ 收银员:
+ 标签间隙:
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+
+
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values/colors.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values/colors.xml
new file mode 100755
index 0000000..3c62d61
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #54bec2
+ #54bec2
+ #54bec2
+
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values/strings.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values/strings.xml
new file mode 100755
index 0000000..75adcf0
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values/strings.xml
@@ -0,0 +1,74 @@
+
+ TscDemo
+ BlueTooth device
+ USB device
+ Ethernet device
+ Serial device
+ Scan connection
+ Disconnect
+ Check status before printing
+ Print case(58mm*70mm)
+ Print PDF
+ Print Menu
+ Print XML
+ Check status
+ No USB device
+ Paired
+ Unpaired
+ Bluetooth is not turned on
+ searching...
+ Search device
+ Search complete
+ Authority
+ No positioning permission
+ Failed to get PDF
+ No read file permission
+ Scanning requires camera permission
+ Android 8.0 system needs to turn on GPS positioning function to search for Bluetooth!\nPlease choose whether to enable
+ Failed to obtain permission, unable to use scanning function
+ Tip
+ OK
+ Cancel
+ Status acquisition exception
+ Failed to obtain the printer status, please check whether the printer is out of paper or open the cover
+ Normal state
+ Status feed、printing
+ Out of paper status
+ Status open
+ State overheated
+ Please connect the printer first
+ not connected
+ Connection failed
+ connection succeeded
+ connecting...
+ Querying...
+ connected
+ Print successfully
+ Print failed
+ Send successfully
+ Send failed
+ BaudRate
+ Serial port address
+
+ - 9600
+ - 19200
+ - 38400
+ - 115200
+
+ No serial device
+ ip is illegal
+ Order
+ Name
+ Quantity
+ Price
+ Total amount:
+ Cashier:
+ label gap:
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+
+
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values/styles.xml b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values/styles.xml
new file mode 100755
index 0000000..5885930
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/test/java/com/printer/tscdemo/ExampleUnitTest.java b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/test/java/com/printer/tscdemo/ExampleUnitTest.java
new file mode 100755
index 0000000..303c607
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/src/test/java/com/printer/tscdemo/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.printer.tscdemo;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/build.gradle b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/build.gradle
new file mode 100755
index 0000000..4f5501c
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/build.gradle
@@ -0,0 +1,63 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ mavenLocal()
+ mavenCentral()
+ //佳博SDK仓库
+ maven {
+ url "http://118.31.6.84:8081/repository/maven-public/"
+ allowInsecureProtocol true
+ }
+ maven {
+ url 'https://maven.aliyun.com/nexus/content/groups/public/'
+ }
+ maven {
+ url 'https://maven.aliyun.com/nexus/content/repositories/jcenter'
+ }
+ maven {
+ url 'https://maven.aliyun.com/nexus/content/repositories/google'
+ }
+ maven {
+ url 'https://maven.aliyun.com/nexus/content/repositories/gradle-plugin'
+ }
+
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.2.1'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ mavenLocal()
+ mavenCentral()
+// //佳博SDK仓库
+ maven {
+ url "http://118.31.6.84:8081/repository/maven-public/"
+ allowInsecureProtocol true
+ }
+ maven {
+ url 'https://maven.aliyun.com/nexus/content/groups/public/'
+ }
+ maven {
+ url 'https://maven.aliyun.com/nexus/content/repositories/jcenter'
+ }
+ maven {
+ url 'https://maven.aliyun.com/nexus/content/repositories/google'
+ }
+ maven {
+ url 'https://maven.aliyun.com/nexus/content/repositories/gradle-plugin'
+ }
+
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradle.properties b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradle.properties
new file mode 100755
index 0000000..e6af540
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradle.properties
@@ -0,0 +1,20 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+android.injected.testOnly=false
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradle/wrapper/gradle-wrapper.jar b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradle/wrapper/gradle-wrapper.jar
new file mode 100755
index 0000000..f6b961f
Binary files /dev/null and b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradle/wrapper/gradle-wrapper.properties b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradle/wrapper/gradle-wrapper.properties
new file mode 100755
index 0000000..01aacb6
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Jul 18 15:36:20 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradlew b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradlew.bat b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradlew.bat
new file mode 100755
index 0000000..f955316
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/settings.gradle b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/settings.gradle
new file mode 100755
index 0000000..10178ee
--- /dev/null
+++ b/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name='TscDemo'
diff --git a/打印机安卓基座/README.md b/打印机安卓基座/README.md
new file mode 100644
index 0000000..3293924
--- /dev/null
+++ b/打印机安卓基座/README.md
@@ -0,0 +1,16 @@
+# 打印机安卓基座
+
+这里只保留当前生效的安卓原生标签打印基座源码:
+
+- `native-fast-printer/`
+
+说明:
+
+- 这里是 `native-fast-printer` 的**唯一源码入口**
+- `美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/` 只是给 uni-app 打包用的镜像目录
+- 修改原生代码后,先执行:
+ - `native-fast-printer/android-src/build-aar.sh`
+- 再执行:
+ - `native-fast-printer/sync-to-uniapp.sh`
+
+当前目录只放这套原生打印基座代码,不再混放其他 SDK、参考项目或历史实验代码。
diff --git a/打印机安卓基座/native-fast-printer/README.md b/打印机安卓基座/native-fast-printer/README.md
new file mode 100644
index 0000000..77c40a8
--- /dev/null
+++ b/打印机安卓基座/native-fast-printer/README.md
@@ -0,0 +1,40 @@
+# native-fast-printer
+
+传统 `nativeplugins` Android 原生插件版高速标签打印模块。
+
+## 能力
+- 经典蓝牙连接 / 断开 / 状态
+- 接收系统模板 JSON
+- 原生生成 TSC 指令
+- 文本、价格、条码、二维码、横线、图片
+- 特殊字符文本和图片走原生位图补丁
+
+## 前端调用
+```js
+const printer = uni.requireNativePlugin('native-fast-printer')
+```
+
+## 方法
+- `connect(params, callback)`
+- `disconnect(callback)`
+- `isConnected(callback)`
+- `printTemplate(params, callback)`
+
+## 源码位置
+- 当前目录是源码主目录
+- `美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/` 是同步后的 uni-app 打包镜像
+
+## 目录结构
+- `android-src/src/com/foodlabel/nativeprinter/`
+ - `NativeFastPrinterModule.java`:uni-app 原生模块入口
+ - `transport/`:蓝牙连接与 SDK 传输层
+ - `template/`:系统模板 JSON → TSC 指令
+ - `debug/`:调试状态与统计信息
+ - `support/`:结果对象、JSON 读取、异常展开
+- `android/`:编译产物 AAR
+- `sync-to-uniapp.sh`:同步到 uni-app 打包镜像
+
+## 说明
+1. 修改源码后执行 `android-src/build-aar.sh`
+2. 再执行 `sync-to-uniapp.sh`
+3. 重新打包 uni-app 自定义基座
diff --git a/打印机安卓基座/native-fast-printer/android-src/build-aar.sh b/打印机安卓基座/native-fast-printer/android-src/build-aar.sh
new file mode 100755
index 0000000..f71bfc0
--- /dev/null
+++ b/打印机安卓基座/native-fast-printer/android-src/build-aar.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_ROOT="$(cd "$ROOT_DIR/../../.." && pwd)"
+OUT_DIR="$(cd "$ROOT_DIR/.." && pwd)/android"
+SRC_DIR="$ROOT_DIR/src"
+BUILD_DIR="$ROOT_DIR/build"
+STUB_DIR="$BUILD_DIR/stubs"
+CLASS_DIR="$BUILD_DIR/classes"
+STUB_CLASS_DIR="$BUILD_DIR/stub-classes"
+ANDROID_JAR="${ANDROID_JAR:-$HOME/Library/Android/sdk/platforms/android-34/android.jar}"
+SDK_LIB_JAR="${SDK_LIB_JAR:-$PROJECT_ROOT/打印机SDK/Android/标签打印机安卓SDK-V3.3.1-20230327/TscDemo/app/libs/SDKLib.jar}"
+OUT_AAR="$OUT_DIR/native_fast_printer-release.aar"
+
+rm -rf "$BUILD_DIR"
+mkdir -p "$STUB_DIR/com/taobao/weex/annotation" \
+ "$STUB_DIR/com/taobao/weex/bridge" \
+ "$STUB_DIR/io/dcloud/feature/uniapp/common" \
+ "$STUB_DIR/com/alibaba/fastjson" \
+ "$CLASS_DIR" "$STUB_CLASS_DIR" "$OUT_DIR" "$BUILD_DIR/libs"
+
+cat > "$STUB_DIR/com/taobao/weex/annotation/JSMethod.java" <<'STUB'
+package com.taobao.weex.annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface JSMethod { boolean uiThread() default true; }
+STUB
+
+cat > "$STUB_DIR/com/taobao/weex/bridge/JSCallback.java" <<'STUB'
+package com.taobao.weex.bridge;
+public interface JSCallback {
+ void invoke(Object value);
+ void invokeAndKeepAlive(Object value);
+}
+STUB
+
+cat > "$STUB_DIR/io/dcloud/feature/uniapp/common/UniModule.java" <<'STUB'
+package io.dcloud.feature.uniapp.common;
+public class UniModule {}
+STUB
+
+cat > "$STUB_DIR/com/alibaba/fastjson/JSONObject.java" <<'STUB'
+package com.alibaba.fastjson;
+import java.util.HashMap;
+public class JSONObject extends HashMap {
+ public String getString(String key){ Object v = get(key); return v == null ? null : String.valueOf(v); }
+ public Integer getInteger(String key){ Object v = get(key); if (v == null) return null; if (v instanceof Number) return ((Number)v).intValue(); return Integer.parseInt(String.valueOf(v)); }
+}
+STUB
+
+find "$STUB_DIR" -name '*.java' -print0 | xargs -0 javac -source 1.8 -target 1.8 -encoding UTF-8 -cp "$ANDROID_JAR" -d "$STUB_CLASS_DIR"
+jar cf "$BUILD_DIR/stubs.jar" -C "$STUB_CLASS_DIR" .
+
+find "$SRC_DIR" -name '*.java' -print0 | xargs -0 javac -source 1.8 -target 1.8 -encoding UTF-8 -cp "$ANDROID_JAR:$BUILD_DIR/stubs.jar:$SDK_LIB_JAR" -d "$CLASS_DIR"
+jar cf "$BUILD_DIR/classes.jar" -C "$CLASS_DIR" .
+cp "$SDK_LIB_JAR" "$BUILD_DIR/libs/SDKLib.jar"
+
+cat > "$BUILD_DIR/AndroidManifest.xml" <<'MANIFEST'
+
+MANIFEST
+: > "$BUILD_DIR/R.txt"
+mkdir -p "$BUILD_DIR/META-INF/com/android/build/gradle"
+cat > "$BUILD_DIR/META-INF/com/android/build/gradle/aar-metadata.properties" <<'META'
+aarFormatVersion=1.0
+aarMetadataVersion=1.0
+META
+
+cd "$BUILD_DIR"
+rm -f "$OUT_AAR"
+zip -q -r "$OUT_AAR" AndroidManifest.xml classes.jar R.txt META-INF libs
+
+echo "Built AAR: $OUT_AAR"
diff --git a/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/NativeFastPrinterModule.java b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/NativeFastPrinterModule.java
new file mode 100644
index 0000000..692dd2f
--- /dev/null
+++ b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/NativeFastPrinterModule.java
@@ -0,0 +1,153 @@
+package com.foodlabel.nativeprinter;
+
+import com.alibaba.fastjson.JSONObject;
+import com.foodlabel.nativeprinter.debug.NativePrintDebugState;
+import com.foodlabel.nativeprinter.support.PluginResult;
+import com.foodlabel.nativeprinter.support.SafeJson;
+import com.foodlabel.nativeprinter.support.ThrowableUtils;
+import com.foodlabel.nativeprinter.template.NativeTemplateCommandBuilder;
+import com.foodlabel.nativeprinter.transport.GprinterBluetoothTransport;
+import com.taobao.weex.annotation.JSMethod;
+import com.taobao.weex.bridge.JSCallback;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import io.dcloud.feature.uniapp.common.UniModule;
+
+public class NativeFastPrinterModule extends UniModule {
+ private static final String BACKEND = "gprinter-sdk";
+ private static final String PLUGIN_VERSION = "1.1.0";
+ private static final Object LOCK = new Object();
+ private static final ExecutorService PRINT_EXECUTOR = Executors.newSingleThreadExecutor();
+ private static final NativePrintDebugState DEBUG_STATE = new NativePrintDebugState(BACKEND, PLUGIN_VERSION);
+ private static final GprinterBluetoothTransport BLUETOOTH_TRANSPORT = new GprinterBluetoothTransport();
+
+ @JSMethod(uiThread = false)
+ public void connect(JSONObject params, JSCallback callback) {
+ String deviceId = SafeJson.getString(params, "deviceId", "");
+ String deviceName = SafeJson.getString(params, "deviceName", "");
+ PluginResult result = ensureConnected(deviceId, deviceName);
+ if (callback != null) {
+ callback.invoke(result.toJsonString());
+ }
+ }
+
+ @JSMethod(uiThread = false)
+ public void disconnect(JSCallback callback) {
+ synchronized (LOCK) {
+ BLUETOOTH_TRANSPORT.disconnect();
+ DEBUG_STATE.clearCurrentDevice();
+ DEBUG_STATE.setStage("disconnect:ok");
+ DEBUG_STATE.clearError();
+ }
+ if (callback != null) {
+ callback.invoke(debugResult(PluginResult.ok(false, "", "", "disconnect:ok")).toJsonString());
+ }
+ }
+
+ @JSMethod(uiThread = false)
+ public void isConnected(JSCallback callback) {
+ boolean connected;
+ synchronized (LOCK) {
+ connected = BLUETOOTH_TRANSPORT.isConnected();
+ }
+ if (callback != null) {
+ callback.invoke(debugResult(PluginResult.ok(connected, DEBUG_STATE.getCurrentDeviceId(), DEBUG_STATE.getCurrentDeviceName(), "isConnected:ok")).toJsonString());
+ }
+ }
+
+ @JSMethod(uiThread = false)
+ public void getDebugInfo(JSCallback callback) {
+ boolean connected;
+ synchronized (LOCK) {
+ connected = BLUETOOTH_TRANSPORT.isConnected();
+ }
+ if (callback != null) {
+ callback.invoke(debugResult(PluginResult.ok(connected, DEBUG_STATE.getCurrentDeviceId(), DEBUG_STATE.getCurrentDeviceName(), "debug:ok")).toJsonString());
+ }
+ }
+
+ @JSMethod(uiThread = false)
+ public void printTemplate(JSONObject params, JSCallback callback) {
+ String deviceId = SafeJson.getString(params, "deviceId", "");
+ String deviceName = SafeJson.getString(params, "deviceName", "");
+ String templateJson = SafeJson.getString(params, "templateJson", "");
+ String dataJson = SafeJson.getString(params, "dataJson", "{}");
+ int dpi = SafeJson.getInt(params, "dpi", 203);
+ int printQty = Math.max(1, SafeJson.getInt(params, "printQty", 1));
+
+ if (templateJson == null || templateJson.trim().isEmpty()) {
+ if (callback != null) {
+ callback.invoke(errorResult(9011006, "Template json is empty.").toJsonString());
+ }
+ return;
+ }
+
+ PluginResult connectResult = ensureConnected(deviceId, deviceName);
+ if (!connectResult.success) {
+ if (callback != null) callback.invoke(connectResult.toJsonString());
+ return;
+ }
+
+ DEBUG_STATE.setStage("printTemplate:queued");
+ if (callback != null) {
+ callback.invoke(debugResult(PluginResult.ok(true, DEBUG_STATE.getCurrentDeviceId(), DEBUG_STATE.getCurrentDeviceName(), "printTemplate:queued")).toJsonString());
+ }
+
+ PRINT_EXECUTOR.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ DEBUG_STATE.resetBuildMetrics();
+ DEBUG_STATE.setStage("build-command");
+ long buildStarted = System.currentTimeMillis();
+ NativeTemplateCommandBuilder.BuildResult buildResult =
+ NativeTemplateCommandBuilder.buildWithStats(templateJson, dataJson, dpi, printQty);
+ DEBUG_STATE.setBuildMs(Math.max(0L, System.currentTimeMillis() - buildStarted));
+ DEBUG_STATE.setBuildResult(buildResult);
+
+ long writeStarted = System.currentTimeMillis();
+ synchronized (LOCK) {
+ if (!BLUETOOTH_TRANSPORT.isConnected()) {
+ errorResult(9011005, "Bluetooth printer transport is not ready.");
+ return;
+ }
+ DEBUG_STATE.setStage("write-command");
+ boolean ok = BLUETOOTH_TRANSPORT.write(buildResult.bytes);
+ if (!ok) {
+ errorResult(9011011, "Printer writeDataImmediately returned false.");
+ return;
+ }
+ }
+ DEBUG_STATE.setWriteMs(Math.max(0L, System.currentTimeMillis() - writeStarted));
+ DEBUG_STATE.markPrinted(System.currentTimeMillis());
+ DEBUG_STATE.setStage("printTemplate:ok");
+ DEBUG_STATE.clearError();
+ } catch (Throwable e) {
+ DEBUG_STATE.setError(ThrowableUtils.unwrap(e));
+ DEBUG_STATE.setStage("printTemplate:error");
+ }
+ }
+ });
+ }
+
+ private PluginResult ensureConnected(String deviceId, String deviceName) {
+ synchronized (LOCK) {
+ PluginResult result = BLUETOOTH_TRANSPORT.ensureConnected(deviceId, deviceName, DEBUG_STATE);
+ if (!result.success) {
+ return debugResult(result);
+ }
+ return debugResult(PluginResult.ok(true, DEBUG_STATE.getCurrentDeviceId(), DEBUG_STATE.getCurrentDeviceName(), "connect:ok"));
+ }
+ }
+
+ private static PluginResult errorResult(int code, String message) {
+ DEBUG_STATE.setError(message == null ? "" : message);
+ return debugResult(PluginResult.error(code, message));
+ }
+
+ private static PluginResult debugResult(PluginResult result) {
+ return DEBUG_STATE.attachTo(result);
+ }
+}
diff --git a/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/support/PluginResult.java b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/support/PluginResult.java
new file mode 100644
index 0000000..6ef0e92
--- /dev/null
+++ b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/support/PluginResult.java
@@ -0,0 +1,60 @@
+package com.foodlabel.nativeprinter.support;
+
+import org.json.JSONObject;
+
+public final class PluginResult {
+ public final boolean success;
+ public final int code;
+ public final String message;
+ public final boolean connected;
+ public final String deviceId;
+ public final String deviceName;
+ public final JSONObject extra;
+
+ private PluginResult(boolean success, int code, String message, boolean connected, String deviceId, String deviceName) {
+ this.success = success;
+ this.code = code;
+ this.message = message == null ? "" : message;
+ this.connected = connected;
+ this.deviceId = deviceId == null ? "" : deviceId;
+ this.deviceName = deviceName == null ? "" : deviceName;
+ this.extra = new JSONObject();
+ }
+
+ public static PluginResult ok(boolean connected, String deviceId, String deviceName, String message) {
+ return new PluginResult(true, 1, message, connected, deviceId, deviceName);
+ }
+
+ public static PluginResult error(int code, String message) {
+ return new PluginResult(false, code, message, false, "", "");
+ }
+
+ public PluginResult withMeta(String key, Object value) {
+ if (key == null || key.isEmpty() || value == null) return this;
+ try {
+ extra.put(key, value);
+ } catch (Exception ignored) {
+ }
+ return this;
+ }
+
+ public String toJsonString() {
+ try {
+ JSONObject json = new JSONObject();
+ json.put("code", success ? 1 : code);
+ json.put("msg", message);
+ json.put("connected", connected);
+ json.put("deviceId", deviceId);
+ json.put("deviceName", deviceName);
+ json.put("success", success);
+ java.util.Iterator keys = extra.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ json.put(key, extra.opt(key));
+ }
+ return json.toString();
+ } catch (Exception e) {
+ return "{\"code\":0,\"msg\":\"plugin_result_error\"}";
+ }
+ }
+}
diff --git a/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/support/SafeJson.java b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/support/SafeJson.java
new file mode 100644
index 0000000..4c7b71a
--- /dev/null
+++ b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/support/SafeJson.java
@@ -0,0 +1,33 @@
+package com.foodlabel.nativeprinter.support;
+
+import com.alibaba.fastjson.JSONObject;
+
+public final class SafeJson {
+ private SafeJson() {}
+
+ public static String getString(JSONObject json, String key, String fallback) {
+ if (json == null || key == null) return fallback;
+ try {
+ String value = json.getString(key);
+ return value == null ? fallback : value;
+ } catch (Exception e) {
+ Object value = json.get(key);
+ return value == null ? fallback : String.valueOf(value);
+ }
+ }
+
+ public static int getInt(JSONObject json, String key, int fallback) {
+ if (json == null || key == null) return fallback;
+ try {
+ Integer value = json.getInteger(key);
+ return value == null ? fallback : value;
+ } catch (Exception e) {
+ try {
+ Object value = json.get(key);
+ return value == null ? fallback : Integer.parseInt(String.valueOf(value));
+ } catch (Exception ignored) {
+ return fallback;
+ }
+ }
+ }
+}
diff --git a/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/support/ThrowableUtils.java b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/support/ThrowableUtils.java
new file mode 100644
index 0000000..b6990bd
--- /dev/null
+++ b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/support/ThrowableUtils.java
@@ -0,0 +1,24 @@
+package com.foodlabel.nativeprinter.support;
+
+public final class ThrowableUtils {
+ private ThrowableUtils() {
+ }
+
+ public static String unwrap(Throwable error) {
+ if (error == null) return "unknown_error";
+ StringBuilder builder = new StringBuilder();
+ Throwable current = error;
+ int depth = 0;
+ while (current != null && depth < 6) {
+ if (builder.length() > 0) builder.append(" | caused by: ");
+ builder.append(current.getClass().getName());
+ String message = current.getMessage();
+ if (message != null && !message.trim().isEmpty()) {
+ builder.append(": ").append(message);
+ }
+ current = current.getCause();
+ depth++;
+ }
+ return builder.toString();
+ }
+}
diff --git a/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/template/NativeTemplateCommandBuilder.java b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/template/NativeTemplateCommandBuilder.java
new file mode 100644
index 0000000..78b41d9
--- /dev/null
+++ b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/template/NativeTemplateCommandBuilder.java
@@ -0,0 +1,690 @@
+package com.foodlabel.nativeprinter.template;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.util.Base64;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class NativeTemplateCommandBuilder {
+ private static final double DESIGN_DPI = 96.0;
+ private static final int TEXT_PADDING_DOTS = 6;
+ private static final int DEFAULT_THRESHOLD = 180;
+ private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{\\{\\s*([\\w.-]+)\\s*\\}\\}");
+
+ private NativeTemplateCommandBuilder() {
+ }
+
+ public static byte[] build(String templateJson, String dataJson, int dpi, int printQty) throws Exception {
+ JSONObject template = new JSONObject(templateJson);
+ JSONObject data = (dataJson == null || dataJson.trim().isEmpty()) ? new JSONObject() : new JSONObject(dataJson);
+ return buildWithStats(template, data, dpi, printQty).bytes;
+ }
+
+ public static byte[] build(JSONObject template, JSONObject data, int dpi, int printQty) throws Exception {
+ return buildWithStats(template, data, dpi, printQty).bytes;
+ }
+
+ public static BuildResult buildWithStats(String templateJson, String dataJson, int dpi, int printQty) throws Exception {
+ JSONObject template = new JSONObject(templateJson);
+ JSONObject data = (dataJson == null || dataJson.trim().isEmpty()) ? new JSONObject() : new JSONObject(dataJson);
+ return buildWithStats(template, data, dpi, printQty);
+ }
+
+ public static BuildResult buildWithStats(JSONObject template, JSONObject data, int dpi, int printQty) throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ int nativeTextCount = 0;
+ int rasterTextCount = 0;
+ int qrCodeCount = 0;
+ int barcodeCount = 0;
+ int imagePatchCount = 0;
+ int lineCount = 0;
+ String unit = getString(template, "unit", "inch");
+ double widthMm = round1(toMillimeter(getDouble(template, "width", 0), unit));
+ double heightMm = round1(toMillimeter(getDouble(template, "height", 0), unit));
+ double pageWidthPx = widthMm / 25.4 * DESIGN_DPI;
+
+ addLine(out, "SIZE " + formatMm(widthMm) + " mm," + formatMm(heightMm) + " mm");
+ addLine(out, "GAP 0 mm,0 mm");
+ addLine(out, "CODEPAGE 1252");
+ addLine(out, "DENSITY 14");
+ addLine(out, "SPEED 5");
+ addLine(out, "CLS");
+
+ JSONArray elements = template.optJSONArray("elements");
+ if (elements != null) {
+ for (int i = 0; i < elements.length(); i++) {
+ JSONObject element = elements.optJSONObject(i);
+ if (element == null) continue;
+ JSONObject config = element.optJSONObject("config");
+ if (config == null) config = new JSONObject();
+ String type = getString(element, "type", "").toUpperCase();
+
+ if (type.startsWith("TEXT_")) {
+ String text = resolveElementText(type, config, data);
+ if (text.isEmpty()) continue;
+ String align = resolveElementAlign(element, config, pageWidthPx);
+ if (shouldRasterizeText(text, type)) {
+ rasterTextCount++;
+ BitmapPatch patch = createTextPatch(element, type, config, text, dpi, align);
+ writeBitmapPatch(out, patch);
+ } else {
+ nativeTextCount++;
+ int scale = resolveTextScale(getDouble(config, "fontSize", 14), dpi);
+ int x = resolveTextX(align, getDouble(element, "x", 0), getDouble(element, "width", 0), dpi, text, scale);
+ int y = pxToDots(getDouble(element, "y", 0), dpi);
+ int rotation = "vertical".equalsIgnoreCase(getString(element, "rotation", "horizontal")) ? 90 : 0;
+ addLine(out, "TEXT " + x + "," + y + ",\"TSS24.BF2\"," + rotation + "," + scale + "," + scale + ",\"" + escapeTscString(text) + "\"");
+ }
+ continue;
+ }
+
+ if ("QRCODE".equals(type)) {
+ qrCodeCount++;
+ String value = resolveElementDataValue(type, config, data);
+ if (value.isEmpty()) continue;
+ String level = normalizeQrLevel(getString(config, "errorLevel", "M"));
+ int x = pxToDots(getDouble(element, "x", 0), dpi);
+ int y = pxToDots(getDouble(element, "y", 0), dpi);
+ int size = resolveQrModuleSize(getDouble(element, "width", 0), getDouble(element, "height", 0), dpi, value, level);
+ addLine(out, "QRCODE " + x + "," + y + "," + level + "," + size + ",A,0,\"" + escapeTscString(value) + "\"");
+ continue;
+ }
+
+ if ("BARCODE".equals(type)) {
+ barcodeCount++;
+ String value = resolveElementDataValue(type, config, data);
+ if (value.isEmpty()) continue;
+ int x = pxToDots(getDouble(element, "x", 0), dpi);
+ int y = pxToDots(getDouble(element, "y", 0), dpi);
+ int height = Math.max(20, pxToDots(getDouble(element, "height", 0), dpi));
+ int readable = getBoolean(config, "showText", true) ? 1 : 0;
+ String orientation = getString(config, "orientation", getString(element, "rotation", "horizontal"));
+ int rotation = "vertical".equalsIgnoreCase(orientation) ? 90 : 0;
+ int narrow = clamp(getDouble(element, "width", 0) / Math.max(40.0, value.length() * 6.0), 1, 4);
+ int wide = clamp(getDouble(element, "width", 0) / Math.max(24.0, value.length() * 3.0), 2, 6);
+ String symbology = normalizeBarcodeType(getString(config, "barcodeType", "CODE128"));
+ addLine(out, "BARCODE " + x + "," + y + ",\"" + symbology + "\"," + height + "," + readable + "," + rotation + "," + narrow + "," + wide + ",\"" + escapeTscString(value) + "\"");
+ continue;
+ }
+
+ if ("IMAGE".equals(type)) {
+ BitmapPatch patch = createImagePatch(element, config, dpi);
+ if (patch != null) {
+ imagePatchCount++;
+ writeBitmapPatch(out, patch);
+ }
+ continue;
+ }
+
+ if ("BLANK".equals(type) && "line".equalsIgnoreCase(getString(element, "border", ""))) {
+ lineCount++;
+ int x = pxToDots(getDouble(element, "x", 0), dpi);
+ int y = pxToDots(getDouble(element, "y", 0), dpi);
+ int width = Math.max(1, pxToDots(getDouble(element, "width", 0), dpi));
+ int height = Math.max(1, pxToDots(getDouble(element, "height", 1), dpi));
+ addLine(out, "BAR " + x + "," + y + "," + width + "," + height);
+ }
+ }
+ }
+
+ addLine(out, "PRINT 1," + Math.max(1, printQty));
+ return new BuildResult(
+ out.toByteArray(),
+ nativeTextCount,
+ rasterTextCount,
+ qrCodeCount,
+ barcodeCount,
+ imagePatchCount,
+ lineCount,
+ elements == null ? 0 : elements.length()
+ );
+ }
+
+ private static String resolveElementText(String type, JSONObject config, JSONObject data) {
+ String configText = getString(config, "text", "");
+ boolean hasText = !configText.isEmpty();
+ if ("TEXT_PRICE".equals(type)) {
+ String bindingKey = resolveBindingKey(type, config);
+ String boundValue = resolveTemplateValue(data, bindingKey);
+ String raw = !boundValue.isEmpty() ? boundValue : (hasText ? applyTemplateData(configText, data) : "");
+ if (raw.isEmpty()) return "";
+ String prefix = getString(config, "prefix", "");
+ String suffix = getString(config, "suffix", "");
+ int decimal = (int) getDouble(config, "decimal", -1);
+ if (decimal >= 0) {
+ try {
+ double value = Double.parseDouble(raw);
+ raw = String.format(java.util.Locale.US, "%1$." + decimal + "f", value);
+ } catch (Exception ignored) {
+ }
+ }
+ return prefix + raw + suffix;
+ }
+ if (hasText && "TEXT_STATIC".equals(type)) {
+ return applyTemplateData(configText, data);
+ }
+ if (hasText && configText.contains("{{")) {
+ return applyTemplateData(configText, data);
+ }
+ String bindingKey = resolveBindingKey(type, config);
+ String boundValue = resolveTemplateValue(data, bindingKey);
+ if (!boundValue.isEmpty()) return boundValue;
+ return hasText ? applyTemplateData(configText, data) : "";
+ }
+
+ private static String resolveElementDataValue(String type, JSONObject config, JSONObject data) {
+ String raw = getString(config, "data", getString(config, "value", ""));
+ if (!raw.isEmpty()) return applyTemplateData(raw, data);
+ return resolveTemplateValue(data, resolveBindingKey(type, config));
+ }
+
+ private static String resolveBindingKey(String type, JSONObject config) {
+ String[] keys = new String[]{"dataKey", "field", "bindField", "key", "valueKey"};
+ for (String key : keys) {
+ String value = getString(config, key, "");
+ if (!value.isEmpty()) return value;
+ }
+ switch (type) {
+ case "TEXT_PRODUCT": return "productName";
+ case "TEXT_LABEL_ID": return "labelId";
+ case "TEXT_CATEGORY": return "category";
+ case "TEXT_PRICE": return "price";
+ case "TEXT_DATE": return "date";
+ case "TEXT_TIME": return "time";
+ case "QRCODE": return "qrCode";
+ case "BARCODE": return "barcode";
+ default:
+ String pureType = type.replace("TEXT_", "").replace("FIELD_", "").replace("VALUE_", "");
+ return pureType.isEmpty() ? "" : toCamelCase(pureType);
+ }
+ }
+
+ private static String resolveTemplateValue(JSONObject data, String key) {
+ if (key == null || key.isEmpty()) return "";
+ String[] candidates;
+ switch (key) {
+ case "productName": candidates = new String[]{"productName", "product"}; break;
+ case "product": candidates = new String[]{"product", "productName"}; break;
+ case "qrCode": candidates = new String[]{"qrCode", "labelId", "barcode"}; break;
+ case "barcode": candidates = new String[]{"barcode", "labelId", "qrCode"}; break;
+ default: candidates = new String[]{key};
+ }
+ for (String candidate : candidates) {
+ Object value = data.opt(candidate);
+ if (value != null) return String.valueOf(value);
+ }
+ return "";
+ }
+
+ private static String applyTemplateData(String text, JSONObject data) {
+ Matcher matcher = PLACEHOLDER_PATTERN.matcher(text == null ? "" : text);
+ StringBuffer buffer = new StringBuffer();
+ while (matcher.find()) {
+ String key = matcher.group(1);
+ Object value = data.opt(key);
+ matcher.appendReplacement(buffer, Matcher.quoteReplacement(value == null ? "" : String.valueOf(value)));
+ }
+ matcher.appendTail(buffer);
+ return buffer.toString();
+ }
+
+ private static String toCamelCase(String value) {
+ String[] parts = value.toLowerCase().split("[_\\s-]+");
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < parts.length; i++) {
+ if (parts[i].isEmpty()) continue;
+ if (builder.length() == 0) {
+ builder.append(parts[i]);
+ } else {
+ builder.append(Character.toUpperCase(parts[i].charAt(0))).append(parts[i].substring(1));
+ }
+ }
+ return builder.toString();
+ }
+
+ private static String resolveElementAlign(JSONObject element, JSONObject config, double pageWidthPx) {
+ String align = getString(config, "textAlign", "").toLowerCase();
+ if ("left".equals(align) || "center".equals(align) || "right".equals(align)) return align;
+ double centerX = getDouble(element, "x", 0) + getDouble(element, "width", 0) / 2.0;
+ if (centerX <= pageWidthPx * 0.33) return "left";
+ if (centerX >= pageWidthPx * 0.67) return "right";
+ return "center";
+ }
+
+ private static boolean shouldRasterizeText(String text, String type) {
+ if (text == null || text.isEmpty()) return false;
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if (c < 32 || c > 126) {
+ return true;
+ }
+ }
+ CharsetEncoder encoder = getPrinterEncoder();
+ if (encoder == null) return true;
+ try {
+ return !encoder.canEncode(text);
+ } catch (Exception e) {
+ return true;
+ }
+ }
+
+ private static BitmapPatch createTextPatch(JSONObject element, String type, JSONObject config, String text, int dpi, String align) {
+ int contentWidth = Math.max(8, pxToDots(getDouble(element, "width", 0), dpi));
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setDither(true);
+ paint.setSubpixelText(true);
+ paint.setColor(Color.BLACK);
+ int fontSizeDots = Math.max(14, pxToDots(getDouble(config, "fontSize", 14), dpi));
+ paint.setTextSize(fontSizeDots);
+ boolean bold = "bold".equalsIgnoreCase(getString(config, "fontWeight", "")) || "TEXT_PRICE".equals(type);
+ paint.setFakeBoldText(bold);
+ paint.setTypeface(bold ? Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD) : Typeface.SANS_SERIF);
+
+ if (text.indexOf('\n') < 0) {
+ int singleLineWidth = (int) Math.ceil(paint.measureText(text)) + 8;
+ contentWidth = Math.max(contentWidth, singleLineWidth);
+ }
+
+ java.util.List lines = splitTextLines(text, paint, Math.max(8, contentWidth));
+ Paint.FontMetrics metrics = paint.getFontMetrics();
+ int lineHeight = Math.max(fontSizeDots + 2, (int) Math.ceil(Math.abs(metrics.top) + Math.abs(metrics.bottom) + 2));
+ int totalHeight = lines.size() * lineHeight;
+ float maxLineWidth = 0;
+ for (String line : lines) {
+ maxLineWidth = Math.max(maxLineWidth, paint.measureText(line));
+ }
+ int horizontalPadding = TEXT_PADDING_DOTS * 2;
+ int verticalPadding = TEXT_PADDING_DOTS * 2;
+ int width = ensureMultipleOf8(Math.max(contentWidth + horizontalPadding * 2, (int) Math.ceil(maxLineWidth) + horizontalPadding * 2 + 4));
+ int height = Math.max(16, Math.max(pxToDots(getDouble(element, "height", 0), dpi) + verticalPadding * 2, totalHeight + verticalPadding * 2 + 4));
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ canvas.drawColor(Color.WHITE);
+
+ int topOffset = "TEXT_PRICE".equals(type)
+ ? Math.max(verticalPadding, (height - totalHeight) / 2)
+ : verticalPadding;
+ int drawableWidth = width - horizontalPadding * 2;
+
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines.get(i);
+ float lineWidth = paint.measureText(line);
+ float drawX = horizontalPadding;
+ if ("center".equals(align)) {
+ drawX = horizontalPadding + Math.max(0, (drawableWidth - lineWidth) / 2f);
+ } else if ("right".equals(align)) {
+ drawX = horizontalPadding + Math.max(0, drawableWidth - lineWidth);
+ }
+ float baseline = topOffset + i * lineHeight - metrics.top;
+ canvas.drawText(line, drawX, baseline, paint);
+ }
+
+ BitmapPatch patch = new BitmapPatch(Math.max(0, pxToDots(getDouble(element, "x", 0), dpi) - horizontalPadding),
+ Math.max(0, pxToDots(getDouble(element, "y", 0), dpi) - verticalPadding),
+ invertMonochrome(bitmapToMonochrome(bitmap, DEFAULT_THRESHOLD)));
+ bitmap.recycle();
+ return patch;
+ }
+
+ private static BitmapPatch createImagePatch(JSONObject element, JSONObject config, int dpi) {
+ String source = getString(config, "src", getString(config, "data", getString(config, "url", "")));
+ if (source.isEmpty()) return null;
+ Bitmap sourceBitmap = decodeBitmap(source);
+ if (sourceBitmap == null) return null;
+
+ int width = ensureMultipleOf8(Math.max(8, pxToDots(getDouble(element, "width", 0), dpi)));
+ int height = Math.max(8, pxToDots(getDouble(element, "height", 0), dpi));
+ Bitmap outputBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(outputBitmap);
+ canvas.drawColor(Color.WHITE);
+
+ int sourceWidth = sourceBitmap.getWidth();
+ int sourceHeight = sourceBitmap.getHeight();
+ String scaleMode = getString(config, "scaleMode", "contain").toLowerCase();
+ int targetWidth = width;
+ int targetHeight = height;
+ int targetLeft = 0;
+ int targetTop = 0;
+
+ if (sourceWidth > 0 && sourceHeight > 0 && !"fill".equals(scaleMode)) {
+ double ratio = "cover".equals(scaleMode)
+ ? Math.max((double) width / sourceWidth, (double) height / sourceHeight)
+ : Math.min((double) width / sourceWidth, (double) height / sourceHeight);
+ targetWidth = Math.max(1, (int) Math.round(sourceWidth * ratio));
+ targetHeight = Math.max(1, (int) Math.round(sourceHeight * ratio));
+ targetLeft = (width - targetWidth) / 2;
+ targetTop = (height - targetHeight) / 2;
+ }
+
+ Bitmap scaledBitmap = Bitmap.createScaledBitmap(sourceBitmap, targetWidth, targetHeight, true);
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setFilterBitmap(true);
+ canvas.drawBitmap(scaledBitmap, targetLeft, targetTop, paint);
+
+ BitmapPatch patch = new BitmapPatch(pxToDots(getDouble(element, "x", 0), dpi),
+ pxToDots(getDouble(element, "y", 0), dpi),
+ bitmapToMonochrome(outputBitmap, (int) getDouble(config, "threshold", DEFAULT_THRESHOLD)));
+
+ scaledBitmap.recycle();
+ sourceBitmap.recycle();
+ outputBitmap.recycle();
+ return patch;
+ }
+
+ private static Bitmap decodeBitmap(String source) {
+ try {
+ if (source.startsWith("data:image/")) {
+ int comma = source.indexOf(',');
+ String payload = comma >= 0 ? source.substring(comma + 1) : "";
+ if (payload.isEmpty()) return null;
+ byte[] bytes = Base64.decode(payload, Base64.DEFAULT);
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ }
+ if (source.matches("^[A-Za-z0-9+/=\\r\\n]+$") && source.length() > 128) {
+ byte[] bytes = Base64.decode(source, Base64.DEFAULT);
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ }
+ String path = source.startsWith("file://") ? source.substring(7) : source;
+ return BitmapFactory.decodeFile(path);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private static java.util.List splitTextLines(String text, Paint paint, int maxWidth) {
+ java.util.List lines = new java.util.ArrayList<>();
+ String[] rawLines = (text == null ? "" : text.replace("\r", "")).split("\n");
+ for (String segment : rawLines) {
+ if (segment.isEmpty()) {
+ lines.add("");
+ continue;
+ }
+ StringBuilder current = new StringBuilder();
+ for (int i = 0; i < segment.length(); i++) {
+ char c = segment.charAt(i);
+ String candidate = current.toString() + c;
+ if (current.length() > 0 && paint.measureText(candidate) > maxWidth) {
+ lines.add(current.toString());
+ current.setLength(0);
+ current.append(c);
+ } else {
+ current.append(c);
+ }
+ }
+ if (current.length() > 0) lines.add(current.toString());
+ }
+ if (lines.isEmpty()) lines.add("");
+ return lines;
+ }
+
+ private static void writeBitmapPatch(ByteArrayOutputStream out, BitmapPatch patch) {
+ int bytesPerRow = patch.image.width / 8;
+ addLine(out, "BITMAP " + patch.x + "," + patch.y + "," + bytesPerRow + "," + patch.image.height + ",0,");
+ for (int y = 0; y < patch.image.height; y++) {
+ for (int byteIndex = 0; byteIndex < bytesPerRow; byteIndex++) {
+ int value = 0;
+ for (int bit = 0; bit < 8; bit++) {
+ int x = byteIndex * 8 + bit;
+ int pixel = patch.image.pixels[y * patch.image.width + x];
+ if (pixel == 1) value |= (1 << (7 - bit));
+ }
+ out.write(value & 0xFF);
+ }
+ }
+ out.write('\r');
+ out.write('\n');
+ }
+
+ private static MonochromeImage bitmapToMonochrome(Bitmap bitmap, int threshold) {
+ int bitmapWidth = bitmap.getWidth();
+ int bitmapHeight = bitmap.getHeight();
+ int width = ensureMultipleOf8(bitmapWidth);
+ int[] pixels = new int[width * bitmapHeight];
+ for (int y = 0; y < bitmapHeight; y++) {
+ for (int x = 0; x < width; x++) {
+ if (x >= bitmapWidth) {
+ pixels[y * width + x] = 0;
+ continue;
+ }
+ int color = bitmap.getPixel(x, y);
+ int alpha = (color >>> 24) & 0xFF;
+ int red = (color >>> 16) & 0xFF;
+ int green = (color >>> 8) & 0xFF;
+ int blue = color & 0xFF;
+ double gray = red * 0.299 + green * 0.587 + blue * 0.114;
+ pixels[y * width + x] = alpha <= 10 || gray > threshold ? 0 : 1;
+ }
+ }
+ return new MonochromeImage(width, bitmapHeight, pixels);
+ }
+
+ private static MonochromeImage invertMonochrome(MonochromeImage image) {
+ if (image == null || image.pixels == null) return image;
+ int[] pixels = new int[image.pixels.length];
+ for (int i = 0; i < image.pixels.length; i++) {
+ pixels[i] = image.pixels[i] == 1 ? 0 : 1;
+ }
+ return new MonochromeImage(image.width, image.height, pixels);
+ }
+
+ private static void addLine(ByteArrayOutputStream out, String line) {
+ byte[] bytes = line.getBytes(getPrinterCharset());
+ out.write(bytes, 0, bytes.length);
+ out.write('\r');
+ out.write('\n');
+ }
+
+ private static Charset getPrinterCharset() {
+ try {
+ return Charset.forName("windows-1252");
+ } catch (Throwable first) {
+ try {
+ return Charset.forName("Cp1252");
+ } catch (Throwable second) {
+ return StandardCharsets.ISO_8859_1;
+ }
+ }
+ }
+
+ private static CharsetEncoder getPrinterEncoder() {
+ try {
+ return getPrinterCharset().newEncoder();
+ } catch (Throwable first) {
+ try {
+ return StandardCharsets.ISO_8859_1.newEncoder();
+ } catch (Throwable second) {
+ return null;
+ }
+ }
+ }
+
+ private static String escapeTscString(String value) {
+ return value == null ? "" : value.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+
+ private static String normalizeBarcodeType(String value) {
+ String key = value == null ? "CODE128" : value.trim().toUpperCase();
+ switch (key) {
+ case "CODE39": return "39";
+ case "EAN13": return "EAN13";
+ case "EAN8": return "EAN8";
+ case "UPCA": return "UPCA";
+ case "UPCE": return "UPCE";
+ case "CODABAR": return "CODA";
+ case "ITF14": return "ITF14";
+ case "ITF": return "ITF";
+ default: return "128";
+ }
+ }
+
+ private static String normalizeQrLevel(String value) {
+ String key = value == null ? "M" : value.trim().toUpperCase();
+ if ("L".equals(key) || "M".equals(key) || "Q".equals(key) || "H".equals(key)) return key;
+ return "M";
+ }
+
+ private static int resolveQrModuleSize(double widthPx, double heightPx, int dpi, String value, String level) {
+ int targetDots = Math.max(24, Math.min(pxToDots(widthPx, dpi), pxToDots(heightPx, dpi)));
+ int moduleCount = Math.max(21, estimateQrModuleCount(value, level));
+ return clamp(Math.floorDiv(targetDots, moduleCount), 3, 12);
+ }
+
+ private static int estimateQrModuleCount(String value, String level) {
+ int length = Math.max(1, value == null ? 0 : value.length());
+ int[] capacities;
+ switch (level) {
+ case "L": capacities = new int[]{17, 32, 53, 78, 106, 134, 154, 192, 230, 271}; break;
+ case "Q": capacities = new int[]{11, 20, 32, 46, 60, 74, 86, 108, 130, 151}; break;
+ case "H": capacities = new int[]{7, 14, 24, 34, 44, 58, 64, 84, 98, 119}; break;
+ default: capacities = new int[]{14, 26, 42, 62, 84, 106, 122, 152, 180, 213};
+ }
+ int version = capacities.length;
+ for (int i = 0; i < capacities.length; i++) {
+ if (length <= capacities[i]) {
+ version = i + 1;
+ break;
+ }
+ }
+ return 21 + (version - 1) * 4;
+ }
+
+ private static int resolveTextScale(double fontSizePx, int dpi) {
+ int targetDots = Math.max(12, (int) Math.round(fontSizePx * dpi / DESIGN_DPI));
+ return clamp(targetDots / 24.0, 1, 7);
+ }
+
+ private static int resolveTextX(String align, double xPx, double widthPx, int dpi, String text, int scale) {
+ int left = pxToDots(xPx, dpi);
+ if ("left".equals(align)) return left;
+ int boxWidth = pxToDots(widthPx, dpi);
+ int fontDots = Math.max(24, scale * 24);
+ int textWidth = estimateTextWidthDots(text, fontDots);
+ if ("center".equals(align)) return Math.max(0, left + Math.max(0, boxWidth - textWidth) / 2);
+ return Math.max(0, left + Math.max(0, boxWidth - textWidth));
+ }
+
+ private static int estimateTextWidthDots(String text, int fontDots) {
+ double total = 0;
+ for (int i = 0; i < text.length(); i++) {
+ total += text.charAt(i) > 255 ? fontDots : fontDots * 0.6;
+ }
+ return (int) Math.round(total);
+ }
+
+ private static int clamp(double value, int min, int max) {
+ return Math.max(min, Math.min(max, (int) Math.round(value)));
+ }
+
+ private static int ensureMultipleOf8(int value) {
+ int safe = Math.max(8, value);
+ return safe % 8 == 0 ? safe : safe + (8 - safe % 8);
+ }
+
+ private static int pxToDots(double value, int dpi) {
+ return Math.max(0, (int) Math.round(value * dpi / DESIGN_DPI));
+ }
+
+ private static double toMillimeter(double value, String unit) {
+ if ("mm".equalsIgnoreCase(unit)) return value;
+ if ("cm".equalsIgnoreCase(unit)) return value * 10;
+ if ("px".equalsIgnoreCase(unit)) return value / DESIGN_DPI * 25.4;
+ return value * 25.4;
+ }
+
+ private static double round1(double value) {
+ return Math.round(value * 10.0) / 10.0;
+ }
+
+ private static String formatMm(double value) {
+ return String.format(java.util.Locale.US, "%.1f", value);
+ }
+
+ private static String getString(JSONObject json, String key, String fallback) {
+ Object value = json.opt(key);
+ return value == null ? fallback : String.valueOf(value);
+ }
+
+ private static double getDouble(JSONObject json, String key, double fallback) {
+ try {
+ Object value = json.opt(key);
+ if (value == null) return fallback;
+ if (value instanceof Number) return ((Number) value).doubleValue();
+ return Double.parseDouble(String.valueOf(value));
+ } catch (Exception e) {
+ return fallback;
+ }
+ }
+
+ private static boolean getBoolean(JSONObject json, String key, boolean fallback) {
+ try {
+ Object value = json.opt(key);
+ if (value == null) return fallback;
+ if (value instanceof Boolean) return (Boolean) value;
+ return Boolean.parseBoolean(String.valueOf(value));
+ } catch (Exception e) {
+ return fallback;
+ }
+ }
+
+ private static final class BitmapPatch {
+ final int x;
+ final int y;
+ final MonochromeImage image;
+
+ BitmapPatch(int x, int y, MonochromeImage image) {
+ this.x = x;
+ this.y = y;
+ this.image = image;
+ }
+ }
+
+ private static final class MonochromeImage {
+ final int width;
+ final int height;
+ final int[] pixels;
+
+ MonochromeImage(int width, int height, int[] pixels) {
+ this.width = width;
+ this.height = height;
+ this.pixels = pixels;
+ }
+ }
+
+ public static final class BuildResult {
+ public final byte[] bytes;
+ public final int nativeTextCount;
+ public final int rasterTextCount;
+ public final int qrCodeCount;
+ public final int barcodeCount;
+ public final int imagePatchCount;
+ public final int lineCount;
+ public final int elementCount;
+
+ public BuildResult(byte[] bytes, int nativeTextCount, int rasterTextCount, int qrCodeCount, int barcodeCount,
+ int imagePatchCount, int lineCount, int elementCount) {
+ this.bytes = bytes == null ? new byte[0] : bytes;
+ this.nativeTextCount = nativeTextCount;
+ this.rasterTextCount = rasterTextCount;
+ this.qrCodeCount = qrCodeCount;
+ this.barcodeCount = barcodeCount;
+ this.imagePatchCount = imagePatchCount;
+ this.lineCount = lineCount;
+ this.elementCount = elementCount;
+ }
+ }
+}
diff --git a/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/transport/GprinterBluetoothTransport.java b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/transport/GprinterBluetoothTransport.java
new file mode 100644
index 0000000..4bdeaf8
--- /dev/null
+++ b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/transport/GprinterBluetoothTransport.java
@@ -0,0 +1,158 @@
+package com.foodlabel.nativeprinter.transport;
+
+import android.content.Context;
+
+import com.gprinter.bean.PrinterDevices;
+import com.gprinter.io.BluetoothPort;
+import com.gprinter.io.PortManager;
+import com.gprinter.utils.CallbackListener;
+import com.gprinter.utils.Command;
+import com.gprinter.utils.ConnMethod;
+import com.foodlabel.nativeprinter.debug.NativePrintDebugState;
+import com.foodlabel.nativeprinter.support.PluginResult;
+import com.foodlabel.nativeprinter.support.ThrowableUtils;
+
+import java.lang.reflect.Method;
+
+public final class GprinterBluetoothTransport {
+ private PortManager portManager;
+
+ public synchronized PluginResult ensureConnected(String deviceId, String deviceName, NativePrintDebugState debugState) {
+ debugState.setStage("ensureConnected");
+ debugState.clearError();
+
+ if (deviceId == null || deviceId.trim().isEmpty()) {
+ return PluginResult.error(9011003, "Bluetooth device address is empty.");
+ }
+
+ if (isConnected() && deviceId.equals(debugState.getCurrentDeviceId())) {
+ if (deviceName != null && !deviceName.trim().isEmpty()) {
+ debugState.setCurrentDevice(deviceId, deviceName);
+ }
+ debugState.setStage("connect:ok");
+ return PluginResult.ok(true, debugState.getCurrentDeviceId(), debugState.getCurrentDeviceName(), "connect:ok");
+ }
+
+ disconnect();
+ Context context = resolveContext();
+ if (context == null) {
+ return PluginResult.error(9011010, "Unable to resolve Android context for native printer plugin.");
+ }
+
+ try {
+ PrinterDevices devices = new PrinterDevices.Build()
+ .setContext(context)
+ .setConnMethod(ConnMethod.BLUETOOTH)
+ .setMacAddress(deviceId)
+ .setBlueName(deviceName)
+ .setCommand(Command.TSC)
+ .setCallbackListener(createCallbackListener(debugState))
+ .build();
+ BluetoothPort newPort = new BluetoothPort(devices);
+ debugState.setStage("sdk:openPort");
+ boolean opened = newPort.openPort();
+ if (!opened || !newPort.getConnectStatus()) {
+ try {
+ newPort.closePort();
+ } catch (Exception ignored) {
+ }
+ return PluginResult.error(9011004, "Bluetooth sdk openPort failed.");
+ }
+ portManager = newPort;
+ debugState.setCurrentDevice(deviceId, (deviceName != null && !deviceName.trim().isEmpty()) ? deviceName : "Bluetooth Printer");
+ debugState.setStage("connect:ok");
+ debugState.clearError();
+ return PluginResult.ok(true, debugState.getCurrentDeviceId(), debugState.getCurrentDeviceName(), "connect:ok");
+ } catch (Throwable error) {
+ debugState.setError(ThrowableUtils.unwrap(error));
+ disconnect();
+ return PluginResult.error(9011004, debugState.getLastError());
+ }
+ }
+
+ public synchronized boolean isConnected() {
+ if (portManager == null) return false;
+ try {
+ return portManager.getConnectStatus();
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public synchronized boolean write(byte[] bytes) throws Exception {
+ if (portManager == null) return false;
+ return portManager.writeDataImmediately(bytes);
+ }
+
+ public synchronized void disconnect() {
+ if (portManager != null) {
+ try {
+ portManager.closePort();
+ } catch (Exception ignored) {
+ }
+ }
+ portManager = null;
+ }
+
+ private static CallbackListener createCallbackListener(final NativePrintDebugState debugState) {
+ return new CallbackListener() {
+ @Override
+ public void onConnecting() {
+ debugState.setStage("sdk:onConnecting");
+ }
+
+ @Override
+ public void onCheckCommand() {
+ debugState.setStage("sdk:onCheckCommand");
+ }
+
+ @Override
+ public void onSuccess(PrinterDevices printerDevices) {
+ debugState.setStage("sdk:onSuccess");
+ debugState.clearError();
+ if (printerDevices != null && printerDevices.getBlueName() != null && !printerDevices.getBlueName().trim().isEmpty()) {
+ debugState.setCurrentDevice(debugState.getCurrentDeviceId(), printerDevices.getBlueName());
+ }
+ }
+
+ @Override
+ public void onReceive(byte[] bytes) {
+ debugState.setStage("sdk:onReceive");
+ }
+
+ @Override
+ public void onFailure() {
+ debugState.setStage("sdk:onFailure");
+ debugState.setError("Bluetooth sdk reported onFailure.");
+ }
+
+ @Override
+ public void onDisconnect() {
+ debugState.setStage("sdk:onDisconnect");
+ }
+ };
+ }
+
+ private static Context resolveContext() {
+ try {
+ Class> activityThreadClass = Class.forName("android.app.ActivityThread");
+ Method currentApplication = activityThreadClass.getMethod("currentApplication");
+ Object application = currentApplication.invoke(null);
+ if (application instanceof Context) {
+ return ((Context) application).getApplicationContext();
+ }
+ } catch (Exception ignored) {
+ }
+
+ try {
+ Class> appGlobalsClass = Class.forName("android.app.AppGlobals");
+ Method getInitialApplication = appGlobalsClass.getMethod("getInitialApplication");
+ Object application = getInitialApplication.invoke(null);
+ if (application instanceof Context) {
+ return ((Context) application).getApplicationContext();
+ }
+ } catch (Exception ignored) {
+ }
+ return null;
+ }
+}
diff --git a/打印机安卓基座/native-fast-printer/android/native_fast_printer-release.aar b/打印机安卓基座/native-fast-printer/android/native_fast_printer-release.aar
new file mode 100644
index 0000000..deb972f
Binary files /dev/null and b/打印机安卓基座/native-fast-printer/android/native_fast_printer-release.aar differ
diff --git a/打印机安卓基座/native-fast-printer/package.json b/打印机安卓基座/native-fast-printer/package.json
new file mode 100644
index 0000000..9572213
--- /dev/null
+++ b/打印机安卓基座/native-fast-printer/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "native-fast-printer",
+ "id": "native-fast-printer",
+ "version": "1.0.0",
+ "description": "Android高速标签打印原生插件",
+ "_dp_type": "nativeplugin",
+ "_dp_nativeplugin": {
+ "android": {
+ "plugins": [
+ {
+ "type": "module",
+ "name": "native-fast-printer",
+ "class": "com.foodlabel.nativeprinter.NativeFastPrinterModule"
+ }
+ ],
+ "integrateType": "aar",
+ "dependencies_remark": "使用本地AAR,依赖HBuilder基座内置UniModule/JSCallback/fastjson。",
+ "dependencies": [],
+ "compileOptions": {
+ "sourceCompatibility": "1.8",
+ "targetCompatibility": "1.8"
+ },
+ "abis": [
+ "armeabi-v7a",
+ "arm64-v8a",
+ "x86"
+ ],
+ "minSdkVersion": "21",
+ "useAndroidX": true,
+ "permissions": [
+ "android.permission.BLUETOOTH",
+ "android.permission.BLUETOOTH_ADMIN",
+ "android.permission.BLUETOOTH_CONNECT"
+ ]
+ }
+ }
+}
diff --git a/打印机安卓基座/native-fast-printer/sync-to-uniapp.sh b/打印机安卓基座/native-fast-printer/sync-to-uniapp.sh
new file mode 100755
index 0000000..43843c0
--- /dev/null
+++ b/打印机安卓基座/native-fast-printer/sync-to-uniapp.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+set -euo pipefail
+
+PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_ROOT="$(cd "$PLUGIN_DIR/../.." && pwd)"
+UNIAPP_PLUGIN_DIR="$PROJECT_ROOT/美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer"
+
+rm -rf "$UNIAPP_PLUGIN_DIR"
+mkdir -p "$UNIAPP_PLUGIN_DIR/android"
+
+cp "$PLUGIN_DIR/package.json" "$UNIAPP_PLUGIN_DIR/package.json"
+cp "$PLUGIN_DIR/README.md" "$UNIAPP_PLUGIN_DIR/README.md"
+cp "$PLUGIN_DIR/android/native_fast_printer-release.aar" "$UNIAPP_PLUGIN_DIR/android/"
+
+echo "Synced native-fast-printer to: $UNIAPP_PLUGIN_DIR"
diff --git a/美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/README.md b/美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/README.md
new file mode 100644
index 0000000..77c40a8
--- /dev/null
+++ b/美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/README.md
@@ -0,0 +1,40 @@
+# native-fast-printer
+
+传统 `nativeplugins` Android 原生插件版高速标签打印模块。
+
+## 能力
+- 经典蓝牙连接 / 断开 / 状态
+- 接收系统模板 JSON
+- 原生生成 TSC 指令
+- 文本、价格、条码、二维码、横线、图片
+- 特殊字符文本和图片走原生位图补丁
+
+## 前端调用
+```js
+const printer = uni.requireNativePlugin('native-fast-printer')
+```
+
+## 方法
+- `connect(params, callback)`
+- `disconnect(callback)`
+- `isConnected(callback)`
+- `printTemplate(params, callback)`
+
+## 源码位置
+- 当前目录是源码主目录
+- `美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/` 是同步后的 uni-app 打包镜像
+
+## 目录结构
+- `android-src/src/com/foodlabel/nativeprinter/`
+ - `NativeFastPrinterModule.java`:uni-app 原生模块入口
+ - `transport/`:蓝牙连接与 SDK 传输层
+ - `template/`:系统模板 JSON → TSC 指令
+ - `debug/`:调试状态与统计信息
+ - `support/`:结果对象、JSON 读取、异常展开
+- `android/`:编译产物 AAR
+- `sync-to-uniapp.sh`:同步到 uni-app 打包镜像
+
+## 说明
+1. 修改源码后执行 `android-src/build-aar.sh`
+2. 再执行 `sync-to-uniapp.sh`
+3. 重新打包 uni-app 自定义基座
diff --git a/美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/android/native_fast_printer-release.aar b/美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/android/native_fast_printer-release.aar
new file mode 100644
index 0000000..deb972f
Binary files /dev/null and b/美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/android/native_fast_printer-release.aar differ
diff --git a/美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/package.json b/美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/package.json
new file mode 100644
index 0000000..9572213
--- /dev/null
+++ b/美国版/Food Labeling Management App UniApp/nativeplugins/native-fast-printer/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "native-fast-printer",
+ "id": "native-fast-printer",
+ "version": "1.0.0",
+ "description": "Android高速标签打印原生插件",
+ "_dp_type": "nativeplugin",
+ "_dp_nativeplugin": {
+ "android": {
+ "plugins": [
+ {
+ "type": "module",
+ "name": "native-fast-printer",
+ "class": "com.foodlabel.nativeprinter.NativeFastPrinterModule"
+ }
+ ],
+ "integrateType": "aar",
+ "dependencies_remark": "使用本地AAR,依赖HBuilder基座内置UniModule/JSCallback/fastjson。",
+ "dependencies": [],
+ "compileOptions": {
+ "sourceCompatibility": "1.8",
+ "targetCompatibility": "1.8"
+ },
+ "abis": [
+ "armeabi-v7a",
+ "arm64-v8a",
+ "x86"
+ ],
+ "minSdkVersion": "21",
+ "useAndroidX": true,
+ "permissions": [
+ "android.permission.BLUETOOTH",
+ "android.permission.BLUETOOTH_ADMIN",
+ "android.permission.BLUETOOTH_CONNECT"
+ ]
+ }
+ }
+}
diff --git a/美国版/Food Labeling Management App UniApp/src/manifest.json b/美国版/Food Labeling Management App UniApp/src/manifest.json
index 4c438b3..8287d82 100644
--- a/美国版/Food Labeling Management App UniApp/src/manifest.json
+++ b/美国版/Food Labeling Management App UniApp/src/manifest.json
@@ -2,8 +2,8 @@
"name" : "food.labeling",
"appid" : "__UNI__1BFD76D",
"description" : "",
- "versionName" : "1.0.3",
- "versionCode" : 103,
+ "versionName" : "1.0.5",
+ "versionCode" : 105,
"transformPx" : false,
/* 5+App特有相关 */
"app-plus" : {
@@ -104,6 +104,20 @@
"pid" : "",
"parameters" : {}
}
+ },
+ "native-fast-printer" : {
+ "__plugin_info__" : {
+ "name" : "native-fast-printer",
+ "description" : "Android高速标签打印原生插件",
+ "platforms" : "Android",
+ "url" : "",
+ "android_package_name" : "",
+ "ios_bundle_id" : "",
+ "isCloud" : false,
+ "bought" : -1,
+ "pid" : "",
+ "parameters" : {}
+ }
}
}
},
diff --git a/美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue b/美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue
index 9812a8c..aea8f5d 100644
--- a/美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue
+++ b/美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue
@@ -52,11 +52,16 @@
Device Brand: {{ deviceIdentity.brand || '-' }}
Device Product: {{ deviceIdentity.product || '-' }}
Classic Module: {{ debugInfo.classicModuleReady ? 'Ready' : 'Not Ready' }}
+ Native Plugin: {{ nativeDebug.available ? 'Ready' : 'Missing' }}
+ Native Backend: {{ nativeDebug.backend || '-' }}
+ Native Stage: {{ nativeDebug.stage || '-' }}
+ Native Command Bytes: {{ nativeDebug.commandBytes || 0 }}
Paired Count: {{ debugInfo.pairedCount }}
Virtual BT Printer: {{ debugInfo.foundVirtualPrinter ? 'Found' : 'Not Found' }}
Classic Scan: {{ debugInfo.lastClassicEvent }}
BLE Scan: {{ debugInfo.lastBleEvent }}
{{ debugInfo.lastBleError }}
+ {{ nativeDebug.lastError }}
Android system Location service is OFF. Turn it on before BLE scan.
@@ -183,6 +188,10 @@ import {
} from '../../utils/print/printerConnection'
import { ensureBluetoothPermissions } from '../../utils/print/bluetoothPermissions'
import {
+ getNativeFastPrinterDebugInfo,
+ getNativeFastPrinterState,
+} from '../../utils/print/nativeFastPrinter'
+import {
connectBluetoothPrinter,
describeDiscoveredPrinter,
disconnectCurrentPrinter,
@@ -216,6 +225,7 @@ const debugInfo = ref({
lastBleError: '',
locationServiceRequired: false,
})
+const nativeDebug = ref(getNativeFastPrinterState() || {})
interface BtDevice {
deviceId: string
@@ -234,6 +244,19 @@ function refreshCurrentPrinter () {
debugInfo.value.currentMode = currentPrinter.value.type || 'none'
}
+async function refreshNativeDebug () {
+ nativeDebug.value = getNativeFastPrinterState() || {}
+ try {
+ const info = await getNativeFastPrinterDebugInfo()
+ nativeDebug.value = {
+ ...nativeDebug.value,
+ ...info,
+ }
+ } catch (_) {
+ nativeDebug.value = getNativeFastPrinterState() || {}
+ }
+}
+
function hasPreferredClassicDeviceInList () {
return devices.value.some((item: any) => {
const name = String(item?.name || '').toLowerCase()
@@ -486,9 +509,11 @@ const handleConnect = async (dev: BtDevice) => {
try {
await connectBluetoothPrinter(dev)
refreshCurrentPrinter()
+ await refreshNativeDebug()
connectingId.value = ''
uni.showToast({ title: 'Connected!', icon: 'success' })
} catch (e: any) {
+ await refreshNativeDebug()
errorMsg.value = (e && e.message) ? e.message : 'Connection failed'
connectingId.value = ''
}
@@ -505,13 +530,13 @@ const handleTestPrint = async () => {
if (testPrinting.value) return
testPrinting.value = true
try {
- uni.showLoading({ title: 'Preparing test print...', mask: true })
- await testPrintCurrentPrinter((p) => {
- if (p < 100) uni.showLoading({ title: `Printing ${p}%`, mask: true })
- })
+ uni.showLoading({ title: 'Sending test job...', mask: true })
+ await testPrintCurrentPrinter()
+ await refreshNativeDebug()
uni.hideLoading()
uni.showToast({ title: 'Test print sent!', icon: 'success' })
} catch (e: any) {
+ await refreshNativeDebug()
uni.hideLoading()
const msg = (e && e.message) ? e.message : 'Please check printer connection.'
if (msg === 'BUILTIN_PLUGIN_NOT_FOUND') {
@@ -536,11 +561,13 @@ const handleTestPrint = async () => {
const handleDisconnect = async () => {
await disconnectCurrentPrinter()
refreshCurrentPrinter()
+ await refreshNativeDebug()
uni.showToast({ title: 'Disconnected', icon: 'none' })
}
onMounted(() => {
debugInfo.value.classicModuleReady = !!classicBluetooth
+ refreshNativeDebug()
uni.onBluetoothDeviceFound(onDeviceFound)
uni.onBluetoothAdapterStateChange((res: any) => {
if (!res.available) {
diff --git a/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue b/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
index 101e955..e1ebb0d 100644
--- a/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
+++ b/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
@@ -249,13 +249,10 @@ const handlePrint = async () => {
}
isPrinting.value = true
try {
- uni.showLoading({ title: 'Preparing print data...', mask: true })
+ uni.showLoading({ title: 'Sending print job...', mask: true })
await new Promise(resolve => setTimeout(resolve, 30))
await printSystemTemplateForCurrentPrinter(PREVIEW_SYSTEM_TEMPLATE, printTemplateData.value, {
printQty: printQty.value,
- }, (percent) => {
- if (percent >= 100) return
- uni.showLoading({ title: `Printing ${percent}%`, mask: true })
})
uni.hideLoading()
uni.showToast({
diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts
index 5b11457..981e4a1 100644
--- a/美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts
+++ b/美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts
@@ -11,7 +11,14 @@ import classicBluetooth from '../bluetoothTool.js'
import { rasterizeImageData, rasterizeImageForPrinter } from '../imageRaster'
import { buildEscPosImageData, buildEscPosTemplateData } from '../protocols/escPosBuilder'
import { buildTscImageData, buildTscTemplateData } from '../protocols/tscProtocol'
+import {
+ connectNativeFastPrinter as connectNativeFastPrinterPlugin,
+ disconnectNativeFastPrinter as disconnectNativeFastPrinterPlugin,
+ isNativeFastPrinterAvailable,
+ printNativeFastTemplate as printNativeFastTemplatePlugin,
+} from '../nativeFastPrinter'
import { adaptSystemLabelTemplate } from '../systemTemplateAdapter'
+import { TEST_PRINT_SYSTEM_TEMPLATE, TEST_PRINT_TEMPLATE_DATA } from '../templates/testPrintTemplate'
import { describePrinterCandidate, getPrinterDriverByKey, resolvePrinterDriver } from './driverRegistry'
import type {
CurrentPrinterSummary,
@@ -34,25 +41,25 @@ function getPrinterTypeDisplayName (type: '' | 'bluetooth' | 'builtin'): string
function connectClassicBluetooth (device: PrinterCandidate, driver: PrinterDriver): Promise {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
- const classic = classicBluetooth
- if (!classic || !classic.connDevice) {
- reject(new Error('Classic Bluetooth not available. Ensure app is running on the device.'))
- return
- }
- classic.connDevice(device.deviceId, (ok: boolean) => {
- if (!ok) {
- reject(new Error('Classic Bluetooth connection failed.'))
- return
- }
- setBluetoothConnection({
+ if (isNativeFastPrinterAvailable()) {
+ connectNativeFastPrinterPlugin({
deviceId: device.deviceId,
deviceName: device.name || 'Bluetooth Printer',
- deviceType: 'classic',
- driverKey: driver.key,
- mtu: driver.preferredBleMtu || 20,
+ }).then(() => {
+ setBluetoothConnection({
+ deviceId: device.deviceId,
+ deviceName: device.name || 'Bluetooth Printer',
+ deviceType: 'classic',
+ driverKey: driver.key,
+ mtu: driver.preferredBleMtu || 20,
+ })
+ resolve()
+ }).catch((error: any) => {
+ reject(error instanceof Error ? error : new Error(String(error || 'Classic Bluetooth connection failed.')))
})
- resolve()
- })
+ return
+ }
+ reject(new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND. Please rebuild the custom base with native-fast-printer.'))
// #endif
// #ifndef APP-PLUS
reject(new Error('Classic Bluetooth requires the app.'))
@@ -207,8 +214,40 @@ export function getCurrentPrinterSummary (): CurrentPrinterSummary {
}
}
+function canUseNativeFastTemplatePrint (driver: PrinterDriver): boolean {
+ const connection = getBluetoothConnection()
+ return driver.protocol === 'tsc'
+ && connection?.deviceType === 'classic'
+ && isNativeFastPrinterAvailable()
+}
+
+function getNativeClassicConnection () {
+ const connection = getBluetoothConnection()
+ if (!connection || connection.deviceType !== 'classic') return null
+ return connection
+}
+
export async function testPrintCurrentPrinter (onProgress?: (percent: number) => void): Promise {
const driver = getCurrentPrinterDriver()
+ const connection = getBluetoothConnection()
+ if (driver.protocol === 'tsc' && connection?.deviceType === 'classic' && !isNativeFastPrinterAvailable()) {
+ throw new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND. Please rebuild the custom base with native-fast-printer.')
+ }
+ if (canUseNativeFastTemplatePrint(driver)) {
+ const nativeConnection = getNativeClassicConnection()
+ if (nativeConnection) {
+ await printNativeFastTemplatePlugin({
+ deviceId: nativeConnection.deviceId,
+ deviceName: nativeConnection.deviceName,
+ template: TEST_PRINT_SYSTEM_TEMPLATE,
+ data: TEST_PRINT_TEMPLATE_DATA,
+ dpi: driver.imageDpi || 203,
+ printQty: 1,
+ })
+ if (onProgress) onProgress(100)
+ return driver
+ }
+ }
await sendToPrinter(driver.buildTestPrintData(), onProgress)
return driver
}
@@ -279,6 +318,26 @@ export async function printSystemTemplateForCurrentPrinter (
onProgress?: (percent: number) => void
): Promise {
const driver = getCurrentPrinterDriver()
+ const connection = getBluetoothConnection()
+ if (driver.protocol === 'tsc' && connection?.deviceType === 'classic' && !isNativeFastPrinterAvailable()) {
+ throw new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND. Please rebuild the custom base with native-fast-printer.')
+ }
+ if (canUseNativeFastTemplatePrint(driver)) {
+ const nativeConnection = getNativeClassicConnection()
+ if (nativeConnection) {
+ await printNativeFastTemplatePlugin({
+ deviceId: nativeConnection.deviceId,
+ deviceName: nativeConnection.deviceName,
+ template,
+ data,
+ dpi: driver.imageDpi || 203,
+ printQty: options.printQty || 1,
+ })
+ if (onProgress) onProgress(100)
+ return driver
+ }
+ }
+
const structuredTemplate = adaptSystemLabelTemplate(template, data, {
dpi: driver.imageDpi || 203,
printQty: options.printQty || 1,
@@ -301,6 +360,15 @@ export function disconnectCurrentPrinter (): Promise {
if (type === 'bluetooth' && connection?.deviceType === 'classic') {
// #ifdef APP-PLUS
+ if (isNativeFastPrinterAvailable()) {
+ disconnectNativeFastPrinterPlugin().catch((e: any) => {
+ console.error('Disconnect native fast printer failed', e)
+ }).finally(() => {
+ clearPrinter()
+ resolve()
+ })
+ return
+ }
try {
const classic = classicBluetooth
if (classic && classic.disConnDevice) classic.disConnDevice()
diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/nativeBitmapPatch.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/nativeBitmapPatch.ts
new file mode 100644
index 0000000..c27d915
--- /dev/null
+++ b/美国版/Food Labeling Management App UniApp/src/utils/print/nativeBitmapPatch.ts
@@ -0,0 +1,311 @@
+import type {
+ MonochromeImageData,
+ SystemTemplateElementBase,
+ SystemTemplateTextAlign,
+} from './types/printer'
+
+declare const plus: any
+
+const DESIGN_DPI = 96
+const DEFAULT_THRESHOLD = 180
+const TEXT_PADDING_DOTS = 6
+
+type BitmapPatchItem = {
+ type: 'bitmap'
+ x: number
+ y: number
+ image: MonochromeImageData
+}
+
+function clamp (value: number, min: number, max: number): number {
+ return Math.max(min, Math.min(max, Math.round(value)))
+}
+
+function ensureMultipleOf8 (value: number): number {
+ const safe = Math.max(8, Math.round(value || 0))
+ return safe % 8 === 0 ? safe : safe + (8 - (safe % 8))
+}
+
+function pxToDots (value: number, dpi: number): number {
+ return Math.max(0, Math.round((Number(value) || 0) * dpi / DESIGN_DPI))
+}
+
+function normalizeBase64Payload (source: string): string {
+ const value = String(source || '').trim()
+ if (!value) return ''
+ if (value.startsWith('data:image/')) {
+ const index = value.indexOf(',')
+ return index >= 0 ? value.slice(index + 1) : ''
+ }
+ if (/^[A-Za-z0-9+/=\r\n]+$/.test(value) && value.length > 128) {
+ return value.replace(/\s+/g, '')
+ }
+ return ''
+}
+
+function resolveLocalImagePath (source: string): string {
+ let path = String(source || '').trim()
+ if (!path) return ''
+ if (path.startsWith('file://')) {
+ path = path.replace(/^file:\/\//, '')
+ }
+ // #ifdef APP-PLUS
+ try {
+ const converted = plus.io.convertLocalFileSystemURL(path)
+ if (converted) path = converted
+ } catch (_) {}
+ // #endif
+ try {
+ path = decodeURIComponent(path)
+ } catch (_) {}
+ return path
+}
+
+function getAndroidGraphics () {
+ // #ifdef APP-PLUS
+ try {
+ if (typeof plus === 'undefined' || String(plus.os?.name || '').toLowerCase() !== 'android') return null
+ return {
+ Bitmap: plus.android.importClass('android.graphics.Bitmap'),
+ BitmapFactory: plus.android.importClass('android.graphics.BitmapFactory'),
+ BitmapConfig: plus.android.importClass('android.graphics.Bitmap$Config'),
+ Canvas: plus.android.importClass('android.graphics.Canvas'),
+ Paint: plus.android.importClass('android.graphics.Paint'),
+ Color: plus.android.importClass('android.graphics.Color'),
+ Typeface: plus.android.importClass('android.graphics.Typeface'),
+ Base64: plus.android.importClass('android.util.Base64'),
+ }
+ } catch (error) {
+ console.error('getAndroidGraphics failed', error)
+ return null
+ }
+ // #endif
+ // #ifndef APP-PLUS
+ return null
+ // #endif
+}
+
+function bitmapToMonochromeImage (
+ bitmap: any,
+ threshold = DEFAULT_THRESHOLD
+): MonochromeImageData {
+ const bitmapWidth = Number(bitmap.getWidth ? bitmap.getWidth() : 0)
+ const bitmapHeight = Number(bitmap.getHeight ? bitmap.getHeight() : 0)
+ const width = ensureMultipleOf8(bitmapWidth)
+ const height = Math.max(1, bitmapHeight)
+ const pixels: number[] = new Array(width * height).fill(0)
+
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ if (x >= bitmapWidth) {
+ pixels[y * width + x] = 0
+ continue
+ }
+ const color = Number(bitmap.getPixel(x, y))
+ const alpha = (color >>> 24) & 0xff
+ const red = (color >>> 16) & 0xff
+ const green = (color >>> 8) & 0xff
+ const blue = color & 0xff
+ const gray = red * 0.299 + green * 0.587 + blue * 0.114
+ pixels[y * width + x] = alpha <= 10 || gray > threshold ? 0 : 1
+ }
+ }
+
+ return { width, height, pixels }
+}
+
+function splitTextLines (text: string, paint: any, maxWidth: number): string[] {
+ const lines: string[] = []
+ const rawLines = String(text || '').replace(/\r/g, '').split('\n')
+
+ rawLines.forEach((segment) => {
+ if (!segment) {
+ lines.push('')
+ return
+ }
+
+ let current = ''
+ for (let i = 0; i < segment.length; i++) {
+ const char = segment.charAt(i)
+ const candidate = current + char
+ const measure = Number(paint.measureText(candidate))
+ if (current && measure > maxWidth) {
+ lines.push(current)
+ current = char
+ } else {
+ current = candidate
+ }
+ }
+ if (current || lines.length === 0) lines.push(current)
+ })
+
+ return lines.length > 0 ? lines : ['']
+}
+
+export function shouldRasterizeTextElement (text: string, type: string): boolean {
+ const normalizedType = String(type || '').toUpperCase()
+ if (!text) return false
+ if (normalizedType === 'TEXT_PRICE') return true
+ if (/[€£¥¥$éÉáàâäãåæçèêëìíîïñòóôöõøùúûüýÿœšž]/.test(text)) return true
+ return /[^\x20-\x7E]/.test(text)
+}
+
+export function createTextBitmapPatch (params: {
+ element: SystemTemplateElementBase
+ text: string
+ dpi: number
+ align: SystemTemplateTextAlign
+}): BitmapPatchItem | null {
+ const graphics = getAndroidGraphics()
+ if (!graphics) return null
+
+ const { element, text, dpi, align } = params
+ const config = element.config || {}
+ const Bitmap = graphics.Bitmap
+ const BitmapConfig = graphics.BitmapConfig
+ const Canvas = graphics.Canvas
+ const Paint = graphics.Paint
+ const Color = graphics.Color
+ const Typeface = graphics.Typeface
+
+ const contentWidth = Math.max(8, pxToDots(element.width, dpi))
+ const width = ensureMultipleOf8(contentWidth + TEXT_PADDING_DOTS * 2)
+ const height = Math.max(16, pxToDots(element.height, dpi) + TEXT_PADDING_DOTS * 2)
+ const bitmap = Bitmap.createBitmap(width, height, BitmapConfig.ARGB_8888)
+ const canvas = new Canvas(bitmap)
+ canvas.drawColor(Color.WHITE)
+
+ const paint = new Paint()
+ paint.setAntiAlias(true)
+ paint.setDither(true)
+ paint.setColor(Color.BLACK)
+ paint.setSubpixelText(true)
+ const fontSizeDots = Math.max(14, pxToDots(Number(config.fontSize || 14), dpi))
+ paint.setTextSize(fontSizeDots)
+ const isBold = String(config.fontWeight || '').toLowerCase() === 'bold' || String(element.type || '').toUpperCase() === 'TEXT_PRICE'
+ paint.setFakeBoldText(isBold)
+ paint.setTypeface(isBold ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT)
+
+ const maxTextWidth = Math.max(8, contentWidth)
+ const lines = splitTextLines(text, paint, maxTextWidth)
+ const fontMetrics = paint.getFontMetrics()
+ const lineHeight = Math.max(
+ fontSizeDots + 2,
+ Math.ceil(Math.abs(Number(fontMetrics.top)) + Math.abs(Number(fontMetrics.bottom)) + 2)
+ )
+ const totalHeight = lines.length * lineHeight
+ const isCenteredVertically = String(element.type || '').toUpperCase() === 'TEXT_PRICE'
+ const topOffset = isCenteredVertically
+ ? Math.max(TEXT_PADDING_DOTS, Math.floor((height - totalHeight) / 2))
+ : TEXT_PADDING_DOTS
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i]
+ const lineWidth = Number(paint.measureText(line))
+ let drawX = TEXT_PADDING_DOTS
+ if (align === 'center') {
+ drawX = TEXT_PADDING_DOTS + Math.max(0, Math.round((maxTextWidth - lineWidth) / 2))
+ } else if (align === 'right') {
+ drawX = TEXT_PADDING_DOTS + Math.max(0, Math.round(maxTextWidth - lineWidth))
+ }
+ const baseline = topOffset + i * lineHeight - Number(fontMetrics.top)
+ canvas.drawText(line, drawX, baseline, paint)
+ }
+
+ const image = bitmapToMonochromeImage(bitmap)
+ try {
+ bitmap.recycle && bitmap.recycle()
+ } catch (_) {}
+
+ return {
+ type: 'bitmap',
+ x: Math.max(0, pxToDots(element.x, dpi) - TEXT_PADDING_DOTS),
+ y: Math.max(0, pxToDots(element.y, dpi) - TEXT_PADDING_DOTS),
+ image,
+ }
+}
+
+function decodeSourceBitmap (source: string, graphics: ReturnType) {
+ if (!graphics) return null
+ const rawSource = String(source || '').trim()
+ if (!rawSource) return null
+
+ const base64Payload = normalizeBase64Payload(rawSource)
+ if (base64Payload) {
+ const bytes = graphics.Base64.decode(base64Payload, 0)
+ return graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.length)
+ }
+
+ const localPath = resolveLocalImagePath(rawSource)
+ if (!localPath) return null
+ return graphics.BitmapFactory.decodeFile(localPath)
+}
+
+export function createImageBitmapPatch (params: {
+ element: SystemTemplateElementBase
+ dpi: number
+}): BitmapPatchItem | null {
+ const graphics = getAndroidGraphics()
+ if (!graphics) return null
+
+ const { element, dpi } = params
+ const config = element.config || {}
+ const sourceBitmap = decodeSourceBitmap(String(config.src || config.data || config.url || ''), graphics)
+ if (!sourceBitmap) return null
+
+ const Bitmap = graphics.Bitmap
+ const BitmapConfig = graphics.BitmapConfig
+ const Canvas = graphics.Canvas
+ const Paint = graphics.Paint
+ const Color = graphics.Color
+
+ const width = ensureMultipleOf8(Math.max(8, pxToDots(element.width, dpi)))
+ const height = Math.max(8, pxToDots(element.height, dpi))
+ const outputBitmap = Bitmap.createBitmap(width, height, BitmapConfig.ARGB_8888)
+ const canvas = new Canvas(outputBitmap)
+ canvas.drawColor(Color.WHITE)
+
+ const sourceWidth = Number(sourceBitmap.getWidth ? sourceBitmap.getWidth() : 0)
+ const sourceHeight = Number(sourceBitmap.getHeight ? sourceBitmap.getHeight() : 0)
+ const scaleMode = String(config.scaleMode || 'contain').toLowerCase()
+
+ let targetWidth = width
+ let targetHeight = height
+ let targetLeft = 0
+ let targetTop = 0
+
+ if (sourceWidth > 0 && sourceHeight > 0 && scaleMode !== 'fill') {
+ const ratio = scaleMode === 'cover'
+ ? Math.max(width / sourceWidth, height / sourceHeight)
+ : Math.min(width / sourceWidth, height / sourceHeight)
+ targetWidth = Math.max(1, Math.round(sourceWidth * ratio))
+ targetHeight = Math.max(1, Math.round(sourceHeight * ratio))
+ targetLeft = Math.round((width - targetWidth) / 2)
+ targetTop = Math.round((height - targetHeight) / 2)
+ }
+
+ const scaledBitmap = Bitmap.createScaledBitmap(sourceBitmap, targetWidth, targetHeight, true)
+ const paint = new Paint()
+ paint.setAntiAlias(true)
+ paint.setFilterBitmap(true)
+ canvas.drawBitmap(scaledBitmap, targetLeft, targetTop, paint)
+
+ const image = bitmapToMonochromeImage(outputBitmap, Number(config.threshold || DEFAULT_THRESHOLD))
+
+ try {
+ scaledBitmap?.recycle && scaledBitmap.recycle()
+ } catch (_) {}
+ try {
+ sourceBitmap?.recycle && sourceBitmap.recycle()
+ } catch (_) {}
+ try {
+ outputBitmap?.recycle && outputBitmap.recycle()
+ } catch (_) {}
+
+ return {
+ type: 'bitmap',
+ x: pxToDots(element.x, dpi),
+ y: pxToDots(element.y, dpi),
+ image,
+ }
+}
diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/nativeFastPrinter.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/nativeFastPrinter.ts
new file mode 100644
index 0000000..1fe229f
--- /dev/null
+++ b/美国版/Food Labeling Management App UniApp/src/utils/print/nativeFastPrinter.ts
@@ -0,0 +1,262 @@
+import type { LabelTemplateData, SystemLabelTemplate } from './types/printer'
+
+type NativePrinterResult = {
+ code?: number
+ msg?: string
+ errMsg?: string
+ connected?: boolean
+ deviceId?: string
+ deviceName?: string
+ success?: boolean
+ backend?: string
+ pluginVersion?: string
+ stage?: string
+ lastError?: string
+ buildMs?: number
+ writeMs?: number
+ commandBytes?: number
+ lastPrintAt?: number
+ nativeTextCount?: number
+ rasterTextCount?: number
+ qrCodeCount?: number
+ barcodeCount?: number
+ imagePatchCount?: number
+ lineCount?: number
+ elementCount?: number
+ available?: boolean
+ lastAction?: string
+}
+
+const nativeFastPrinterState: NativePrinterResult = {
+ available: false,
+ lastAction: 'idle',
+}
+
+function getUniApi (): any {
+ return uni as any
+}
+
+function parsePluginResult (payload: any): NativePrinterResult {
+ if (!payload) return {}
+ if (typeof payload === 'string') {
+ try {
+ return JSON.parse(payload)
+ } catch (_) {
+ return { msg: payload }
+ }
+ }
+ return payload as NativePrinterResult
+}
+
+function updateNativeState (patch: NativePrinterResult) {
+ Object.assign(nativeFastPrinterState, patch, {
+ available: isNativeFastPrinterAvailable(),
+ })
+}
+
+function getNativePlugin (): any | null {
+ // #ifdef APP-PLUS
+ try {
+ const api = getUniApi()
+ if (typeof api.requireNativePlugin !== 'function') return null
+ const plugin = api.requireNativePlugin('native-fast-printer')
+ return plugin || null
+ } catch (_) {
+ return null
+ }
+ // #endif
+ // #ifndef APP-PLUS
+ return null
+ // #endif
+}
+
+function ensureNativePlugin (): any {
+ const plugin = getNativePlugin()
+ if (!plugin) {
+ updateNativeState({
+ available: false,
+ lastAction: 'plugin:missing',
+ lastError: 'NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND',
+ })
+ throw new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND')
+ }
+ return plugin
+}
+
+export function isNativeFastPrinterAvailable (): boolean {
+ const plugin = getNativePlugin()
+ return !!plugin
+ && typeof plugin.connect === 'function'
+ && typeof plugin.printTemplate === 'function'
+}
+
+export function getNativeFastPrinterState (): NativePrinterResult | null {
+ return {
+ ...nativeFastPrinterState,
+ available: isNativeFastPrinterAvailable(),
+ }
+}
+
+function buildTimeoutError (action: string, timeoutMs: number): Error {
+ const snapshot = getNativeFastPrinterState()
+ const detail = [
+ `action=${action}`,
+ `timeout=${Math.round(timeoutMs / 1000)}s`,
+ snapshot?.backend ? `backend=${snapshot.backend}` : '',
+ snapshot?.stage ? `stage=${snapshot.stage}` : '',
+ snapshot?.commandBytes ? `commandBytes=${snapshot.commandBytes}` : '',
+ snapshot?.lastError ? `lastError=${snapshot.lastError}` : '',
+ ].filter(Boolean).join('\n')
+ return new Error(`Native printer timeout.\n${detail}`.trim())
+}
+
+function wrapCallback (
+ action: string,
+ timeoutMs: number,
+ executor: (resolve: (value: NativePrinterResult) => void, reject: (reason?: any) => void) => void
+) {
+ return new Promise((resolve, reject) => {
+ let settled = false
+ const timer = setTimeout(() => {
+ if (settled) return
+ settled = true
+ updateNativeState({
+ lastAction: `${action}:timeout`,
+ })
+ reject(buildTimeoutError(action, timeoutMs))
+ }, timeoutMs)
+
+ const done = (handler: () => void) => {
+ if (settled) return
+ settled = true
+ clearTimeout(timer)
+ handler()
+ }
+
+ executor(
+ (value) => done(() => resolve(value)),
+ (reason) => done(() => reject(reason)),
+ )
+ })
+}
+
+export function getNativeFastPrinterDebugInfo () {
+ return wrapCallback('getDebugInfo', 5000, (resolve, reject) => {
+ try {
+ const nativePlugin = ensureNativePlugin()
+ if (typeof nativePlugin.getDebugInfo !== 'function') {
+ const snapshot = getNativeFastPrinterState()
+ resolve(snapshot || {})
+ return
+ }
+ nativePlugin.getDebugInfo((payload: any) => {
+ const res = parsePluginResult(payload)
+ updateNativeState({
+ ...res,
+ lastAction: 'getDebugInfo',
+ })
+ resolve(res)
+ })
+ } catch (error: any) {
+ reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_FAST_PRINTER_DEBUG_FAILED')))
+ }
+ })
+}
+
+export function connectNativeFastPrinter (options: {
+ deviceId: string
+ deviceName?: string
+}) {
+ return wrapCallback('connect', 12000, (resolve, reject) => {
+ try {
+ const nativePlugin = ensureNativePlugin()
+ if (typeof nativePlugin.connect !== 'function') {
+ reject(new Error('NATIVE_FAST_PRINTER_CONNECT_METHOD_NOT_FOUND'))
+ return
+ }
+ nativePlugin.connect({
+ deviceId: options.deviceId,
+ deviceName: options.deviceName || '',
+ }, (payload: any) => {
+ const res = parsePluginResult(payload)
+ updateNativeState({
+ ...res,
+ lastAction: 'connect',
+ })
+ if (res.code === 1 || res.success === true) {
+ resolve(res)
+ return
+ }
+ reject(new Error(res.msg || res.errMsg || 'NATIVE_FAST_PRINTER_CONNECT_FAILED'))
+ })
+ } catch (error: any) {
+ reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_FAST_PRINTER_CONNECT_FAILED')))
+ }
+ })
+}
+
+export function disconnectNativeFastPrinter () {
+ return wrapCallback('disconnect', 8000, (resolve, reject) => {
+ try {
+ const nativePlugin = ensureNativePlugin()
+ if (typeof nativePlugin.disconnect !== 'function') {
+ reject(new Error('NATIVE_FAST_PRINTER_DISCONNECT_METHOD_NOT_FOUND'))
+ return
+ }
+ nativePlugin.disconnect((payload: any) => {
+ const res = parsePluginResult(payload)
+ updateNativeState({
+ ...res,
+ lastAction: 'disconnect',
+ })
+ if (res.code === 1 || res.success === true) {
+ resolve(res)
+ return
+ }
+ reject(new Error(res.msg || res.errMsg || 'NATIVE_FAST_PRINTER_DISCONNECT_FAILED'))
+ })
+ } catch (error: any) {
+ reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_FAST_PRINTER_DISCONNECT_FAILED')))
+ }
+ })
+}
+
+export function printNativeFastTemplate (options: {
+ deviceId: string
+ deviceName?: string
+ template: SystemLabelTemplate
+ data?: LabelTemplateData
+ dpi?: number
+ printQty?: number
+}) {
+ return wrapCallback('printTemplate', 20000, (resolve, reject) => {
+ try {
+ const nativePlugin = ensureNativePlugin()
+ if (typeof nativePlugin.printTemplate !== 'function') {
+ reject(new Error('NATIVE_FAST_PRINTER_PRINT_METHOD_NOT_FOUND'))
+ return
+ }
+ nativePlugin.printTemplate({
+ deviceId: options.deviceId,
+ deviceName: options.deviceName || '',
+ templateJson: JSON.stringify(options.template),
+ dataJson: JSON.stringify(options.data || {}),
+ dpi: options.dpi || 203,
+ printQty: options.printQty || 1,
+ }, (raw: any) => {
+ const res = parsePluginResult(raw)
+ updateNativeState({
+ ...res,
+ lastAction: 'printTemplate',
+ })
+ if (res.code === 1 || res.success === true) {
+ resolve(res)
+ return
+ }
+ reject(new Error(res.msg || res.errMsg || 'NATIVE_FAST_PRINTER_PRINT_FAILED'))
+ })
+ } catch (error: any) {
+ reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_FAST_PRINTER_PRINT_FAILED')))
+ }
+ })
+}
diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts
index 3deb609..499a180 100644
--- a/美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts
+++ b/美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts
@@ -8,27 +8,61 @@ import type {
import { resolveEscTemplate } from '../templateRenderer'
import { createTestPrintTemplate } from '../templates/testPrintTemplate'
+function normalizePrinterText (str: string): string {
+ return String(str || '')
+ .normalize('NFKC')
+ .replace(/[\u2018\u2019]/g, '\'')
+ .replace(/[\u201C\u201D]/g, '"')
+ .replace(/[\u2013\u2014]/g, '-')
+}
+
function stringToBytes (str: string): number[] {
+ const normalized = normalizePrinterText(str)
const out: number[] = []
- for (let i = 0; i < str.length; i++) {
- let c = str.charCodeAt(i)
- if (c < 0x80) {
- out.push(c)
- } else if (c < 0x800) {
- out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f))
- } else if (c < 0xd800 || c >= 0xe000) {
- out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f))
- } else {
- i++
- const c2 = str.charCodeAt(i)
- const u = ((c & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000
- out.push(
- 0xf0 | (u >> 18),
- 0x80 | ((u >> 12) & 0x3f),
- 0x80 | ((u >> 6) & 0x3f),
- 0x80 | (u & 0x3f)
- )
+ const cp1252Map: Record = {
+ 0x20ac: 0x80,
+ 0x201a: 0x82,
+ 0x0192: 0x83,
+ 0x201e: 0x84,
+ 0x2026: 0x85,
+ 0x2020: 0x86,
+ 0x2021: 0x87,
+ 0x02c6: 0x88,
+ 0x2030: 0x89,
+ 0x0160: 0x8a,
+ 0x2039: 0x8b,
+ 0x0152: 0x8c,
+ 0x017d: 0x8e,
+ 0x2018: 0x91,
+ 0x2019: 0x92,
+ 0x201c: 0x93,
+ 0x201d: 0x94,
+ 0x2022: 0x95,
+ 0x2013: 0x96,
+ 0x2014: 0x97,
+ 0x02dc: 0x98,
+ 0x2122: 0x99,
+ 0x0161: 0x9a,
+ 0x203a: 0x9b,
+ 0x0153: 0x9c,
+ 0x017e: 0x9e,
+ 0x0178: 0x9f,
+ }
+ for (let i = 0; i < normalized.length; i++) {
+ const code = normalized.charCodeAt(i)
+ if (code < 0x80) {
+ out.push(code)
+ continue
+ }
+ if (code >= 0xa0 && code <= 0xff) {
+ out.push(code)
+ continue
+ }
+ if (cp1252Map[code] != null) {
+ out.push(cp1252Map[code])
+ continue
}
+ out.push(0x3f)
}
return out
}
@@ -104,6 +138,7 @@ function appendBoxLine (out: number[], text = '', width = 32) {
function createEscDocument (builder: (out: number[]) => void): number[] {
const out: number[] = []
out.push(0x1b, 0x40)
+ out.push(0x1b, 0x74, 16)
builder(out)
out.push(0x1b, 0x64, 0x04)
return out
diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts
index 51cbda0..093b7ff 100644
--- a/美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts
+++ b/美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts
@@ -1,3 +1,8 @@
+import {
+ createImageBitmapPatch,
+ createTextBitmapPatch,
+ shouldRasterizeTextElement,
+} from './nativeBitmapPatch'
import { applyTemplateData } from './templateRenderer'
import type {
EscTemplateItem,
@@ -119,13 +124,34 @@ function resolveTemplateFieldValue (data: LabelTemplateData, key: string): strin
return ''
}
+function formatPriceValue (
+ rawValue: string,
+ config: Record
+): string {
+ const prefix = getConfigString(config, ['prefix'], '')
+ const suffix = getConfigString(config, ['suffix'], '')
+ const decimal = getConfigNumber(config, ['decimal'], -1)
+ const numericValue = Number(rawValue)
+ const value = !Number.isNaN(numericValue) && Number.isFinite(numericValue) && decimal >= 0
+ ? numericValue.toFixed(decimal)
+ : rawValue
+ return `${prefix}${value}${suffix}`
+}
+
function resolveElementText (
element: SystemTemplateElementBase,
data: LabelTemplateData
): string {
const config = element.config || {}
+ const type = String(element.type || '').toUpperCase()
const hasText = config.text != null && config.text !== ''
- if (hasText && String(element.type || '').toUpperCase() === 'TEXT_STATIC') {
+ if (type === 'TEXT_PRICE') {
+ const bindingKey = resolveBindingKey(element)
+ const boundValue = resolveTemplateFieldValue(data, bindingKey)
+ const baseValue = boundValue || (hasText ? applyTemplateData(String(config.text), data) : '')
+ return baseValue ? formatPriceValue(baseValue, config) : ''
+ }
+ if (hasText && type === 'TEXT_STATIC') {
return applyTemplateData(String(config.text), data)
}
if (hasText && String(config.text).includes('{{')) {
@@ -191,6 +217,40 @@ function resolveTextScale (fontSizePx: number, dpi: number): number {
return clamp(targetDots / 24, 1, 7)
}
+function estimateQrModuleCount (value: string, level: 'L' | 'M' | 'Q' | 'H'): number {
+ const capacities: Record<'L' | 'M' | 'Q' | 'H', number[]> = {
+ L: [17, 32, 53, 78, 106, 134, 154, 192, 230, 271],
+ M: [14, 26, 42, 62, 84, 106, 122, 152, 180, 213],
+ Q: [11, 20, 32, 46, 60, 74, 86, 108, 130, 151],
+ H: [7, 14, 24, 34, 44, 58, 64, 84, 98, 119],
+ }
+ const length = Math.max(1, String(value || '').length)
+ const versions = capacities[level] || capacities.M
+ let version = versions.length
+ for (let i = 0; i < versions.length; i++) {
+ if (length <= versions[i]) {
+ version = i + 1
+ break
+ }
+ }
+ return 21 + (version - 1) * 4
+}
+
+function resolveQrModuleSize (
+ widthPx: number,
+ heightPx: number,
+ dpi: number,
+ value: string,
+ level: 'L' | 'M' | 'Q' | 'H'
+): number {
+ const targetDots = Math.max(24, Math.min(
+ pxToDots(widthPx, dpi),
+ pxToDots(heightPx, dpi)
+ ))
+ const moduleCount = Math.max(21, estimateQrModuleCount(value, level))
+ return clamp(Math.floor(targetDots / moduleCount), 3, 12)
+}
+
function resolveTextX (params: {
align: SystemTemplateTextAlign
xPx: number
@@ -221,6 +281,8 @@ function buildTscTemplate (
const heightMm = roundNumber(toMillimeter(template.height, template.unit || 'inch'))
const items: TscTemplateItem[] = []
+ const pageWidth = templateWidthPx(template)
+
sortElements(template.elements).forEach((element) => {
const config = element.config || {}
const type = String(element.type || '').toUpperCase()
@@ -229,7 +291,21 @@ function buildTscTemplate (
const text = resolveElementText(element, data)
if (!text) return
const scale = resolveTextScale(getConfigNumber(config, ['fontSize'], 14), dpi)
- const align = resolveElementAlign(element, templateWidthPx(template))
+ const align = resolveElementAlign(element, pageWidth)
+
+ if (shouldRasterizeTextElement(text, type)) {
+ const bitmapPatch = createTextBitmapPatch({
+ element,
+ text,
+ dpi,
+ align,
+ })
+ if (bitmapPatch) {
+ items.push(bitmapPatch)
+ return
+ }
+ }
+
items.push({
type: 'text',
x: resolveTextX({
@@ -253,13 +329,14 @@ function buildTscTemplate (
if (type === 'QRCODE') {
const value = resolveElementDataValue(element, data)
if (!value) return
+ const level = normalizeQrLevel(getConfigString(config, ['errorLevel'], 'M'))
items.push({
type: 'qrcode',
x: pxToDots(element.x, dpi),
y: pxToDots(element.y, dpi),
value,
- level: normalizeQrLevel(getConfigString(config, ['errorLevel'], 'M')),
- cellWidth: clamp(Math.min(element.width, element.height) / 20, 2, 10),
+ level,
+ cellWidth: resolveQrModuleSize(element.width, element.height, dpi, value, level),
mode: 'A',
})
return
@@ -280,6 +357,26 @@ function buildTscTemplate (
narrow: clamp(element.width / Math.max(40, value.length * 6), 1, 4),
wide: clamp(element.width / Math.max(24, value.length * 3), 2, 6),
})
+ return
+ }
+
+ if (type === 'IMAGE') {
+ const bitmapPatch = createImageBitmapPatch({
+ element,
+ dpi,
+ })
+ if (bitmapPatch) items.push(bitmapPatch)
+ return
+ }
+
+ if (type === 'BLANK' && String(element.border || '').toLowerCase() === 'line') {
+ items.push({
+ type: 'bar',
+ x: pxToDots(element.x, dpi),
+ y: pxToDots(element.y, dpi),
+ width: Math.max(1, pxToDots(element.width, dpi)),
+ height: Math.max(1, pxToDots(element.height || 1, dpi)),
+ })
}
})
@@ -326,12 +423,13 @@ function buildEscTemplate (
if (type === 'QRCODE') {
const value = resolveElementDataValue(element, data)
if (!value) return
+ const level = normalizeQrLevel(getConfigString(config, ['errorLevel'], 'M'))
items.push({
type: 'qrcode',
value,
align,
- size: clamp(Math.min(element.width, element.height) / 24, 3, 10),
- level: normalizeQrLevel(getConfigString(config, ['errorLevel'], 'M')),
+ size: resolveQrModuleSize(element.width, element.height, 203, value, level),
+ level,
})
return
}
@@ -348,6 +446,14 @@ function buildEscTemplate (
width: clamp(element.width / Math.max(48, value.length * 4), 2, 6),
showText: config.showText !== false,
})
+ return
+ }
+
+ if (type === 'BLANK' && String(element.border || '').toLowerCase() === 'line') {
+ items.push({
+ type: 'rule',
+ width: clamp(element.width / 8, 8, 48),
+ })
}
})
diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/templates/test.json b/美国版/Food Labeling Management App UniApp/src/utils/print/templates/test.json
index 3692484..c41f163 100644
--- a/美国版/Food Labeling Management App UniApp/src/utils/print/templates/test.json
+++ b/美国版/Food Labeling Management App UniApp/src/utils/print/templates/test.json
@@ -1,42 +1,103 @@
{
- "id": "template-1773998862063",
+ "id": "template-1772158111858",
"name": "未命名模板",
"labelType": "PRICE",
"unit": "inch",
- "width": 4,
- "height": 2,
+ "width": 3,
+ "height": 6,
"appliedLocation": "ALL",
"showRuler": true,
- "showGrid": true,
+ "showGrid": false,
"elements": [
{
- "id": "el-1773998886036-34sylni",
+ "id": "el-1774011780062-pxyqycw",
"type": "TEXT_STATIC",
- "x": 104,
- "y": 16,
+ "x": 80,
+ "y": 32,
"width": 120,
"height": 24,
"rotation": "horizontal",
"border": "none",
"config": {
- "text": "文本",
+ "text": "Tasty Café",
"fontFamily": "Arial",
- "fontSize": 14,
+ "fontSize": 20,
"fontWeight": "normal",
"textAlign": "center"
}
},
{
- "id": "el-1773998909568-4jjwdx7",
+ "id": "el-1774011806412-ctz9hgl",
+ "type": "QRCODE",
+ "x": 64,
+ "y": 80,
+ "width": 160,
+ "height": 128,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "data": "https://example.com",
+ "errorLevel": "M"
+ }
+ },
+ {
+ "id": "el-1775020000000-imgtest",
+ "type": "IMAGE",
+ "x": 216,
+ "y": 96,
+ "width": 56,
+ "height": 56,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAAEi6oPRAAAKQ2lDQ1BJQ0MgcHJvZmlsZQAAeNqdU3dYk/cWPt/3ZQ9WQtjwsZdsgQAiI6wIyBBZohCSAGGEEBJAxYWIClYUFRGcSFXEgtUKSJ2I4qAouGdBiohai1VcOO4f3Ke1fXrv7e371/u855zn/M55zw+AERImkeaiagA5UoU8Otgfj09IxMm9gAIVSOAEIBDmy8JnBcUAAPADeXh+dLA//AGvbwACAHDVLiQSx+H/g7pQJlcAIJEA4CIS5wsBkFIAyC5UyBQAyBgAsFOzZAoAlAAAbHl8QiIAqg0A7PRJPgUA2KmT3BcA2KIcqQgAjQEAmShHJAJAuwBgVYFSLALAwgCgrEAiLgTArgGAWbYyRwKAvQUAdo5YkA9AYACAmUIszAAgOAIAQx4TzQMgTAOgMNK/4KlfcIW4SAEAwMuVzZdL0jMUuJXQGnfy8ODiIeLCbLFCYRcpEGYJ5CKcl5sjE0jnA0zODAAAGvnRwf44P5Dn5uTh5mbnbO/0xaL+a/BvIj4h8d/+vIwCBAAQTs/v2l/l5dYDcMcBsHW/a6lbANpWAGjf+V0z2wmgWgrQevmLeTj8QB6eoVDIPB0cCgsL7SViob0w44s+/zPhb+CLfvb8QB7+23rwAHGaQJmtwKOD/XFhbnauUo7nywRCMW735yP+x4V//Y4p0eI0sVwsFYrxWIm4UCJNx3m5UpFEIcmV4hLpfzLxH5b9CZN3DQCshk/ATrYHtctswH7uAQKLDljSdgBAfvMtjBoLkQAQZzQyefcAAJO/+Y9AKwEAzZek4wAAvOgYXKiUF0zGCAAARKCBKrBBBwzBFKzADpzBHbzAFwJhBkRADCTAPBBCBuSAHAqhGJZBGVTAOtgEtbADGqARmuEQtMExOA3n4BJcgetwFwZgGJ7CGLyGCQRByAgTYSE6iBFijtgizggXmY4EImFINJKApCDpiBRRIsXIcqQCqUJqkV1II/ItchQ5jVxA+pDbyCAyivyKvEcxlIGyUQPUAnVAuagfGorGoHPRdDQPXYCWomvRGrQePYC2oqfRS+h1dAB9io5jgNExDmaM2WFcjIdFYIlYGibHFmPlWDVWjzVjHVg3dhUbwJ5h7wgkAouAE+wIXoQQwmyCkJBHWExYQ6gl7CO0EroIVwmDhDHCJyKTqE+0JXoS+cR4YjqxkFhGrCbuIR4hniVeJw4TX5NIJA7JkuROCiElkDJJC0lrSNtILaRTpD7SEGmcTCbrkG3J3uQIsoCsIJeRt5APkE+S+8nD5LcUOsWI4kwJoiRSpJQSSjVlP+UEpZ8yQpmgqlHNqZ7UCKqIOp9aSW2gdlAvU4epEzR1miXNmxZDy6Qto9XQmmlnafdoL+l0ugndgx5Fl9CX0mvoB+nn6YP0dwwNhg2Dx0hiKBlrGXsZpxi3GS+ZTKYF05eZyFQw1zIbmWeYD5hvVVgq9ip8FZHKEpU6lVaVfpXnqlRVc1U/1XmqC1SrVQ+rXlZ9pkZVs1DjqQnUFqvVqR1Vu6k2rs5Sd1KPUM9RX6O+X/2C+mMNsoaFRqCGSKNUY7fGGY0hFsYyZfFYQtZyVgPrLGuYTWJbsvnsTHYF+xt2L3tMU0NzqmasZpFmneZxzQEOxrHg8DnZnErOIc4NznstAy0/LbHWaq1mrX6tN9p62r7aYu1y7Rbt69rvdXCdQJ0snfU6bTr3dQm6NrpRuoW623XP6j7TY+t56Qn1yvUO6d3RR/Vt9KP1F+rv1u/RHzcwNAg2kBlsMThj8MyQY+hrmGm40fCE4agRy2i6kcRoo9FJoye4Ju6HZ+M1eBc+ZqxvHGKsNN5l3Gs8YWJpMtukxKTF5L4pzZRrmma60bTTdMzMyCzcrNisyeyOOdWca55hvtm82/yNhaVFnMVKizaLx5balnzLBZZNlvesmFY+VnlW9VbXrEnWXOss623WV2xQG1ebDJs6m8u2qK2brcR2m23fFOIUjynSKfVTbtox7PzsCuya7AbtOfZh9iX2bfbPHcwcEh3WO3Q7fHJ0dcx2bHC866ThNMOpxKnD6VdnG2ehc53zNRemS5DLEpd2lxdTbaeKp26fesuV5RruutK10/Wjm7ub3K3ZbdTdzD3Ffav7TS6bG8ldwz3vQfTw91jicczjnaebp8LzkOcvXnZeWV77vR5Ps5wmntYwbcjbxFvgvct7YDo+PWX6zukDPsY+Ap96n4e+pr4i3z2+I37Wfpl+B/ye+zv6y/2P+L/hefIW8U4FYAHBAeUBvYEagbMDawMfBJkEpQc1BY0FuwYvDD4VQgwJDVkfcpNvwBfyG/ljM9xnLJrRFcoInRVaG/owzCZMHtYRjobPCN8Qfm+m+UzpzLYIiOBHbIi4H2kZmRf5fRQpKjKqLupRtFN0cXT3LNas5Fn7Z72O8Y+pjLk722q2cnZnrGpsUmxj7Ju4gLiquIF4h/hF8ZcSdBMkCe2J5MTYxD2J43MC52yaM5zkmlSWdGOu5dyiuRfm6c7Lnnc8WTVZkHw4hZgSl7I/5YMgQlAvGE/lp25NHRPyhJuFT0W+oo2iUbG3uEo8kuadVpX2ON07fUP6aIZPRnXGMwlPUit5kRmSuSPzTVZE1t6sz9lx2S05lJyUnKNSDWmWtCvXMLcot09mKyuTDeR55m3KG5OHyvfkI/lz89sVbIVM0aO0Uq5QDhZML6greFsYW3i4SL1IWtQz32b+6vkjC4IWfL2QsFC4sLPYuHhZ8eAiv0W7FiOLUxd3LjFdUrpkeGnw0n3LaMuylv1Q4lhSVfJqedzyjlKD0qWlQyuCVzSVqZTJy26u9Fq5YxVhlWRV72qX1VtWfyoXlV+scKyorviwRrjm4ldOX9V89Xlt2treSrfK7etI66Trbqz3Wb+vSr1qQdXQhvANrRvxjeUbX21K3nShemr1js20zcrNAzVhNe1bzLas2/KhNqP2ep1/XctW/a2rt77ZJtrWv913e/MOgx0VO97vlOy8tSt4V2u9RX31btLugt2PGmIbur/mft24R3dPxZ6Pe6V7B/ZF7+tqdG9s3K+/v7IJbVI2jR5IOnDlm4Bv2pvtmne1cFoqDsJB5cEn36Z8e+NQ6KHOw9zDzd+Zf7f1COtIeSvSOr91rC2jbaA9ob3v6IyjnR1eHUe+t/9+7zHjY3XHNY9XnqCdKD3x+eSCk+OnZKeenU4/PdSZ3Hn3TPyZa11RXb1nQ8+ePxd07ky3X/fJ897nj13wvHD0Ivdi2yW3S609rj1HfnD94UivW2/rZffL7Vc8rnT0Tes70e/Tf/pqwNVz1/jXLl2feb3vxuwbt24m3Ry4Jbr1+Hb27Rd3Cu5M3F16j3iv/L7a/eoH+g/qf7T+sWXAbeD4YMBgz8NZD+8OCYee/pT/04fh0kfMR9UjRiONj50fHxsNGr3yZM6T4aeypxPPyn5W/3nrc6vn3/3i+0vPWPzY8Av5i8+/rnmp83Lvq6mvOscjxx+8znk98ab8rc7bfe+477rfx70fmSj8QP5Q89H6Y8en0E/3Pud8/vwv94Tz+4A5JREAAAAZdEVYdFNvZnR3YXJlAEFkb2JlIEltYWdlUmVhZHlxyWU8AAADKmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxMzIgNzkuMTU5Mjg0LCAyMDE2LzA0LzE5LTEzOjEzOjQwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGRkE0MjcxNTdEQzYxMUU4QkZBOERDOEVCQ0U0NTBGMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGRkE0MjcxNDdEQzYxMUU4QkZBOERDOEVCQ0U0NTBGMSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNS41IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QkE4RkFCN0M3REM1MTFFOEJGQThEQzhFQkNFNDUwRjEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QkE4RkFCN0Q3REM1MTFFOEJGQThEQzhFQkNFNDUwRjEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5BZZ+3AAAB1ElEQVR42mJkAALtmZb/GfAAJkIKwIoYiAA4FV1JO0Ylk0hWxILLHTgV6cyywqoIIIAYiQinb8S4iYs036E7esgEJq6ABAGAACImMBmo5m6yDcLlR5gcNnnaumhADWIhJoOTbRC+9ILPa9+o4TWAAAIlyDVAOphCc1SYqGAICNwZxumIidi8NILz2qhBdCyPaOcicgq1wRnYAAFErRKSgZo+GzSOoWpQD1sHsRCjCDnzkpp90DM+If2jUTbqoFEHjZZDpJYroyFESeNmNFHTykEqg8g9bwACCNRiVAYyLgEx1wA7Zu3V9OMhVBt1opajBlsaCh7NZaMOGnXQgFeupHZjKO1CjUbZqINGHTTqoFEHjTpo1EGjDhqMgw342kejUTaahggpoOdg1WiUjTpoODoIvL7tzSBykB5AgPbtGIdBGIYCaBR16swROEQvzT06cxjm1lRFDC0LcpXC+xJzpIdJhOW8e4z359MVWSde1C32xRYasC0mCmascDZzrQz+7NgABAgQINnRY/iUrb5D9v9l9toqCBAgQIAAAQIESAABAgQIEKCD5ZK9QPaMigoCdIJP7NdjOyoIECBAgGQBGjB8zVDjam153T0OqInJbBAWfdg8AExKZVcA71uIAAAAAElFTkSuQmCC",
+ "scaleMode": "contain"
+ }
+ },
+ {
+ "id": "el-1774011847653-m8svjm1",
+ "type": "BARCODE",
+ "x": 56,
+ "y": 496,
+ "width": 160,
+ "height": 48,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "barcodeType": "CODE128",
+ "data": "18180817871",
+ "showText": true,
+ "orientation": "horizontal"
+ }
+ },
+ {
+ "id": "el-1774011878312-5yo0csi",
"type": "TEXT_PRODUCT",
- "x": 96,
- "y": 128,
+ "x": 8,
+ "y": 264,
+ "width": 272,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "Cheese Burger Deluxe",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
+ }
+ },
+ {
+ "id": "el-1774011967712-m0a2aoy",
+ "type": "TEXT_STATIC",
+ "x": 8,
+ "y": 296,
"width": 120,
"height": 24,
"rotation": "horizontal",
"border": "none",
"config": {
- "text": "商品名",
+ "text": "Ingredients:",
"fontFamily": "Arial",
"fontSize": 14,
"fontWeight": "normal",
@@ -44,16 +105,16 @@
}
},
{
- "id": "el-1773998913096-cgabpx1",
+ "id": "el-1774011987154-jzx6iih",
"type": "TEXT_STATIC",
"x": 88,
- "y": 152,
- "width": 120,
+ "y": 296,
+ "width": 192,
"height": 24,
"rotation": "horizontal",
"border": "none",
"config": {
- "text": "文本",
+ "text": "Cheese, Lettuce, Tomato, ",
"fontFamily": "Arial",
"fontSize": 14,
"fontWeight": "normal",
@@ -61,34 +122,141 @@
}
},
{
- "id": "el-1773999052674-uzocw1j",
- "type": "QRCODE",
- "x": 128,
- "y": 40,
- "width": 80,
- "height": 80,
+ "id": "el-1774012011693-321yub6",
+ "type": "TEXT_STATIC",
+ "x": 8,
+ "y": 328,
+ "width": 256,
+ "height": 24,
"rotation": "horizontal",
"border": "none",
"config": {
- "data": "12341千问请问抛弃我",
- "errorLevel": "M"
+ "text": "Beef Pattie, Cucumber.",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
}
},
{
- "id": "el-1773999078958-5tgoru7",
- "type": "BARCODE",
- "x": 208,
- "y": 128,
+ "id": "el-1774012015804-ohlfmqy",
+ "type": "TEXT_STATIC",
+ "x": 8,
+ "y": 360,
+ "width": 232,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "Allergens: Milk, Nuts",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
+ }
+ },
+ {
+ "id": "el-1774012061462-u3cyixy",
+ "type": "TEXT_STATIC",
+ "x": 8,
+ "y": 392,
+ "width": 216,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "Preped On: 11/12/25",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
+ }
+ },
+ {
+ "id": "el-1774012067221-78hzefs",
+ "type": "TEXT_STATIC",
+ "x": 8,
+ "y": 424,
+ "width": 192,
+ "height": 24,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "Must Use By: 13/12/25",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "left"
+ }
+ },
+ {
+ "id": "el-1774013823300-e6inmnh",
+ "type": "TEXT_PRICE",
+ "x": 56,
+ "y": 200,
"width": 160,
- "height": 48,
+ "height": 56,
"rotation": "horizontal",
"border": "none",
"config": {
- "barcodeType": "CODE128",
- "data": "14124151231",
- "showText": true,
- "orientation": "horizontal"
+ "text": "9.99",
+ "prefix": "$",
+ "decimal": 2,
+ "fontFamily": "Arial",
+ "fontSize": 30,
+ "fontWeight": "bold",
+ "textAlign": "center"
+ }
+ },
+ {
+ "id": "el-1774014853682-zc8efun",
+ "type": "TEXT_STATIC",
+ "x": 8,
+ "y": 472,
+ "width": 264,
+ "height": 16,
+ "rotation": "horizontal",
+ "border": "none",
+ "config": {
+ "text": "222 W. Union Street, Concord, NC",
+ "fontFamily": "Arial",
+ "fontSize": 14,
+ "fontWeight": "normal",
+ "textAlign": "center"
}
+ },
+ {
+ "id": "el-1774014904848-0ejbswu",
+ "type": "BLANK",
+ "x": 8,
+ "y": 456,
+ "width": 270,
+ "height": 2,
+ "rotation": "horizontal",
+ "border": "line",
+ "config": {}
+ },
+ {
+ "id": "el-1774015010687-jkd254c",
+ "type": "BLANK",
+ "x": 8,
+ "y": 64,
+ "width": 270,
+ "height": 2,
+ "rotation": "horizontal",
+ "border": "line",
+ "config": {}
+ },
+ {
+ "id": "el-1774015083456-ampm74x",
+ "type": "BLANK",
+ "x": 8,
+ "y": 248,
+ "width": 270,
+ "height": 2,
+ "rotation": "horizontal",
+ "border": "line",
+ "config": {}
}
]
}
diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/templates/testPrintTemplate.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/templates/testPrintTemplate.ts
index 154fc1d..6ec52ad 100644
--- a/美国版/Food Labeling Management App UniApp/src/utils/print/templates/testPrintTemplate.ts
+++ b/美国版/Food Labeling Management App UniApp/src/utils/print/templates/testPrintTemplate.ts
@@ -4,10 +4,7 @@ import type { LabelTemplateData, StructuredLabelTemplate, SystemLabelTemplate }
export const TEST_PRINT_SYSTEM_TEMPLATE = testTemplateJson as SystemLabelTemplate
-export const TEST_PRINT_TEMPLATE_DATA: LabelTemplateData = {
- productName: '商品名',
- product: '商品名',
-}
+export const TEST_PRINT_TEMPLATE_DATA: LabelTemplateData = {}
export function createTestPrintTemplate (
dpi = 203,
diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/tscLabelBuilder.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/tscLabelBuilder.ts
index de821f1..4b21651 100644
--- a/美国版/Food Labeling Management App UniApp/src/utils/print/tscLabelBuilder.ts
+++ b/美国版/Food Labeling Management App UniApp/src/utils/print/tscLabelBuilder.ts
@@ -5,34 +5,69 @@
*/
import type { MonochromeImageData, PrintImageOptions, StructuredTscTemplate } from './types/printer'
-/** 将字符串转为 UTF-8 字节数组(不依赖 TextEncoder) */
-function stringToUtf8Bytes (str: string): number[] {
+function normalizePrinterText (str: string): string {
+ return String(str || '')
+ .normalize('NFKC')
+ .replace(/[\u2018\u2019]/g, '\'')
+ .replace(/[\u201C\u201D]/g, '"')
+ .replace(/[\u2013\u2014]/g, '-')
+}
+
+/** 将字符串转为 Windows-1252 字节数组,优先保证西文特殊字符可打印 */
+function stringToPrinterBytes (str: string): number[] {
+ const normalized = normalizePrinterText(str)
const out: number[] = []
- for (let i = 0; i < str.length; i++) {
- let c = str.charCodeAt(i)
- if (c < 0x80) {
- out.push(c)
- } else if (c < 0x800) {
- out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f))
- } else if (c < 0xd800 || c >= 0xe000) {
- out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f))
- } else {
- i++
- const c2 = str.charCodeAt(i)
- const u = ((c & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000
- out.push(
- 0xf0 | (u >> 18),
- 0x80 | ((u >> 12) & 0x3f),
- 0x80 | ((u >> 6) & 0x3f),
- 0x80 | (u & 0x3f)
- )
+ const cp1252Map: Record = {
+ 0x20ac: 0x80,
+ 0x201a: 0x82,
+ 0x0192: 0x83,
+ 0x201e: 0x84,
+ 0x2026: 0x85,
+ 0x2020: 0x86,
+ 0x2021: 0x87,
+ 0x02c6: 0x88,
+ 0x2030: 0x89,
+ 0x0160: 0x8a,
+ 0x2039: 0x8b,
+ 0x0152: 0x8c,
+ 0x017d: 0x8e,
+ 0x2018: 0x91,
+ 0x2019: 0x92,
+ 0x201c: 0x93,
+ 0x201d: 0x94,
+ 0x2022: 0x95,
+ 0x2013: 0x96,
+ 0x2014: 0x97,
+ 0x02dc: 0x98,
+ 0x2122: 0x99,
+ 0x0161: 0x9a,
+ 0x203a: 0x9b,
+ 0x0153: 0x9c,
+ 0x017e: 0x9e,
+ 0x0178: 0x9f,
+ }
+
+ for (let i = 0; i < normalized.length; i++) {
+ const code = normalized.charCodeAt(i)
+ if (code < 0x80) {
+ out.push(code)
+ continue
}
+ if (code >= 0xa0 && code <= 0xff) {
+ out.push(code)
+ continue
+ }
+ if (cp1252Map[code] != null) {
+ out.push(cp1252Map[code])
+ continue
+ }
+ out.push(0x3f)
}
return out
}
function addCommandBytes (out: number[], str: string) {
- const bytes = stringToUtf8Bytes(str)
+ const bytes = stringToPrinterBytes(str)
for (let i = 0; i < bytes.length; i++) out.push(bytes[i])
}
@@ -89,6 +124,7 @@ export function buildTscLabel (options: {
add(`SIZE ${widthMm} mm,${heightMm} mm`)
add('GAP 0 mm,0 mm')
+ add('CODEPAGE 1252')
add('CLS')
add(`TEXT 50,${y},"TSS24.BF2",0,1,1,"${escapeTscString(productName)}"`)
y += 35
@@ -118,6 +154,7 @@ export function buildTestTscLabel (): number[] {
add('SIZE 100 mm,65 mm')
add('GAP 0 mm,0 mm')
+ add('CODEPAGE 1252')
add('CLS')
add('BOX 20,20,780,500,3')
add('BAR 20,90,760,3')
@@ -156,6 +193,7 @@ export function buildTscTemplateLabel (template: StructuredTscTemplate): number[
add(`SIZE ${template.widthMm} mm,${template.heightMm} mm`)
add(`GAP ${template.gapMm || 0} mm,0 mm`)
+ add('CODEPAGE 1252')
if (template.density != null) add(`DENSITY ${template.density}`)
if (template.speed != null) add(`SPEED ${template.speed}`)
add('CLS')
@@ -179,6 +217,14 @@ export function buildTscTemplateLabel (template: StructuredTscTemplate): number[
add(`BARCODE ${item.x},${item.y},"${normalizeTscBarcodeType(item.symbology)}",${Math.max(20, Math.round(item.height || 80))},${item.readable === false ? 0 : 1},${item.rotation || 0},${Math.max(1, Math.round(item.narrow || 2))},${Math.max(2, Math.round(item.wide || 2))},"${escapeTscString(item.value)}"`)
return
}
+ if (item.type === 'bitmap') {
+ const bytesPerRow = item.image.width / 8
+ const bitmapBytes = pixelsToTscBitmapBytes(item.image)
+ add(`BITMAP ${item.x},${item.y},${bytesPerRow},${item.image.height},0,`)
+ for (let i = 0; i < bitmapBytes.length; i++) out.push(bitmapBytes[i])
+ out.push(0x0d, 0x0a)
+ return
+ }
add(`TEXT ${item.x},${item.y},"${item.font || 'TSS24.BF2'}",${item.rotation || 0},${item.xScale || 1},${item.yScale || 1},"${escapeTscString(item.text)}"`)
})
@@ -225,6 +271,7 @@ export function buildTscImageLabel (
add(`SIZE ${roundMm(widthMm)} mm,${roundMm(heightMm)} mm`)
add('GAP 0 mm,0 mm')
+ add('CODEPAGE 1252')
add('DENSITY 14')
add('SPEED 5')
add('CLS')
diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts
index caf2632..ba0aed1 100644
--- a/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts
+++ b/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts
@@ -126,12 +126,20 @@ export interface TscTemplateBarcodeItem {
wide?: number
}
+export interface TscTemplateBitmapItem {
+ type: 'bitmap'
+ x: number
+ y: number
+ image: MonochromeImageData
+}
+
export type TscTemplateItem =
| TscTemplateTextItem
| TscTemplateBoxItem
| TscTemplateBarItem
| TscTemplateQrCodeItem
| TscTemplateBarcodeItem
+ | TscTemplateBitmapItem
export interface StructuredTscTemplate {
widthMm: number
--
libgit2 0.21.4