feat(print): use IPP smartPrint and normalize IPP capabilities and job mapping

This commit is contained in:
2026-01-13 21:14:30 +00:00
parent 716347bac1
commit b89e8cbc3c
5 changed files with 1279 additions and 333 deletions

View File

@@ -1,5 +1,15 @@
# Changelog # Changelog
## 2026-01-13 - 3.1.0 - feat(print)
use IPP smartPrint and normalize IPP capabilities and job mapping
- Use IppProtocol.smartPrint for automatic format detection/conversion when submitting print jobs.
- Normalize and map IIppJob -> IPrintJob via mapIppJobToInternal, collapsing extended IPP job states into internal states.
- Parse IIppPrinterCapabilities fields (mediaSizeSupported, mediaTypeSupported, sidesSupported, printQualitySupported, copiesSupported) and derive supportsDuplex from sidesSupported and maxCopies from copiesSupported range with a fallback.
- Map numeric IPP printQuality values (3,4,5) to internal quality strings (draft, normal, high).
- Switched calls to getPrinterAttributes/getJobAttributes and adjusted job listing to map returned IIppJob objects.
- Export new IPP types from protocols index: IIppPrinterCapabilities, IIppJob, IIppPrintOptions.
## 2026-01-12 - 3.0.2 - fix(devicemanager) ## 2026-01-12 - 3.0.2 - fix(devicemanager)
no changes detected - nothing to commit no changes detected - nothing to commit

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@ecobridge.xyz/devicemanager', name: '@ecobridge.xyz/devicemanager',
version: '3.0.2', version: '3.1.0',
description: 'a device manager for talking to devices on network and over usb' description: 'a device manager for talking to devices on network and over usb'
} }

View File

@@ -4,7 +4,7 @@
*/ */
import { Feature, type TDeviceReference } from './feature.abstract.js'; import { Feature, type TDeviceReference } from './feature.abstract.js';
import { IppProtocol } from '../protocols/index.js'; import { IppProtocol, type IIppPrinterCapabilities, type IIppJob } from '../protocols/index.js';
import type { import type {
TPrintProtocol, TPrintProtocol,
TPrintSides, TPrintSides,
@@ -16,7 +16,6 @@ import type {
IPrintFeatureInfo, IPrintFeatureInfo,
IFeatureOptions, IFeatureOptions,
} from '../interfaces/feature.interfaces.js'; } from '../interfaces/feature.interfaces.js';
import type { IPrinterCapabilities } from '../interfaces/index.js';
/** /**
* Options for creating a PrintFeature * Options for creating a PrintFeature
@@ -101,7 +100,7 @@ export class PrintFeature extends Feature {
this.ippClient = new IppProtocol(address, port, path); this.ippClient = new IppProtocol(address, port, path);
// Verify connection by getting printer attributes // Verify connection by getting printer attributes
const attrs = await this.ippClient.getAttributes(); const attrs = await this.ippClient.getPrinterAttributes();
this.updateCapabilitiesFromIpp(attrs); this.updateCapabilitiesFromIpp(attrs);
} }
// JetDirect and LPD don't need connection verification // JetDirect and LPD don't need connection verification
@@ -154,7 +153,8 @@ export class PrintFeature extends Feature {
throw new Error('Print feature not connected'); throw new Error('Print feature not connected');
} }
return this.ippClient.getJobs(); const jobs = await this.ippClient.getJobs();
return jobs.map(job => this.mapIppJobToInternal(job));
} }
/** /**
@@ -165,7 +165,8 @@ export class PrintFeature extends Feature {
throw new Error('Print feature not connected'); throw new Error('Print feature not connected');
} }
return this.ippClient.getJobInfo(jobId); const job = await this.ippClient.getJobAttributes(jobId);
return this.mapIppJobToInternal(job);
} }
/** /**
@@ -191,9 +192,17 @@ export class PrintFeature extends Feature {
this.emit('print:started', options); this.emit('print:started', options);
// IppProtocol.print() accepts IPrintOptions and returns IPrintJob // Use smartPrint for auto format detection and conversion
const job = await this.ippClient.print(data, options); const ippJob = await this.ippClient.smartPrint(data, {
jobName: options?.jobName,
copies: options?.copies,
media: options?.mediaSize,
sides: options?.sides,
printQuality: options?.quality,
colorMode: options?.colorMode,
});
const job = this.mapIppJobToInternal(ippJob);
this.emit('print:submitted', job); this.emit('print:submitted', job);
return job; return job;
} }
@@ -202,58 +211,57 @@ export class PrintFeature extends Feature {
// Helper Methods // Helper Methods
// ============================================================================ // ============================================================================
private updateCapabilitiesFromIpp(caps: IPrinterCapabilities): void { private updateCapabilitiesFromIpp(caps: IIppPrinterCapabilities): void {
this.supportsColor = caps.colorSupported; this.supportsColor = caps.colorSupported;
this.supportsDuplex = caps.duplexSupported; // Derive duplexSupported from sidesSupported
this.maxCopies = caps.maxCopies; this.supportsDuplex = caps.sidesSupported?.some(s =>
s.includes('two-sided')
) ?? false;
// Get max copies from range
this.maxCopies = caps.copiesSupported?.upper ?? 99;
if (caps.mediaSizes && caps.mediaSizes.length > 0) { if (caps.mediaSizeSupported && caps.mediaSizeSupported.length > 0) {
this.supportedMediaSizes = caps.mediaSizes; this.supportedMediaSizes = caps.mediaSizeSupported;
} }
if (caps.mediaTypes && caps.mediaTypes.length > 0) { if (caps.mediaTypeSupported && caps.mediaTypeSupported.length > 0) {
this.supportedMediaTypes = caps.mediaTypes; this.supportedMediaTypes = caps.mediaTypeSupported;
} }
if (caps.sidesSupported && caps.sidesSupported.length > 0) { if (caps.sidesSupported && caps.sidesSupported.length > 0) {
this.supportedSides = caps.sidesSupported.filter((s): s is TPrintSides => this.supportedSides = caps.sidesSupported.filter((s): s is TPrintSides =>
['one-sided', 'two-sided-long-edge', 'two-sided-short-edge'].includes(s) ['one-sided', 'two-sided-long-edge', 'two-sided-short-edge'].includes(s)
); );
} }
if (caps.qualitySupported && caps.qualitySupported.length > 0) { // Map IPP quality values (3=draft, 4=normal, 5=high) to strings
this.supportedQualities = caps.qualitySupported.filter((q): q is TPrintQuality => if (caps.printQualitySupported && caps.printQualitySupported.length > 0) {
['draft', 'normal', 'high'].includes(q) const qualityMap: Record<number, TPrintQuality> = { 3: 'draft', 4: 'normal', 5: 'high' };
); this.supportedQualities = caps.printQualitySupported
.map(q => qualityMap[q])
.filter((q): q is TPrintQuality => q !== undefined);
} }
} }
private qualityToIpp(quality: TPrintQuality): number { /**
switch (quality) { * Map IIppJob to IPrintJob, normalizing extended states
case 'draft': return 3; */
case 'normal': return 4; private mapIppJobToInternal(job: IIppJob): IPrintJob {
case 'high': return 5; // Map extended IPP states to simpler internal states
default: return 4; const stateMap: Record<IIppJob['state'], IPrintJob['state']> = {
} 'pending': 'pending',
} 'pending-held': 'pending',
'processing': 'processing',
private mapIppJob(job: Record<string, unknown>): IPrintJob { 'processing-stopped': 'processing',
const stateMap: Record<number, IPrintJob['state']> = { 'canceled': 'canceled',
3: 'pending', 'aborted': 'aborted',
4: 'pending', 'completed': 'completed',
5: 'processing',
6: 'processing',
7: 'canceled',
8: 'aborted',
9: 'completed',
}; };
return { return {
id: job['job-id'] as number, id: job.id,
name: job['job-name'] as string ?? 'Unknown', name: job.name,
state: stateMap[(job['job-state'] as number) ?? 3] ?? 'pending', state: stateMap[job.state],
stateReason: (job['job-state-reasons'] as string[])?.[0], stateReason: job.stateReasons?.[0],
createdAt: new Date((job['time-at-creation'] as number) * 1000), createdAt: job.createdAt,
completedAt: job['time-at-completed'] completedAt: job.completedAt,
? new Date((job['time-at-completed'] as number) * 1000)
: undefined,
}; };
} }

View File

@@ -10,7 +10,12 @@ export { EsclProtocol } from './protocol.escl.js';
export { SaneProtocol } from './protocol.sane.js'; export { SaneProtocol } from './protocol.sane.js';
// IPP printer protocol // IPP printer protocol
export { IppProtocol } from './protocol.ipp.js'; export {
IppProtocol,
type IIppPrinterCapabilities,
type IIppJob,
type IIppPrintOptions,
} from './protocol.ipp.js';
// SNMP query protocol // SNMP query protocol
export { export {

File diff suppressed because it is too large Load Diff