Add PDF conversion utilities and update build scripts for Linux packaging
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good

Introduce a new pdfUtils.js file for on-demand PDF conversion using pdf-to-img and sharp. Update package.json to include new build scripts for Linux packaging, enhancing the build process with public package handling. Add .npmrc for public hoisting patterns and create build-linux-packages.sh for streamlined package creation.
This commit is contained in:
Tom Butcher 2026-07-26 21:47:21 +01:00
parent d1592bf55e
commit 55b4c1a42e
7 changed files with 123 additions and 75 deletions

6
.npmrc Normal file
View File

@ -0,0 +1,6 @@
public-hoist-pattern[]=pdf-to-img
public-hoist-pattern[]=pdfjs-dist
public-hoist-pattern[]=sharp
public-hoist-pattern[]=@img/*
public-hoist-pattern[]=canvas
public-hoist-pattern[]=@napi-rs/*

View File

@ -15,8 +15,10 @@
"build:electron-renderer": "vite build src/electron --outDir build/electron", "build:electron-renderer": "vite build src/electron --outDir build/electron",
"build:electron": "pnpm build && electron-builder", "build:electron": "pnpm build && electron-builder",
"build:electron:mac": "pnpm build && electron-builder --mac dmg --arm64 --x64 && electron-builder --mac pkg --arm64 && electron-builder --mac pkg --x64", "build:electron:mac": "pnpm build && electron-builder --mac dmg --arm64 --x64 && electron-builder --mac pkg --arm64 && electron-builder --mac pkg --x64",
"build:linux": "pnpm run cleanBuild && pnpm run build:server && pnpm run build:linux-bundle && shx mkdir -p app_dist/linux && pkg build/pkg-entry.cjs --config package.json --targets node18-linux-x64 --output app_dist/linux/farmcontrol-server && fpm -f -s dir -t deb -n farmcontrol-server -p app_dist/linux/ -v $(node -p \"require('./package.json').version\") --after-install packaging/linux/after-install.sh --after-remove packaging/linux/after-remove.sh app_dist/linux/farmcontrol-server=/usr/bin/farmcontrol-server packaging/linux/farmcontrol-server.service=/lib/systemd/system/farmcontrol-server.service && fpm -f -s dir -t rpm -n farmcontrol-server -p app_dist/linux/ -v $(node -p \"require('./package.json').version\") --after-install packaging/linux/after-install.sh --after-remove packaging/linux/after-remove.sh app_dist/linux/farmcontrol-server=/usr/bin/farmcontrol-server packaging/linux/farmcontrol-server.service=/lib/systemd/system/farmcontrol-server.service", "build:linux": "pnpm run cleanBuild && pnpm run build:server && pnpm run build:linux-bundle && pnpm run build:linux-pkg && pnpm run build:linux-packages",
"build:linux-bundle": "esbuild src/headless.js --bundle --platform=node --target=node18 --format=cjs --outfile=build/pkg-entry.cjs --packages=external --external:electron --inject:./scripts/import-meta-url.js --define:import.meta.url=import_meta_url", "build:linux-bundle": "node scripts/build-linux-bundle.mjs",
"build:linux-pkg": "shx mkdir -p app_dist/linux && pnpm exec pkg build/pkg-entry.cjs --config package.json --targets node18-linux-x64 --public-packages \"pdf-to-img,pdfjs-dist,sharp,canvas,@img/*,@napi-rs/*\" --output app_dist/linux/farmcontrol-server",
"build:linux-packages": "bash scripts/build-linux-packages.sh",
"cleanBuild": "rimraf build" "cleanBuild": "rimraf build"
}, },
"author": "Tom Butcher", "author": "Tom Butcher",
@ -70,11 +72,12 @@
], ],
"assets": [ "assets": [
"build/config.json", "build/config.json",
"node_modules/pdf-to-img/**/*",
"node_modules/pdfjs-dist/**/*",
"node_modules/sharp/**/*", "node_modules/sharp/**/*",
"node_modules/@img/**/*", "node_modules/@img/**/*",
"node_modules/canvas/**/*", "node_modules/canvas/**/*",
"node_modules/@napi-rs/**/*", "node_modules/@napi-rs/**/*"
"node_modules/pdf-to-img/**/*"
], ],
"targets": [ "targets": [
"node18-linux-x64" "node18-linux-x64"

View File

@ -0,0 +1,27 @@
import * as esbuild from 'esbuild'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
await esbuild.build({
entryPoints: [path.join(rootDir, 'src/headless.js')],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
outfile: path.join(rootDir, 'build/pkg-entry.cjs'),
external: [
'electron',
'sharp',
'canvas',
'@napi-rs/canvas',
'pdf-to-img',
'pdfjs-dist'
],
inject: [path.join(rootDir, 'scripts/import-meta-url.js')],
define: {
'import.meta.url': 'import_meta_url'
},
logLevel: 'info'
})

21
scripts/build-linux-packages.sh Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
version="$(node -p "require('./package.json').version")"
output_dir="app_dist/linux"
binary="${output_dir}/farmcontrol-server"
common_fpm_args=(
-f
-s dir
-n farmcontrol-server
-p "${output_dir}/"
-v "${version}"
--after-install packaging/linux/after-install.sh
--after-remove packaging/linux/after-remove.sh
"${binary}=/usr/bin/farmcontrol-server"
packaging/linux/farmcontrol-server.service=/lib/systemd/system/farmcontrol-server.service
)
fpm -t deb "${common_fpm_args[@]}"
fpm -t rpm "${common_fpm_args[@]}"

View File

@ -2,7 +2,7 @@
import log4js from "log4js"; import log4js from "log4js";
import { loadConfig } from "../../config.js"; import { loadConfig } from "../../config.js";
import { ThermalPrinter, PrinterTypes } from "node-thermal-printer"; import { ThermalPrinter, PrinterTypes } from "node-thermal-printer";
import { convertPDFToImage } from "../../utils.js"; import { convertPDFToImage } from "../../pdfUtils.js";
const config = loadConfig(); const config = loadConfig();
const logger = log4js.getLogger("Receipt Printer Interface"); const logger = log4js.getLogger("Receipt Printer Interface");

61
src/pdfUtils.js Normal file
View File

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

View File

@ -1,7 +1,5 @@
import readline from "node:readline"; import readline from "node:readline";
import os from "os"; import os from "os";
import { pdf } from "pdf-to-img";
import sharp from "sharp";
let isPrompting = false; // prevent multiple prompts at the same time let isPrompting = false; // prevent multiple prompts at the same time
@ -56,71 +54,3 @@ export function getDeviceInfo() {
}, },
}; };
} }
/**
* Converts a PDF buffer or file path to an array of image buffers
* @param {Buffer|string} pdfInput - PDF buffer or file path
* @param {Object} options - Conversion options (optional)
* @param {number} options.scale - Scale factor for image resolution (default: 2)
* @param {number} options.width - Target width in pixels (will calculate scale if provided)
* @param {number} options.height - Target height in pixels (optional)
* @param {number[]} options.page_numbers - Array of page numbers to convert (optional, converts all pages by default)
* @returns {Promise<Buffer[]>} Array of image buffers, one for each page
*/
export async function convertPDFToImage(pdfInput, options = {}) {
try {
// pdf-to-img uses scale instead of width/height directly
// Default scale of 2 provides good quality for thermal printers
let scale = options.scale || 2;
const pdfOptions = {
scale,
...options,
};
const document = await pdf(pdfInput, pdfOptions);
const outputImages = [];
// If specific page numbers are requested, use getPage() for each
if (
options.page_numbers &&
Array.isArray(options.page_numbers) &&
options.page_numbers.length > 0
) {
for (const pageNum of options.page_numbers) {
// page numbers are 1-indexed in the API
let image = await document.getPage(pageNum);
// Resize with Sharp if width or height are provided
if (options.width || options.height) {
const resizeOptions = {};
if (options.width) resizeOptions.width = options.width;
if (options.height) resizeOptions.height = options.height;
image = await sharp(image).resize(resizeOptions).toBuffer();
}
outputImages.push(image);
}
} else {
// Convert all pages: async iterable to array
for await (const image of document) {
let processedImage = image;
// Resize with Sharp if width or height are provided
if (options.width || options.height) {
const resizeOptions = {};
if (options.width) resizeOptions.width = options.width;
if (options.height) resizeOptions.height = options.height;
processedImage = await sharp(image).resize(resizeOptions).toBuffer();
}
outputImages.push(processedImage);
}
}
return outputImages; // Array of image buffers
} catch (error) {
console.error("Error converting PDF to image:", error);
throw error;
}
}