import type { MonochromeImageData, PrintImageOptions, PrinterDriver, RawImageDataSource } from './types/printer' const DEFAULT_IMAGE_THRESHOLD = 180 function yieldToUi (): Promise { return new Promise((resolve) => { setTimeout(resolve, 0) }) } function ensureMultipleOf8 (value: number, maxValue?: number): number { const safeMax = maxValue && maxValue > 0 ? Math.max(8, Math.floor(maxValue)) : 0 let width = Math.max(8, Math.round(value || 0)) if (safeMax && width > safeMax) width = safeMax if (width % 8 !== 0) { width += 8 - (width % 8) if (safeMax && width > safeMax) { width = safeMax - (safeMax % 8) if (width <= 0) width = 8 } } return width } 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 getDefaultMaxWidthDots (driver: PrinterDriver): number { if (driver.imageMaxWidthDots && driver.imageMaxWidthDots > 0) return driver.imageMaxWidthDots return driver.protocol === 'esc' ? 384 : 800 } export function rasterizeImageData ( source: RawImageDataSource, options: PrintImageOptions = {} ): MonochromeImageData { const sourceWidth = Math.max(1, Math.round(source.width || 0)) const sourceHeight = Math.max(1, Math.round(source.height || 0)) const targetWidth = ensureMultipleOf8(sourceWidth) const threshold = options.threshold != null ? Number(options.threshold) : DEFAULT_IMAGE_THRESHOLD const pixels: number[] = new Array(targetWidth * sourceHeight).fill(0) for (let y = 0; y < sourceHeight; y++) { for (let x = 0; x < sourceWidth; x++) { const index = (y * sourceWidth + x) * 4 const alpha = Number(source.data[index + 3] ?? 255) const red = Number(source.data[index] ?? 255) const green = Number(source.data[index + 1] ?? 255) const blue = Number(source.data[index + 2] ?? 255) const gray = red * 0.299 + green * 0.587 + blue * 0.114 pixels[y * targetWidth + x] = alpha <= 10 || gray > threshold ? 0 : 1 } } return { width: targetWidth, height: sourceHeight, pixels, } } export async function rasterizeImageForPrinter ( source: string, driver: PrinterDriver, options: PrintImageOptions = {} ): Promise { // #ifdef APP-PLUS const rawSource = String(source || '').trim() if (!rawSource) { throw new Error('IMAGE_SOURCE_EMPTY') } const BitmapFactory = plus.android.importClass('android.graphics.BitmapFactory') const Bitmap = plus.android.importClass('android.graphics.Bitmap') const Base64 = plus.android.importClass('android.util.Base64') const base64Payload = normalizeBase64Payload(rawSource) let sourceBitmap: any = null if (base64Payload) { const bytes = Base64.decode(base64Payload, 0) sourceBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length) } else { const localPath = resolveLocalImagePath(rawSource) sourceBitmap = BitmapFactory.decodeFile(localPath) } if (!sourceBitmap) { throw new Error('IMAGE_DECODE_FAILED') } const sourceWidth = Number(sourceBitmap.getWidth ? sourceBitmap.getWidth() : 0) const sourceHeight = Number(sourceBitmap.getHeight ? sourceBitmap.getHeight() : 0) if (!sourceWidth || !sourceHeight) { try { sourceBitmap.recycle && sourceBitmap.recycle() } catch (_) {} throw new Error('IMAGE_SIZE_INVALID') } const protocolMaxWidth = getDefaultMaxWidthDots(driver) const maxWidthDots = options.maxWidthDots && options.maxWidthDots > 0 ? options.maxWidthDots : protocolMaxWidth const targetWidth = ensureMultipleOf8(options.targetWidthDots || sourceWidth, maxWidthDots) const aspectRatio = sourceHeight / sourceWidth const targetHeight = Math.max( 1, Math.round(options.targetHeightDots || (targetWidth * aspectRatio)) ) const threshold = options.threshold != null ? Number(options.threshold) : DEFAULT_IMAGE_THRESHOLD const scaledBitmap = Bitmap.createScaledBitmap(sourceBitmap, targetWidth, targetHeight, true) const rasterPixels: number[] = new Array(targetWidth * targetHeight) for (let y = 0; y < targetHeight; y++) { if (y > 0 && y % 16 === 0) { await yieldToUi() } for (let x = 0; x < targetWidth; x++) { const color = Number(scaledBitmap.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 rasterPixels[y * targetWidth + x] = alpha <= 10 || gray > threshold ? 0 : 1 } } try { if (scaledBitmap !== sourceBitmap && sourceBitmap?.recycle) sourceBitmap.recycle() } catch (_) {} try { scaledBitmap?.recycle && scaledBitmap.recycle() } catch (_) {} return { width: targetWidth, height: targetHeight, pixels: rasterPixels, } // #endif // #ifndef APP-PLUS throw new Error('IMAGE_PRINT_APP_ONLY') // #endif }