- Nuevo layout responsivo mobile-first con tabs inferiores - Sidebar colapsable en desktop con cola de impresión - Sistema de templates reutilizables con localStorage - Soporte Dark/Light mode con UColorModeButton - Composables usePrintQueue y useTemplates para estado global - Componentes modulares: CommandBuilder, QuickActions, PrintQueue, QueueItem - Navegación por tabs: Constructor | Cola | Templates
121 lines
2.8 KiB
TypeScript
121 lines
2.8 KiB
TypeScript
// Endpoint para imprimir imágenes usando Jimp
|
|
import Jimp from 'jimp'
|
|
import { EposMessageBuilder } from '~/server/utils/eposBuilder'
|
|
import { buildSoapEnvelope, sendToPrinter, parsePrinterResponse } from '~/server/utils/printer'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
try {
|
|
const config = useRuntimeConfig()
|
|
const body = await readBody(event)
|
|
|
|
const {
|
|
path,
|
|
width,
|
|
threshold = 128,
|
|
mode = 'mono'
|
|
} = body as {
|
|
path?: string
|
|
width?: number
|
|
threshold?: number
|
|
mode?: string
|
|
}
|
|
|
|
if (!path) {
|
|
return {
|
|
ok: false,
|
|
error: 'path required'
|
|
}
|
|
}
|
|
|
|
// Leer y procesar imagen
|
|
const img = await Jimp.read(path)
|
|
let targetWidth = width || img.bitmap.width
|
|
if (targetWidth <= 0) targetWidth = img.bitmap.width
|
|
|
|
const scale = targetWidth / img.bitmap.width
|
|
const targetHeight = Math.max(1, Math.round(img.bitmap.height * scale))
|
|
|
|
img.resize(targetWidth, targetHeight, Jimp.RESIZE_BILINEAR)
|
|
img.grayscale()
|
|
|
|
// Empaquetar bits MSB first por byte
|
|
const bytesPerRow = Math.ceil(targetWidth / 8)
|
|
const out = Buffer.alloc(bytesPerRow * targetHeight)
|
|
let outIdx = 0
|
|
|
|
for (let y = 0; y < targetHeight; y++) {
|
|
let byte = 0
|
|
let bit = 7
|
|
let rowBytes = 0
|
|
|
|
for (let x = 0; x < targetWidth; x++) {
|
|
const idx = (y * targetWidth + x) * 4
|
|
const rgba = img.bitmap.data
|
|
const r = rgba[idx]
|
|
const g = rgba[idx + 1]
|
|
const b = rgba[idx + 2]
|
|
const lum = 0.299 * r + 0.587 * g + 0.114 * b
|
|
const isBlack = lum < threshold
|
|
|
|
if (isBlack) byte |= (1 << bit)
|
|
bit--
|
|
|
|
if (bit < 0) {
|
|
out[outIdx++] = byte
|
|
rowBytes++
|
|
byte = 0
|
|
bit = 7
|
|
}
|
|
}
|
|
|
|
// Rellenar bits restantes
|
|
if (bit !== 7) {
|
|
out[outIdx++] = byte
|
|
rowBytes++
|
|
}
|
|
|
|
// Rellenar a bytes completos si es necesario
|
|
while (rowBytes < bytesPerRow) {
|
|
out[outIdx++] = 0
|
|
rowBytes++
|
|
}
|
|
}
|
|
|
|
const base64 = out.toString('base64')
|
|
|
|
// Construir mensaje ePOS
|
|
const builder = new EposMessageBuilder()
|
|
builder.imageRaw({
|
|
width: targetWidth,
|
|
height: targetHeight,
|
|
mode,
|
|
base64
|
|
})
|
|
|
|
const soap = buildSoapEnvelope(builder.build())
|
|
|
|
const result = await sendToPrinter(
|
|
soap,
|
|
config.printerHost,
|
|
config.printerDeviceId,
|
|
parseInt(config.printerTimeoutMs)
|
|
)
|
|
|
|
const { success, code } = parsePrinterResponse(result.data)
|
|
|
|
return {
|
|
ok: success,
|
|
httpStatus: result.status,
|
|
code,
|
|
raw: result.data,
|
|
width: targetWidth,
|
|
height: targetHeight
|
|
}
|
|
} catch (err: any) {
|
|
return {
|
|
ok: false,
|
|
error: err.message
|
|
}
|
|
}
|
|
})
|