Update configuration, add dependencies, and enhance socket event handling
Some checks failed
farmcontrol/farmcontrol-ws/pipeline/head There was a failure building this commit

- Changed log level in config.json from "debug" to "trace" for more detailed logging.
- Added new dependencies "pdf-to-img" and "sharp" in package.json for image processing capabilities.
- Introduced 'renderTemplateJPG' event handling in SocketHost and SocketUser classes to support JPG rendering.
- Implemented the renderJPG method in TemplateManager for converting templates to JPG format, including error handling and logging.
This commit is contained in:
Tom Butcher 2026-07-28 23:16:05 +01:00
parent 53b5924767
commit c2372f661e
7 changed files with 1980 additions and 9 deletions

1827
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -35,9 +35,11 @@
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"pdf-to-img": "^6.1.0",
"posthtml": "^0.16.7", "posthtml": "^0.16.7",
"puppeteer": "^24.31.0", "puppeteer": "^24.31.0",
"redis": "^5.10.0", "redis": "^5.10.0",
"sharp": "^0.34.5",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-adapter-mongo": "^2.0.5", "socket.io-adapter-mongo": "^2.0.5",
"socketio-jwt": "^4.6.2" "socketio-jwt": "^4.6.2"

View File

@ -7,40 +7,40 @@ const subJobSchema = new mongoose.Schema({
printer: { printer: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'printer', ref: 'printer',
required: true required: true,
}, },
job: { job: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'job', ref: 'job',
required: true required: true,
}, },
moonrakerJobId: { moonrakerJobId: {
type: String, type: String,
required: false required: false,
}, },
gcodeFile: { gcodeFile: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'gcodeFile', ref: 'gcodeFile',
required: true required: true,
}, },
state: { state: {
type: { required: true, type: String }, type: { required: true, type: String },
progress: { required: false, type: Number } progress: { required: false, type: Number },
}, },
number: { number: {
type: Number, type: Number,
required: true required: true,
}, },
createdAt: { createdAt: {
type: Date, type: Date,
default: Date.now default: Date.now,
}, },
updatedAt: { updatedAt: {
type: Date, type: Date,
default: Date.now default: Date.now,
}, },
startedAt: { required: false, type: Date, default: null }, startedAt: { required: false, type: Date, default: null },
finishedAt: { required: false, type: Date, default: null } finishedAt: { required: false, type: Date, default: null },
}); });
subJobSchema.index({ moonrakerJobId: 'text', 'state.type': 'text' }); subJobSchema.index({ moonrakerJobId: 'text', 'state.type': 'text' });

View File

@ -68,6 +68,10 @@ export class SocketHost {
'renderTemplatePDF', 'renderTemplatePDF',
this.handleRenderTemplatePDFEvent.bind(this) this.handleRenderTemplatePDFEvent.bind(this)
); );
this.socket.on(
'renderTemplateJPG',
this.handleRenderTemplateJPGEvent.bind(this)
);
this.socket.on('objectEvent', this.handleObjectEventEvent.bind(this)); this.socket.on('objectEvent', this.handleObjectEventEvent.bind(this));
this.socket.on('disconnect', this.handleDisconnect.bind(this)); this.socket.on('disconnect', this.handleDisconnect.bind(this));
} }
@ -262,6 +266,16 @@ export class SocketHost {
callback(result); callback(result);
} }
async handleRenderTemplateJPGEvent(data, callback) {
const result = await this.templateManager.renderJPG(
data._id,
data.content,
data.object,
{ width: data.width }
);
callback(result);
}
async setDevicesState(state, online, connectedAt) { async setDevicesState(state, online, connectedAt) {
logger.info('Setting devices state to', state, 'and online to', online); logger.info('Setting devices state to', state, 'and online to', online);

View File

@ -93,6 +93,10 @@ export class SocketUser {
'renderTemplatePDF', 'renderTemplatePDF',
this.handleRenderTemplatePDFEvent.bind(this) this.handleRenderTemplatePDFEvent.bind(this)
); );
this.socket.on(
'renderTemplateJPG',
this.handleRenderTemplateJPGEvent.bind(this)
);
this.socket.on( this.socket.on(
'generateHostOtp', 'generateHostOtp',
this.handleGenerateHostOtpEvent.bind(this) this.handleGenerateHostOtpEvent.bind(this)
@ -342,6 +346,26 @@ export class SocketUser {
} }
} }
} }
async handleRenderTemplateJPGEvent(data, callback) {
try {
const result = await this.templateManager.renderJPG(
data._id,
data.content,
data.object,
{ width: data.width }
);
if (typeof callback === 'function') {
callback(result);
}
} catch (err) {
logger.error('Render template JPG event error:', err);
if (typeof callback === 'function') {
callback({ error: err.message });
}
}
}
async handleGenerateHostOtpEvent(data, callback) { async handleGenerateHostOtpEvent(data, callback) {
const result = await generateHostOTP(data._id); const result = await generateHostOTP(data._id);
callback(result); callback(result);

78
src/templates/pdfUtils.js Normal file
View File

@ -0,0 +1,78 @@
/**
* PDF conversion utilities loaded on demand so startup
* does not require ESM-only pdf-to-img at module load time.
*/
import { pathToFileURL } from 'node:url';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
async function importPackage(name) {
const packagePath = require.resolve(name);
return import(pathToFileURL(packagePath).href);
}
function resolveDefault(module) {
return module?.default ?? module;
}
export async function convertPDFToImage(pdfInput, options = {}) {
const [pdfToImgModule, sharpModule] = await Promise.all([
importPackage('pdf-to-img'),
importPackage('sharp')
]);
const { pdf } = pdfToImgModule;
const sharp = resolveDefault(sharpModule);
try {
const { width, height, page_numbers, scale = 2, ...pdfOnlyOptions } =
options;
const pdfOptions = {
scale,
...pdfOnlyOptions
};
const document = await pdf(pdfInput, pdfOptions);
const outputImages = [];
if (
page_numbers &&
Array.isArray(page_numbers) &&
page_numbers.length > 0
) {
for (const pageNum of page_numbers) {
let image = await document.getPage(pageNum);
if (width || height) {
const resizeOptions = {};
if (width) resizeOptions.width = width;
if (height) resizeOptions.height = height;
image = await sharp(image).resize(resizeOptions).toBuffer();
}
outputImages.push(image);
}
} else {
for await (const image of document) {
let processedImage = image;
if (width || height) {
const resizeOptions = {};
if (width) resizeOptions.width = width;
if (height) resizeOptions.height = height;
processedImage = await sharp(image)
.resize(resizeOptions)
.toBuffer();
}
outputImages.push(processedImage);
}
}
return outputImages;
} catch (error) {
console.error('Error converting PDF to image:', error);
throw error;
}
}

View File

@ -15,6 +15,7 @@ import { dirname, join } from 'path';
import { getObject, listObjects } from '../database/database.js'; import { getObject, listObjects } from '../database/database.js';
import { getModelByName } from '../utils.js'; import { getModelByName } from '../utils.js';
import { generatePDF } from './pdffactory.js'; import { generatePDF } from './pdffactory.js';
import { convertPDFToImage } from './pdfUtils.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@ -595,6 +596,31 @@ export class TemplateManager {
} }
} }
/**
* Renders a template to image format for receipt printers
* @param {string} id - The document template ID
* @param {string} content - The template content
* @param {Object} data - Data object to pass to the template
* @param {Object} options - Image conversion options (e.g. width, height)
* @returns {Promise<Object>} Object containing image buffers or error
*/
async renderJPG(id, content, data = {}, options = {}) {
try {
logger.debug('Rendering JPG for template:', id);
const pdfResult = await this.renderPDF(id, content, data, options);
if (pdfResult.error != undefined) {
return { error: pdfResult.error };
}
const images = await convertPDFToImage(pdfResult.pdf, options);
return { images };
} catch (error) {
logger.warn('Error whilst rendering JPG:', error.message);
return { error: error.message };
}
}
async listObjects(objectType, filter = {}, populate = []) { async listObjects(objectType, filter = {}, populate = []) {
const model = getModelByName(objectType); const model = getModelByName(objectType);
if (model == undefined) { if (model == undefined) {