diff --git a/src/BrotherSdk.ts b/src/BrotherSdk.ts new file mode 100644 index 0000000..c8d9333 --- /dev/null +++ b/src/BrotherSdk.ts @@ -0,0 +1,268 @@ +import { + getAbsolutePath, + getPrintOption, + getEncodingOption, +} from "./helpers"; +import { + Data, + Config, + Encoding, + ImageOptions, +} from "./types"; +import { + openTemplate, + closeTemplate, + populateObjectsInTemplate, + getPrinterName, + imageData, + getPrinters, + startPrint, + printOut, + endPrint, + exportTemplate, +} from "./adapter"; + +export default class BrotherSdk { + #ready: boolean; + + templatePath: string; + + exportDir: string; + + // One mutation observer, regardless of instances. + static #observer: MutationObserver | undefined; + + /** + * **Retrieve The List Of Installed Printers** + * + * asynchronously retrieves the list of installed printers compatible with the bpac SDK. + * + * @returns {Promise} + * a promise that resolves to an array of installed printers + * compatible with the 'bpac' SDK. + * @throws {Error} + * will throw an err if the method fails. + * + */ + static async getPrinterList(): Promise { + const printers = await getPrinters(); + return printers; + } + + /** + * @constructor + * Constructs a new instance of the BrotherSdk class, used + * for interacting with Brother SDK functionality. + * @param {Object} object + * @param {String} object.templatePath + * Specifies the path to the template file (supports various formats) + * - Local path: "c:/path/to/your/template.lbx" + * - UNC path: "\\server\share\template.lbx" + * - Remote URL: "http://yourserver.com/templates/label.lbx" + * @param {String} [object.exportDir = ""] + * The path for exporting generated templates. + */ + constructor({ + templatePath, + exportDir, + }: { + templatePath: string; + exportDir?: string; + }) { + this.templatePath = templatePath; + this.exportDir = exportDir || ""; + this.#ready = false; + this.#initialize(); + } + + #initialize() { + const targetNode = document.body; + const className = "bpac-extension-installed"; + + if (this.#ready) { + return; + } + + if (targetNode.classList.contains(className)) { + this.#ready = true; + return; + } + + if (BrotherSdk.#observer) { + return; + } + + BrotherSdk.#observer = new MutationObserver(() => { + if (targetNode.classList.contains(className)) { + this.#ready = true; + BrotherSdk.#observer?.disconnect(); + BrotherSdk.#observer = undefined; + } + }); + + BrotherSdk.#observer.observe(targetNode, { + attributes: true, + attributeFilter: ["class"], + }); + } + + async #isPrintReady(timeout: number = 3000): Promise { + if (this.#ready) { + return true; + } + + return new Promise((resolve, reject) => { + setTimeout(() => { + if (this.#ready) { + resolve(true); + } else { + reject( + new Error( + "Cannot establish printer communication: b-PAC extension missing or inactive. Install/enable.", + ), + ); + } + }, timeout); + }); + } + + /** + * **Method For Printing A Label** + * + * asynchronously print a label using the specified configurations. + * + * @param {Object} data + * an object containing key-value pairs, where each key represents an object ID, + * and the corresponding value is the text to be set on that object. + * @param {Object} config + * { object } + * @param {Number} [config.copies = 1] + * number of copies to be printed. + * @param {String} [config.printName = "BPAC-Document"] + * print document name. + * @param {String} [config.option = "default"] + * use the `PrinterOptions` enum for valid options. formally "quality" + */ + async print(data: Data, config: Config): Promise { + const copies:number = config?.copies || 1; + const printName:string = config?.printName || "BPAC-Document"; + const opts:number = getPrintOption(config?.option); + + await this.#isPrintReady(); + + await openTemplate(this.templatePath); + await populateObjectsInTemplate(data); + await startPrint(printName, opts); + await printOut(copies); + await endPrint(); + await closeTemplate(); + + return true; + } + + /** + * **Method For Retrieving The Label's Image Data** + * + * asynchronously retrieves and returns Base64-encoded image data for a label. + * + * @param {object} data + * an object containing key-value pairs, where each key represents an object ID, + * and the corresponding value is the text to be set on that object. + * @param {object} options + * { object } + * @param {string} options.height + * if the vertical size (dpi) of the image to be acquired is specified as 0, it + * becomes a size that maintains the aspect ratio based on width. + * @param {string} options.width + * horizontal size (dpi) of the image to be acquired. If 0 is specified, + * it becomes a size that maintains the aspect ratio based on height. + * @returns {Promise} + * a promise that resolves to a Base64 encoded string representing the image data. + */ + async getImageData( + data: Data, + options: ImageOptions, + ): Promise { + const height = options?.height || 0; + const width = options?.width || 0; + + await this.#isPrintReady(); + await openTemplate(this.templatePath); + await populateObjectsInTemplate(data); + const base64Data = await imageData(width, height); + await closeTemplate(); + + return `${base64Data}`; + } + + /** + * **Retrieve The Printer's Name** + * + * asynchronously retrieves the printers name for the current context. + * + * @returns {Promise} + * a promise that resolves with the name of the printer. + * @throws {Error} + * fails to get the printer name. + * + */ + async getPrinterName(): Promise { + await this.#isPrintReady(); + await openTemplate(this.templatePath); + const printer = getPrinterName(); + await closeTemplate(); + return printer; + } + + /** + * **Export Label To File** + * + * asynchronously populate & export a copy of the file in various formats. + * + * @param {Object} data + * an object containing key-value pairs, where each key represents + * an object ID, and the corresponding value is the text to be set on that object. + * @param {String} [filePathOrFileName = ""] + * provide a file name or absolute path. + * - "myLabel.lbx" to be store in exportDir. + * - "C:/Templates/myLabel.lbx" to override the exportDir. + * @param {("default" | "lbx" | "lbl" | "lbi" | "bmp" | "paf")} [encoding = "default"] + * file encoding type. + * @param {Number} [resolution = 0] + * provide a resolution in dpi used for the conversion into bitmap format. + * Specifies the resolution of the output device. + * (Screen: 72 or 96; output to SC-2000: 600) + * If a value of 0 is specified, the printer resolution is used. + * + * The resolution param is only valid for LBI and BMP formats. + * @returns {Promise} + * @throws {Error} + * fails to export. + */ + async export( + data: Data = {}, + filePathOrFileName: string = "", + encoding: Encoding = Encoding.Default, + resolution: number = 0, + ): Promise { + const fileType = getEncodingOption(encoding); + const path:string = getAbsolutePath( + this.exportDir, + filePathOrFileName, + ); + + await this.#isPrintReady(); + await openTemplate(this.templatePath); + await populateObjectsInTemplate(data); + const status = await exportTemplate(fileType, path, resolution); + await closeTemplate(); + + if (status) { + return true; + } + + throw new Error( + "Export failed: Please check the export directory and filename.", + ); + } +} diff --git a/src/adapter.ts b/src/adapter.ts new file mode 100644 index 0000000..8e94202 --- /dev/null +++ b/src/adapter.ts @@ -0,0 +1,108 @@ +import * as bpac from "./vendor/bpac-v3.4"; +import { Data, ObjectTypes } from "./types"; + +const doc = bpac.IDocument; + +export const openTemplate = async (path: string): Promise => { + const isOpen = await doc.Open(path); + + if (isOpen) { + return true; + } + + throw new Error( + "Failed to open template file, please check path and try again.", + ); +}; + +export const getPrinterName = async (): Promise => { + const printerName: string = await doc.GetPrinterName(); + return printerName; +}; + +export const getPrinters = async (): Promise => { + const obj = await doc.GetPrinter(); + const printers = await obj.GetInstalledPrinters(); + return printers; +}; + +// Fix these params +export const startPrint = async (printName:string, options:number): Promise => { + const isComplete = await doc.StartPrint(printName, options); + return isComplete; +}; + +export const printOut = async (copies: number): Promise => { + const isComplete = await doc.PrintOut(copies, 0); + return isComplete; +}; + +export const endPrint = async () => { + const isComplete = await doc.EndPrint(); + return isComplete; +}; + +export const imageData = async (width:number, height:number): Promise => { + const data = await doc.GetImageData(4, width, height); + return data; +}; + +export const closeTemplate = async (): Promise => { + const isClosed = await doc.Close(); + + if (isClosed) { + return true; + } + + throw new Error("Failed to close template file."); +}; + +export const exportTemplate = async ( + type:number, + destination:string, + resolution:number, +): Promise => { + const isComplete = await doc.Export(type, destination, resolution); + return isComplete; +}; + +export const populateObjectsInTemplate = async (data: Data): Promise => { + const keys: string[] = Object.keys(data); + + // eslint-disable-next-line no-restricted-syntax + for (const prop of keys) { + const value = data[prop]; + const obj = await doc.GetObject(prop); + + if (!obj) { + await closeTemplate(); + throw new Error( + `There is no object in the specified template with the name of "${prop}".`, + ); + } + + const type = await obj.Type; + + switch (type) { + case ObjectTypes.Text: + obj.Text = value; + break; + case ObjectTypes.Image: + await obj.SetData(0, value, 4); + break; + case ObjectTypes.Datetime: + await obj.SetData(0, value); + break; + case ObjectTypes.Barcode: + await doc.SetBarcodeData(0, value); + break; + case ObjectTypes.Clipart: + await obj.SetData(0, value, 0); + break; + default: + throw new Error(`Unknown type for "${prop}" prop.`); + } + } + + return true; +}; diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..af12847 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,73 @@ +import { PrinterOptions, Encoding } from "./types"; + +export const getAbsolutePath = (basePath: string, filePathOrFileName: string): string => { + // eslint-disable-next-line no-useless-escape + const isPath = /^(.*[\\\/])([^\\\/]+)\.([^.]+)$/; + + if (!isPath.test(filePathOrFileName)) { + if (!basePath) { + throw Error("Please set exportDir or provide the full path."); + } + return basePath + filePathOrFileName; + } + + return filePathOrFileName; +}; + +export const getPrintOption = (option?:PrinterOptions): number => { + switch (option) { + case PrinterOptions.Default: + return 0x0; + case PrinterOptions.AutoCut: + case PrinterOptions.CutPause: + return 0x1; + case PrinterOptions.CutMark: + return 0x2; + case PrinterOptions.HalfCut: + return 0x200; + case PrinterOptions.ChainPrint: + return 0x400; + case PrinterOptions.TailCut: + return 0x800; + case PrinterOptions.SpecialTape: + return 0x00080000; + case PrinterOptions.CutAtEnd: + return 0x04000000; + case PrinterOptions.NoCut: + return 0x10000000; + case PrinterOptions.Mirroring: + return 0x4; + case PrinterOptions.Quality: + return 0x00010000; + case PrinterOptions.HighSpeed: + return 0x01000000; + case PrinterOptions.HighResolution: + return 0x02000000; + case PrinterOptions.Color: + case PrinterOptions.Mono: + return 0x8; + case PrinterOptions.Continue: + return 0x40000000; + default: + return 0x0; + } +}; + +export const getEncodingOption = (fileType?:Encoding) => { + switch (fileType) { + case Encoding.Default: + return 0x0; + case Encoding.Lbx: + return 0x1; + case Encoding.Lbl: + return 0x2; + case Encoding.Lbi: + return 0x3; + case Encoding.Bmp: + return 0x4; + case Encoding.Paf: + return 0x5; + default: + throw new Error(`Invalid encoding. Received ${fileType}.`); + } +}; diff --git a/src/index.ts b/src/index.ts index 87bdff7..e564466 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,368 +1,5 @@ -// eslint-disable-next-line import/extensions -import * as bpac from "./bpac-v3.4.js"; - -// Types -type Data = Record; -type NumberEnum = Record; - -type Config = { - copies?: number; - printName?: string; - quality?: "default" | "fast" | "high"; -}; - -const QUALITY: NumberEnum = Object.freeze({ - default: 0x0, - fast: 0x01000000, - high: 0x02000000, -}); - -const OBJECT_TYPE: NumberEnum = Object.freeze({ - text: 0, - barcode: 1, - image: 2, - datetime: 3, - clipart: 4, -}); - -const FILE_TYPE: NumberEnum = Object.freeze({ - default: 1, - lbx: 1, - lbl: 2, - lbi: 3, - bmp: 4, - paf: 5, -}); - -const doc = bpac.IDocument; - -// Helpers -const getAbsolutePath = ( - basePath: string, - filePathOrFileName: string, -): string => { - // eslint-disable-next-line no-useless-escape - const isPath = /^(.*[\\\/])([^\\\/]+)\.([^.]+)$/; - - if (!isPath.test(filePathOrFileName)) { - if (!basePath) { - throw Error("Please set exportDir or provide the full path."); - } - return basePath + filePathOrFileName; - } - - return filePathOrFileName; -}; - -const openTemplate = async (path: string): Promise => { - const isOpen = await doc.Open(path); - - if (isOpen) { - return true; - } - - throw new Error( - "Failed to open template file, please check path and try again.", - ); -}; - -const closeTemplate = async (): Promise => { - const isClosed = await doc.Close(); - - if (isClosed) { - return true; - } - - throw new Error("Failed to close template file."); -}; - -const populateObjects = async (data: Data): Promise => { - const props: string[] = Object.keys(data); - - // eslint-disable-next-line no-restricted-syntax - for (const prop of props) { - const value = data[prop]; - const obj = await doc.GetObject(prop); - - if (!obj) { - await closeTemplate(); - throw new Error( - `There is no object in the specified template with the name of "${prop}".`, - ); - } - - const type = await obj.Type; - - switch (type) { - case OBJECT_TYPE.text: - obj.Text = value; - break; - case OBJECT_TYPE.image: - await obj.SetData(0, value, 4); - break; - case OBJECT_TYPE.datetime: - await obj.SetData(0, value); - break; - case OBJECT_TYPE.barcode: - await doc.SetBarcodeData(0, value); - break; - case OBJECT_TYPE.clipart: - await obj.SetData(0, value, 0); - break; - default: - throw new Error(`Unknown type for "${prop}" prop.`); - } - } - - return true; -}; -// end of helpers - -class BrotherSdk { - #ready: boolean; - - templatePath: string; - - exportDir: string; - - /** - * @constructor - * Constructs a new instance of the BrotherSdk class, used - * for interacting with Brother SDK functionality. - * @param {Object} object - * @param {String} object.templatePath - * Specifies the path to the template file (supports various formats) - * - Local path: "c:/path/to/your/template.lbx" - * - UNC path: "\\server\share\template.lbx" - * - Remote URL: "http://yourserver.com/templates/label.lbx" - * @param {String} [object.exportDir = ""] - * The path for exporting generated templates. - */ - constructor({ - templatePath, - exportDir, - }: { - templatePath: string; - exportDir?: string; - }) { - this.templatePath = templatePath; - this.exportDir = exportDir || ""; - this.#ready = false; - this.#initialize(); - } - - #initialize() { - const targetNode = document.body; - const className = "bpac-extension-installed"; - - if (targetNode.classList.contains(className)) { - this.#ready = true; - return; - } - - let observer: MutationObserver | undefined; - - // eslint-disable-next-line prefer-const - observer = new MutationObserver(() => { - if (targetNode.classList.contains(className)) { - this.#ready = true; - observer?.disconnect(); - } - }); - - observer.observe(targetNode, { - attributes: true, - attributeFilter: ["class"], - }); - } - - async #isPrintReady(timeout: number = 3000): Promise { - if (this.#ready) { - return true; - } - - return new Promise((resolve, reject) => { - setTimeout(() => { - if (this.#ready) { - resolve(true); - } else { - reject( - new Error( - "Cannot establish printer communication: b-PAC extension missing or inactive. Install/enable.", - ), - ); - } - }, timeout); - }); - } - - /** - * **Method For Printing A Label** - * - * asynchronously print a label using the specified configurations. - * - * @param {Object} data - * an object containing key-value pairs, where each key represents an object ID, - * and the corresponding value is the text to be set on that object. - * @param {Object} config - * @param {Number} [config.copies = 1] - * number of copies to be printed. - * @param {String} [config.printName = "BPAC-Document"] - * print document name. - * @param {("default" | "fast" | "high")} [config.quality = "default"] - * print quality. - */ - async print(data: Data, config: Config): Promise { - const copies = config?.copies || 1; - const printName = config?.printName || "BPAC-Document"; - const printOpt = QUALITY[config?.quality || "default"]; - - await this.#isPrintReady(); - await openTemplate(this.templatePath); - await populateObjects(data); - await doc.StartPrint(printName, printOpt); - await doc.PrintOut(copies, 0); - await doc.EndPrint(); - await closeTemplate(); - - return true; - } - - /** - * **Method For Retrieving The Label's Image Data** - * - * asynchronously retrieves and returns Base64-encoded image data for a label. - * - * @param {object} data - * an object containing key-value pairs, where each key represents an object ID, - * and the corresponding value is the text to be set on that object. - * @param {object} options - * @param {string} options.height - * if the vertical size (dpi) of the image to be acquired is specified as 0, it - * becomes a size that maintains the aspect ratio based on width. - * @param {string} options.width - * horizontal size (dpi) of the image to be acquired. If 0 is specified, - * it becomes a size that maintains the aspect ratio based on height. - * @returns {Promise} - * a promise that resolves to a Base64 encoded string representing the image data. - */ - async getImageData( - data: Data, - options: { height?: number; width?: number }, - ): Promise { - const height = options?.height || 0; - const width = options?.width || 0; - - await this.#isPrintReady(); - await openTemplate(this.templatePath); - await populateObjects(data); - const base64Data = await doc.GetImageData(4, width, height); - await closeTemplate(); - - return `${base64Data}`; - } - - /** - * **Retrieve The List Of Installed Printers** - * - * asynchronously retrieves the list of installed printers compatible with the bpac SDK. - * - * @returns {Promise} - * a promise that resolves to an array of installed printers - * compatible with the 'bpac' SDK. - * @throws {Error} - * will throw an err if the method fails. - * - */ - static async getPrinterList(): Promise { - const printObj = await doc.GetPrinter(); - const printers = await printObj.GetInstalledPrinters(); - - return printers; - } - - /** - * **Retrieve The Printer's Name** - * - * asynchronously retrieves the printers name for the current context. - * - * @returns {Promise} - * a promise that resolves with the name of the printer. - * @throws {Error} - * if fails to get the printer name. - * - */ - async getPrinterName(): Promise { - await this.#isPrintReady(); - await openTemplate(this.templatePath); - const printer = await doc.GetPrinterName(); - await closeTemplate(); - return printer; - } - - /** - * **Export Label To File** - * - * asynchronously populate & export the template file to a specified format. - * - * @param {Object} data - * an object containing key-value pairs, where each key represents - * an object ID, and the corresponding value is the text to be set on that object. - * @param {String} [filePathOrFileName = ""] - * provide a file name "myLabel.lbx" and the file will be stored in the set exportDir. - * provide a full path to override the exportDir. "C:/Templates/myLabel.lbx" - * @param {("default" | "lbx" | "lbl" | "lbi" | "bmp" | "paf")} [encoding = "default"] - * file encoding type. - * @param {Number} [resolution = 0] - * provide a resolution in dpi used for the conversion into bitmap format. - * Specifies the resolution of the output device. - * (Screen: 72 or 96; output to SC-2000: 600) - * If a value of 0 is specified, the printer resolution is used. - * - * The resolution param is only valid for LBI and BMP formats. - * @returns {Promise} - * @throws {Error} - * If fails to export. - */ - async export( - data: Data = {}, - filePathOrFileName: string = "", - encoding: "default" | "lbx" | "lbl" | "lbi" | "bmp" | "paf" = "default", - resolution: number = 0, - ): Promise { - const encodingType = FILE_TYPE[encoding || "default"]; - - if (Number.isNaN(encodingType)) { - throw new Error( - `Invalid encoding type. Expected (${Object.keys(FILE_TYPE).join( - " | ", - )}) received "${encoding}"`, - ); - } - - const destinationPath = getAbsolutePath( - this.exportDir, - filePathOrFileName, - ); - - await this.#isPrintReady(); - await openTemplate(this.templatePath); - await populateObjects(data); - const status = await doc.Export( - encodingType, - destinationPath, - resolution, - ); - await closeTemplate(); - - if (status) { - return true; - } - - throw new Error( - "Export failed: Please check the export directory and filename.", - ); - } -} +import BrotherSdk from "./BrotherSdk"; +import { Encoding, PrinterOptions } from "./types"; +export { BrotherSdk, Encoding, PrinterOptions }; export default BrotherSdk; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..3e1d1d6 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,51 @@ +export type Data = Record; + +export type Config = { + copies?: number; + printName?: string; + option?: PrinterOptions; +}; + +export type ImageOptions = { + width?: number; + height?:number; +}; + +export enum ObjectTypes { + Text = 0, + Barcode = 1, + Image = 2, + Datetime = 3, + Clipart = 4, +} + +export enum Encoding { + Default = "default", + Lbx = "lbx", + Lbl = "lbl", + Lbi = "lbi", + Bmp = "bmp", + Paf = "paf", +} + +export enum PrinterOptions { + Default = "default", + AutoCut = "auto-cut", + CutPause = "cut-pause", + CutMark = "cut-mark", + HalfCut = "half-cut", + ChainPrint = "chain-print", + TailCut = "tail-cut", + SpecialTape = "special-tape", + CutAtEnd = "cut-at-end", + NoCut = "no-cut", + Mirroring = "mirror", + Quality = "quality", + HighSpeed = "high-speed", + HighResolution = "high-resolution", + Color = "color", + Mono = "mono", + Continue = "continue", + Fast = "fast", // Deprecated: Use HighSpeed for faster printing. + High = "high", // Deprecated: Use HighResolution for higher print resolution. +} diff --git a/src/bpac-v3.4.js b/src/vendor/bpac-v3.4.js similarity index 100% rename from src/bpac-v3.4.js rename to src/vendor/bpac-v3.4.js diff --git a/test/browser/playground.js b/test/browser/playground.js index 4f83092..7460375 100644 --- a/test/browser/playground.js +++ b/test/browser/playground.js @@ -1,12 +1,12 @@ //import BrotherSdK from "https://cdn.jsdelivr.net/npm/bpac-js@2.0.4/dist/index.mjs"; -import BrotherSdK from "../../dist/index.mjs"; +import BrotherSdk from "../../dist/index.mjs"; const printBtn = document.getElementById("print-btn"); const previewBtn = document.getElementById("preview-btn"); const exportBtn = document.getElementById("export-btn"); const preview = document.getElementById("preview"); -const tag = new BrotherSdK({ +const tag = new BrotherSdk({ templatePath: "C:/Users/YMH/Desktop/example.lbx", exportDir: "C:/Users/YMH/Desktop/Exported Labels/", }); @@ -30,7 +30,7 @@ const printTag = async () => { const previewTag = async () => { try { preview.src = ""; - const imgData = await tag.getImageData(data, { height: 120 }); + const imgData = await tag.getImageData(data, {height: 120}); preview.src = imgData; } catch (error) { console.log({ error }); @@ -39,7 +39,7 @@ const previewTag = async () => { const exportTag = async () => { try { - const status = await tag.export(data, "my-tag.bmp", "bmp"); + const status = await tag.export(data, "my-tag.bmp", "bmp", 300); console.log({status}) } catch (error) { console.log({ error }); diff --git a/tsconfig.json b/tsconfig.json index dd6a5d4..3f4a416 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "forceConsistentCasingInFileNames": true, // Enforce consistent file casing for compatibility "resolveJsonModule": true, // Allow importing JSON files as modules "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export + "allowImportingTsExtensions": true, "isolatedModules": true, // Treat each file as a separate module (important for React) "noEmit": true, //use tsup to emit & bundle "noUncheckedIndexedAccess": true, @@ -19,8 +20,8 @@ "lib": ["ES2017", "DOM", "DOM.Iterable"] //Includes additional libraries for DOM-related features and newer ECMAScript features, //supporting web development scenarios. }, - "include": ["src/*.ts", "src/*.js"], // Specifies the root directory and file pattern for inclusion in compilation, + "include": ["src/**/*.ts", "src/**/*.js"], // Specifies the root directory and file pattern for inclusion in compilation, // focusing on TypeScript files within the "src" directory. - "exclude": ["./node_modules"] // Excludes the "node_modules" directory from compilation, avoiding redundant + "exclude": ["node_modules"] // Excludes the "node_modules" directory from compilation, avoiding redundant // checks for external dependencies. }