WIP joi validation for impose

This commit is contained in:
Felix Kaspar 2023-11-27 23:35:18 +01:00
parent 09aa3a8bc9
commit ba2588ea24
7 changed files with 154 additions and 115 deletions

View File

@ -42,10 +42,7 @@ router.post("/:workflowUuid?", [
} }
} }
// TODO: Replace with static multer function of pdffile const inputs = PdfFile.fromMulterFiles(req.files as Express.Multer.File[]);
const inputs = await Promise.all((req.files as Express.Multer.File[]).map(async file => {
return new PdfFile(file.originalname.replace(/\.[^/.]+$/, ""), new Uint8Array(await file.buffer), RepresentationType.Uint8Array, file.originalname.replace(/\.[^/.]+$/, ""));
}));
// Allow option to do it synchronously and just make a long request // Allow option to do it synchronously and just make a long request
if(req.body.async === "false") { if(req.body.async === "false") {

View File

@ -1,6 +1,6 @@
import Joi from 'joi'; import Joi from 'joi';
import { PdfFileSchema } from '../wrappers/PdfFile'; import { JoiPdfFileSchema } from '../wrappers/PdfFile';
export class RecordConstraint { export class RecordConstraint {
@ -55,10 +55,10 @@ export class FieldConstraint {
} else if (typeof this.type == 'string') { } else if (typeof this.type == 'string') {
switch (this.type) { switch (this.type) {
case "file.pdf": case "file.pdf":
schema = PdfFileSchema; schema = JoiPdfFileSchema;
break; break;
case "files.pdf": case "files.pdf":
schema = Joi.array().items(PdfFileSchema); schema = Joi.array().items(JoiPdfFileSchema);
break; break;
case "string": case "string":
schema = Joi.string(); schema = Joi.string();

View File

@ -1,86 +1,59 @@
import { PdfFile, RepresentationType } from "../wrappers/PdfFile"; import { PdfFile, RepresentationType } from "../wrappers/PdfFile";
import { FieldConstraint, RecordConstraint } from '../dynamic-ui/OperatorConstraints' import { FieldConstraint, RecordConstraint } from '../dynamic-ui/OperatorConstraints'
import { IOType, Operator, Progress } from "."; import { Operator, Progress, oneToOne } from ".";
import * as pdfcpuWrapper from "#pdfcpu"; // This is updated by tsconfig.json/paths for the context (browser, node, etc.) this module is used in. import * as pdfcpuWrapper from "#pdfcpu"; // This is updated by tsconfig.json/paths for the context (browser, node, etc.) this module is used in.
export type ImposeParamsType = { import Joi from "joi";
file: PdfFile; import { JoiPDFFileSchema } from "../wrappers/PdfFileJoi";
/** Accepted values are 2, 3, 4, 8, 9, 12, 16 - see: {@link https://pdfcpu.io/generate/nup.html#n-up-value} */
nup: 2 | 3 | 4 | 8 | 9 | 12 | 16;
/** A0-A10, other formats available - see: {@link https://pdfcpu.io/paper.html} */ // TODO: This will be replaced by a real translator
format: string; const translationObject = {
operators: {
nup: {
friendlyName: "PDF-Imposition / PDF-N-Up",
description: "Put multiple pages of the input document into a single page of the output document.",
values: {
nup: {
friendlyName: "Page Format",
description: "The Page Size of the ouput document. Append L or P to force Landscape or Portrait."
},
format: {
friendlyName: "N-Up-Value",
description: "How many pages should be in one output page"
}
}
}
},
inputs: {
pdfFile: {
name: "PDF-File(s)",
description: "This operator takes a PDF-File(s) as input"
}
},
outputs: {
pdfFile: {
name: "PDF-File(s)",
description: "This operator outputs PDF-File(s)"
}
}
} }
export class Impose extends Operator { export class Impose extends Operator {
static type: string = "impose"; static type: string = "impose";
static mayInput: IOType = IOType.PDF; /**
static willOutput: IOType = IOType.PDF; * Validation
*/
/** PDF-Imposition, PDF-N-Up: Put multiple pages of the input document into a single page of the output document. - see: {@link https://en.wikipedia.org/wiki/N-up} */ static inputSchema = JoiPDFFileSchema.label(translationObject.inputs.pdfFile.name).description(translationObject.inputs.pdfFile.description);
async run(input: PdfFile[], progressCallback: (state: Progress) => void): Promise<PdfFile[]> { static valueSchema = Joi.object({
return this.oneToOne<PdfFile, PdfFile>(input, async (input, index, max) => { nup: Joi.number().integer().valid(2, 3, 4, 8, 9, 12, 16).required()
//TODO: Support custom Page Sizes .label(translationObject.operators.nup.values.nup.friendlyName).description(translationObject.operators.nup.values.nup.description)
// https://pdfcpu.io/generate/nup.html .example("3").example("4"),
const uint8Array = await pdfcpuWrapper.oneToOne( format: Joi.string().valid(...[
[
"pdfcpu.wasm",
"nup",
"-c",
"disable",
'f:' + this.actionValues.format,
"/output.pdf",
String(this.actionValues.nup),
"input.pdf",
],
await input.uint8Array
);
const result = new PdfFile(
input.originalFilename,
uint8Array,
RepresentationType.Uint8Array,
input.filename + "_imposed"
);
progressCallback({ curFileProgress: 1, operationProgress: index/max })
console.log("ImposeResult: ", result);
return result;
})
}
validate(): { valid: boolean; reason?: string | undefined; } {
let baseValidationResults = super.validate();
if(!baseValidationResults.valid)
return baseValidationResults;
// TODO: This should be ported to SaudF's RecordValidator
if(!this.actionValues.nup) {
return { valid: false, reason: "nup is not defined" }
}
if(!(ImposeParamConstraints.record["nup"].type as number[]).includes(parseInt(this.actionValues.nup))) {
return { valid: false, reason: "NUp accepted values are 2, 3, 4, 8, 9, 12, 16 - see: https://pdfcpu.io/generate/nup.html#n-up-value"}
}
if(!this.actionValues.format) {
return { valid: false, reason: "format is not defined" }
}
if(!(ImposeParamConstraints.record["format"].type as string[]).includes(this.actionValues.format)) {
return { valid: false, reason: "invalid fromat provided - see: https://pdfcpu.io/paper.html"}
}
return { valid: true }
}
}
export const ImposeParamConstraints = new RecordConstraint({
file: new FieldConstraint("display.key", "file.pdf", true, "hint.key"),
nup: new FieldConstraint("display.key", [2, 3, 4, 8, 9, 12, 16], true, "hint.key"),
format: new FieldConstraint("display.key", [
// ISO 216:1975 A // ISO 216:1975 A
"4A0", "2A0", "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "4A0", "2A0", "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10",
@ -119,5 +92,68 @@ export const ImposeParamConstraints = new RecordConstraint({
"JIS-B0", "JIS-B1", "JIS-B2", "JIS-B3", "JIS-B4", "JIS-B5", "JIS-B6", "JIS-B0", "JIS-B1", "JIS-B2", "JIS-B3", "JIS-B4", "JIS-B5", "JIS-B6",
"JIS-B7", "JIS-B8", "JIS-B9", "JIS-B10", "JIS-B11", "JIS-B12", "JIS-B7", "JIS-B8", "JIS-B9", "JIS-B10", "JIS-B11", "JIS-B12",
"Shirokuban4", "Shirokuban5", "Shirokuban6", "Kiku4", "Kiku5", "AB", "B40", "Shikisen" "Shirokuban4", "Shirokuban5", "Shirokuban6", "Kiku4", "Kiku5", "AB", "B40", "Shikisen"
].flatMap(size => [size, size + "P", size + "L"]), true, "hint.key"), ].flatMap(size => [size, size + "P", size + "L"]))
.required()
.label(translationObject.operators.nup.values.format.friendlyName).description(translationObject.operators.nup.values.format.description)
.example("A4").example("A3L")
});
static outputSchema = JoiPDFFileSchema.label(translationObject.outputs.pdfFile.name).description(translationObject.outputs.pdfFile.description);
static schema = Joi.object({
input: Impose.inputSchema.required(),
values: Impose.valueSchema.required(),
output: Impose.outputSchema.optional()
}).label(translationObject.operators.nup.friendlyName).description(translationObject.operators.nup.description);
/**
* Logic
*/
/** PDF-Imposition, PDF-N-Up: Put multiple pages of the input document into a single page of the output document. - see: {@link https://en.wikipedia.org/wiki/N-up} */
async run(input: PdfFile[], progressCallback: (state: Progress) => void): Promise<PdfFile[]> {
return oneToOne<PdfFile, PdfFile>(input, async (input, index, max) => {
//TODO: Support custom Page Sizes
// https://pdfcpu.io/generate/nup.html
const uint8Array = await pdfcpuWrapper.oneToOne(
[
"pdfcpu.wasm",
"nup",
"-c",
"disable",
'f:' + this.actionValues.format,
"/output.pdf",
String(this.actionValues.nup),
"input.pdf",
],
await input.uint8Array
);
const result = new PdfFile(
input.originalFilename,
uint8Array,
RepresentationType.Uint8Array,
input.filename + "_imposed"
);
progressCallback({ curFileProgress: 1, operationProgress: index/max })
console.log("ImposeResult: ", result);
return result;
}) })
}
validate(): { valid: boolean; reason?: string | undefined; } {
let baseValidationResults = super.validate();
if(!baseValidationResults.valid)
return baseValidationResults;
// TODO: Fully integrate joi in the base and remove this func
this.actionValues = Impose.valueSchema.validate(this.actionValues);
if(this.actionValues.error)
return { valid: false, reason: this.actionValues }
return { valid: true }
}
}

View File

@ -1,8 +1,5 @@
import { Action } from "../../declarations/Action"; import { Action } from "../../declarations/Action";
import Joi from "joi";
export enum IOType {
PDF, Image, Text // TODO: Extend with Document File Types
}
export interface ValidationResult { export interface ValidationResult {
valid: boolean, valid: boolean,
@ -17,12 +14,14 @@ export interface Progress {
} }
export class Operator { export class Operator {
/** The type of the operator in camelCase (impose, merge, etc.) */ /** The internal name of the operator in camelCase (impose, merge, etc.) */
static type: string; static type: string;
// This will most likely be needed in the node Editor /** The Joi validators & decorators */
static mayInput: IOType; static inputSchema: Joi.Schema;
static willOutput: IOType; static valueSchema: Joi.Schema;
static outputSchema: Joi.Schema;
static schema: Joi.Schema;
actionValues: any; actionValues: any;
@ -40,26 +39,25 @@ export class Operator {
} }
return { valid: true }; return { valid: true };
} }
}
/** This function should be used if the Operation may take multiple files as inputs and only outputs one file */ /** This function should be used if the Operation may take multiple files as inputs and only outputs one file */
protected async nToOne <I, O>(inputs: I[], callback: (input: I[]) => Promise<O>): Promise<O[]> { export async function nToOne <I, O>(inputs: I[], callback: (input: I[]) => Promise<O>): Promise<O[]> {
return [await callback(inputs)]; return [await callback(inputs)];
} }
/** This function should be used if the Operation takes one file as input and may output multiple files */ /** This function should be used if the Operation takes one file as input and may output multiple files */
protected async oneToN <I, O>(inputs: I[], callback: (input: I, index: number, max: number) => Promise<O[]>): Promise<O[]> { export async function oneToN <I, O>(inputs: I[], callback: (input: I, index: number, max: number) => Promise<O[]>): Promise<O[]> {
let output: O[] = [] let output: O[] = []
for (let i = 0; i < inputs.length; i++) { for (let i = 0; i < inputs.length; i++) {
output = output.concat(await callback(inputs[i], i, inputs.length)); output = output.concat(await callback(inputs[i], i, inputs.length));
} }
return output; return output;
} }
/** This function should be used if the Operation takes one file as input and outputs only one file */ /** This function should be used if the Operation takes one file as input and outputs only one file */
protected async oneToOne <I, O>(inputs: I[], callback: (input: I, index: number, max: number) => Promise<O>): Promise<O[]> { export async function oneToOne <I, O>(inputs: I[], callback: (input: I, index: number, max: number) => Promise<O>): Promise<O[]> {
return this.oneToN(inputs, async (input, index, max) => { return oneToN(inputs, async (input, index, max) => {
return [await callback(input, index, max)] return [await callback(input, index, max)]
}); });
} }
}

View File

@ -1,7 +1,7 @@
import Joi from 'joi'; import Joi from 'joi';
import { PDFPage } from 'pdf-lib'; import { PDFPage } from 'pdf-lib';
import { PdfFile, RepresentationType, PdfFileSchema } from '../wrappers/PdfFile'; import { PdfFile, RepresentationType, JoiPdfFileSchema } from '../wrappers/PdfFile';
const whSchema = Joi.string().custom((value, helpers) => { const whSchema = Joi.string().custom((value, helpers) => {
console.log("value.pageSize", typeof value) console.log("value.pageSize", typeof value)
@ -23,7 +23,7 @@ const whSchema = Joi.string().custom((value, helpers) => {
}); });
export const ScalePageSchema = Joi.object({ export const ScalePageSchema = Joi.object({
file: PdfFileSchema.required(), file: JoiPdfFileSchema.required(),
pageSize: Joi.alternatives().try(whSchema, Joi.array().items(whSchema)).required(), pageSize: Joi.alternatives().try(whSchema, Joi.array().items(whSchema)).required(),
}); });

View File

@ -99,6 +99,7 @@ export class PdfFile {
static fromMulterFile(value: Express.Multer.File): PdfFile { static fromMulterFile(value: Express.Multer.File): PdfFile {
return new PdfFile(value.originalname, value.buffer as Uint8Array, RepresentationType.Uint8Array); return new PdfFile(value.originalname, value.buffer as Uint8Array, RepresentationType.Uint8Array);
} }
static fromMulterFiles(values: Express.Multer.File[]): PdfFile[] { static fromMulterFiles(values: Express.Multer.File[]): PdfFile[] {
return values.map(v => PdfFile.fromMulterFile(v)); return values.map(v => PdfFile.fromMulterFile(v));
@ -129,10 +130,3 @@ export class PdfFile {
return docCache; return docCache;
} }
} }
export const PdfFileSchema = Joi.any().custom((value) => {
if (!(value instanceof PdfFile)) {
throw new Error('value is not a PdfFile');
}
return value;
}, "PdfFile validation");

View File

@ -0,0 +1,14 @@
import Joi from "joi";
import { PdfFile } from "./PdfFile";
export const JoiPDFFileSchema = Joi.binary().custom((value: Express.Multer.File[] | PdfFile, helpers) => {
if (!(value instanceof PdfFile)) {
try {
return PdfFile.fromMulterFiles(value);
} catch (error) {
console.error(error);
throw new Error('value is not of type PdfFile');
}
}
return value;
}, "pdffile validation");