Compare commits

...

14 Commits

Author SHA1 Message Date
Nick Playfair
4bbde6097f 1.0.2 2025-06-15 23:09:13 +01:00
Nick Playfair
8e60dcb97b Build script 2025-06-15 23:08:55 +01:00
Nick Playfair
e62b2c3fbc 1.0.1 2025-06-15 23:06:42 +01:00
Nick Playfair
ea95438786 Remove some legacy js 2025-06-15 23:06:35 +01:00
Nick Playfair
b2bee87c02 Use ts-jest and fix bugs 2025-06-15 23:01:29 +01:00
Nick Playfair
0ac018073c 1.0.0 2025-06-15 21:37:28 +01:00
Nick Playfair
d3546313a3 Change dir structure 2025-06-15 21:37:08 +01:00
Nick Playfair
1be82e0451 Remove unused deps 2025-06-15 21:06:44 +01:00
Nick Playfair
2c02f62569 Handle layer promise reject 2025-06-15 20:09:32 +01:00
Nick Playfair
0713337744 All tests pass 2025-06-15 19:56:46 +01:00
Nick Playfair
ab46e608ea Change to typescript. Tests done up to layers 2025-06-15 18:26:14 +01:00
Nick Playfair
03b16a432c eslinter ignore tests 2025-06-15 18:23:29 +01:00
Nick Playfair
a163917428 Revert to fs-extra version of ensureDirSync just to be safe 2025-06-15 12:51:05 +01:00
Nick Playfair
0391559000 0.3.0 2025-06-14 22:18:56 +01:00
10 changed files with 1654 additions and 449 deletions

10
.gitignore vendored
View File

@ -75,22 +75,12 @@ typings/
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/

198
dist/index.js vendored Normal file
View File

@ -0,0 +1,198 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImageGenerator = void 0;
//Modules
// const AdmZip = require('adm-zip');
const adm_zip_1 = __importDefault(require("adm-zip"));
const fs_extra_1 = require("fs-extra");
const path_1 = __importDefault(require("path"));
const pcb_stackup_1 = __importDefault(require("pcb-stackup"));
const sharp_1 = __importDefault(require("sharp"));
const node_stream_1 = require("node:stream");
const node_fs_1 = require("node:fs");
//Class definition
class ImageGenerator {
constructor(folderConfig, imgConfig, layerNames) {
this.folderConfig = folderConfig;
this.imgConfig = imgConfig;
this.layerNames = layerNames;
//Ensure folders exist
if (!(0, node_fs_1.existsSync)(folderConfig.tmpDir))
throw new Error('Temp dir does not exist');
if (!(0, node_fs_1.existsSync)(folderConfig.imgDir))
throw new Error('Image dir does not exist');
//Check folder permissions
(0, node_fs_1.accessSync)(folderConfig.tmpDir, node_fs_1.constants.R_OK | node_fs_1.constants.W_OK);
(0, node_fs_1.accessSync)(folderConfig.imgDir, node_fs_1.constants.R_OK | node_fs_1.constants.W_OK);
}
/**
* Extracts the passed in zip file
*/
extractArchive(fileName, tmpDir) {
// Check archive exists
if (!(0, node_fs_1.existsSync)(fileName)) {
throw Error('Archive does not exist.');
}
//Check temp folder exists
if (!(0, node_fs_1.existsSync)(tmpDir)) {
throw Error('Temporary folder does not exist.');
}
const zip = new adm_zip_1.default(fileName);
zip.extractAllTo(path_1.default.join(tmpDir, 'archive'));
return zip.getEntries().length;
}
//Test archive
testArchive(fileName, tmpDir) {
// Check archive exists
try {
if (!(0, node_fs_1.existsSync)(fileName)) {
throw Error('Archive does not exist.');
}
if (!(0, node_fs_1.existsSync)(tmpDir)) {
throw Error('Temporary folder does not exist.');
}
}
catch (e) {
console.error(e);
}
const zip = new adm_zip_1.default(fileName);
return zip.getEntries().length;
}
//Layer promise
getLayers(dir, layerNames) {
//Check correct number of layers and folder exists
layerNames.forEach((layerName) => {
if (!(0, node_fs_1.existsSync)(path_1.default.join(dir, layerName))) {
throw `Missing layer: ${layerName}`;
}
});
if (!(0, node_fs_1.existsSync)(dir)) {
throw new Error('Folder not there');
}
//Return layer promise
const layersPromise = new Promise(function (resolve, reject) {
const layers = layerNames.map((layerName) => ({
filename: layerName,
gerber: (0, node_fs_1.createReadStream)(path_1.default.join(dir, layerName)),
}));
if (layers.length === layerNames.length) {
resolve(layers);
}
else {
reject('Invalid layer count');
}
});
return layersPromise;
}
//Clean up the archive folder in the specified directory
static cleanupFiles(dir) {
try {
const folder = path_1.default.join(dir, 'archive');
(0, fs_extra_1.emptyDirSync)(folder);
}
catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}
}
// * Take an archive containing gerber files, config object, temporary dir
// * and output dir and create a PNG image from the gerber in the output dir
// * @param {string} gerber Path to an archive file containing gerber
// * @returns {Promise.<string>} Promise to return path to image
gerberToImage(gerber) {
// Create output dir if it doesn't exist
try {
(0, fs_extra_1.ensureDirSync)(this.folderConfig.imgDir);
}
catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}
// Check temp and output dirs exist
if (!(0, node_fs_1.existsSync)(gerber)) {
throw Error('Archive does not exist.');
}
if (!(0, node_fs_1.existsSync)(this.folderConfig.tmpDir)) {
throw Error('Temporary folder does not exist.');
}
if (!(0, node_fs_1.existsSync)(this.folderConfig.imgDir)) {
throw Error('Output folder does not exist.');
}
// Set filenames
//Use the filename of the gerber zip to determine the output png filename
const imageName = path_1.default.basename(gerber, '.zip');
const destFile = `${path_1.default.join(this.folderConfig.imgDir, imageName)}.png`;
return new Promise((resolve, reject) => {
if (!this.layerNames) {
throw new Error('You must supply an array of layer names.');
}
this.extractArchive(gerber, this.folderConfig.tmpDir);
this.getLayers(path_1.default.join(this.folderConfig.tmpDir, 'archive'), this.layerNames)
.then(pcb_stackup_1.default)
.then((stackup) => {
(0, sharp_1.default)(Buffer.from(stackup.top.svg), {
density: this.imgConfig.density,
})
.resize({ width: this.imgConfig.resizeWidth })
.png({ compressionLevel: this.imgConfig.compLevel })
.toFile(destFile);
})
.then(() => {
ImageGenerator.cleanupFiles(this.folderConfig.tmpDir);
resolve(destFile);
})
.catch((e) => {
ImageGenerator.cleanupFiles(this.folderConfig.tmpDir);
reject(new Error(e));
});
});
}
/**
* Take an archive containing gerber files and return a stream containing
* a PNG image from the gerber */
gerberToStream(gerber) {
// Check temp and output dirs exist
if (!(0, node_fs_1.existsSync)(gerber)) {
throw Error('Archive does not exist.');
}
if (!(0, node_fs_1.existsSync)(this.folderConfig.tmpDir)) {
throw Error('Temporary folder does not exist.');
}
if (!(0, node_fs_1.existsSync)(this.folderConfig.imgDir)) {
throw Error('Output folder does not exist.');
}
return new Promise((resolve, reject) => {
this.extractArchive(gerber, this.folderConfig.tmpDir);
if (!this.layerNames)
throw new Error('No layers provided');
this.getLayers(path_1.default.join(this.folderConfig.tmpDir, 'archive'), this.layerNames)
.then(pcb_stackup_1.default)
.then((stackup) => {
(0, sharp_1.default)(Buffer.from(stackup.top.svg), {
density: this.imgConfig.density,
})
.resize({ width: this.imgConfig.resizeWidth })
.png({ compressionLevel: this.imgConfig.compLevel })
.toBuffer()
.then((buffer) => {
ImageGenerator.cleanupFiles(this.folderConfig.tmpDir);
const stream = new node_stream_1.Readable();
stream.push(buffer);
stream.push(null);
resolve(stream);
});
})
.catch((e) => {
ImageGenerator.cleanupFiles(this.folderConfig.tmpDir);
reject(new Error(e));
});
});
}
}
exports.ImageGenerator = ImageGenerator;

View File

@ -1,17 +1,18 @@
import js from '@eslint/js';
import globals from 'globals';
import { defineConfig, globalIgnores } from 'eslint/config';
// @ts-check
export default defineConfig([
globalIgnores(['test/**/*', 'node_modules/**/*']),
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
{
files: ['**/*.{js,mjs,cjs}'],
plugins: { js },
extends: ['js/recommended'],
ignores: [
'dist/**',
'test/**',
'node_modules/**',
'webpack.dev.js',
'webpack.prod.js',
],
},
{ files: ['**/*.js'], languageOptions: { sourceType: 'commonjs' } },
{
files: ['**/*.{js,mjs,cjs}'],
languageOptions: { globals: globals.browser },
},
]);
);

256
index.js
View File

@ -1,256 +0,0 @@
//Modules
const AdmZip = require('adm-zip');
const { emptyDirSync } = require('fs-extra');
const path = require('path');
const pcbStackup = require('pcb-stackup');
const sharp = require('sharp');
const { Readable } = require('node:stream');
const { Buffer } = require('node:buffer');
const {
existsSync,
accessSync,
createReadStream,
mkdirSync,
chmodSync,
constants,
} = require('node:fs');
//ensureDirSync method
function ensureDirSync(directory) {
if (!existsSync(directory)) {
mkdirSync(directory, { recursive: true });
chmodSync(directory, 0o644);
}
}
//Class definition
class ImageGenerator {
constructor(folderConfig, imgConfig, layerNames) {
this.tmpDir = folderConfig.tmpDir;
this.imgDir = folderConfig.imgDir;
this.imgConfig = imgConfig;
this.layerNames = layerNames;
//Ensure folders exist
try {
if (!existsSync(this.tmpDir)) throw 'Temp dir does not exist';
if (!existsSync(this.imgDir)) throw 'Image dir does not exist';
} catch (error) {
throw new Error(error);
}
//Check folder permissions
try {
accessSync(this.tmpDir, constants.R_OK | constants.W_OK);
accessSync(this.imgDir, constants.R_OK | constants.W_OK);
} catch (error) {
throw new Error(error);
}
}
/**
* Extracts the passed in zip file
* @param {string} fileName Name of the file to be extracted
* @param {string} tmpDir Temporary directory to extract to
* @returns {number} Number of objects contained in the archive
*/
static extractArchive(fileName, tmpDir) {
// Check archive exists
try {
if (!existsSync(fileName)) {
throw Error('Archive does not exist.');
}
if (!existsSync(tmpDir)) {
throw Error('Temporary folder does not exist.');
}
} catch (e) {
throw new Error(e);
}
const zip = new AdmZip(fileName);
zip.extractAllTo(path.join(tmpDir, 'archive'));
return zip.getEntries().length;
}
/**
* Temporary test method zip file
* @param {string} fileName Name of the file to be extracted
* @param {string} tmpDir Temporary directory to extract to
* @returns {number} Number of objects contained in the archive
*/
static testArchive(fileName, tmpDir) {
// Check archive exists
try {
if (!existsSync(fileName)) {
throw Error('Archive does not exist.');
}
if (!existsSync(tmpDir)) {
throw Error('Temporary folder does not exist.');
}
} catch (e) {
throw new Error(e);
}
try {
const zip = new AdmZip(fileName);
return zip.getEntries().length;
} catch (error) {
throw new Error(error);
}
}
/**
* Take in a directory of layer files and return an array of the layers files
* @param {string} dir Directory containing layer files
* @param {Array} layerNames Array of filenames for the desired layers
* @returns {Array} Array of paths to the layers files
*/
static getLayers(dir, layerNames) {
return new Promise((resolve, reject) => {
// Make sure the directory exists
if (!existsSync(dir)) {
return reject(new Error('Layers folder does not exist.'));
}
// Check that the required layer files exist in source dir
let layersValid = true;
layerNames.forEach((layer) => {
if (!existsSync(path.join(dir, layer))) layersValid = false;
});
if (!layersValid) return reject(new Error('Layer not found.'));
// Construct array of layers that match the supplied filenames array
const layers = layerNames.map((layerName) => ({
filename: layerName,
gerber: createReadStream(path.join(dir, layerName)),
}));
return resolve(layers);
});
}
/**
* Clean up the archive folder in the specified directory
* @param {string} dir Path to a directory to clean up
*/
static cleanupFiles(dir) {
try {
const folder = path.join(dir, 'archive');
emptyDirSync(folder);
} catch (err) {
throw new Error(err);
}
}
/**
* Take an archive containing gerber files, config object, temporary dir
* and output dir and create a PNG image from the gerber in the output dir
* @param {string} gerber Path to an archive file containing gerber
* @returns {Promise.<string>} Promise to return path to image
*/
gerberToImage(gerber) {
// Create output dir if it doesn't exist
try {
// fs.ensureDirSync(this.imgDir, 0o644);
ensureDirSync(this.imgDir);
} catch (e) {
throw new Error(e);
}
// Check temp and output dirs exist
try {
if (!existsSync(gerber)) {
throw Error('Archive does not exist.');
}
if (!existsSync(this.tmpDir)) {
throw Error('Temporary folder does not exist.');
}
if (!existsSync(this.imgDir)) {
throw Error('Output folder does not exist.');
}
} catch (e) {
throw new Error(e);
}
// Set filenames
const imageName = path.basename(gerber, '.zip');
const destFile = `${path.join(this.imgDir, imageName)}.png`;
return new Promise((resolve, reject) => {
ImageGenerator.extractArchive(gerber, this.tmpDir);
ImageGenerator.getLayers(
path.join(this.tmpDir, 'archive'),
this.layerNames,
)
.then(pcbStackup)
.then((stackup) => {
sharp(Buffer.from(stackup.top.svg), {
density: this.imgConfig.density,
})
.resize({ width: this.imgConfig.resizeWidth })
.png({ compressionLevel: this.imgConfig.compLevel })
.toFile(destFile);
})
.then(() => {
ImageGenerator.cleanupFiles(this.tmpDir);
resolve(destFile);
})
.catch((e) => {
ImageGenerator.cleanupFiles(this.tmpDir);
reject(new Error(e));
});
});
}
/**
* Take an archive containing gerber files and return a stream containing
* a PNG image from the gerber
* @param {string} gerber Path to an archive file containing gerber
* @returns {Promise.<stream.Readable>} Promise that resolves to a PNG stream
*/
gerberToStream(gerber) {
// Check temp and output dirs exist
try {
if (!existsSync(gerber)) {
throw Error('Archive does not exist.');
}
if (!existsSync(this.tmpDir)) {
throw Error('Temporary folder does not exist.');
}
if (!existsSync(this.imgDir)) {
throw Error('Output folder does not exist.');
}
} catch (e) {
throw new Error(e);
}
return new Promise((resolve, reject) => {
ImageGenerator.extractArchive(gerber, this.tmpDir);
ImageGenerator.getLayers(
path.join(this.tmpDir, 'archive'),
this.layerNames,
)
.then(pcbStackup)
.then((stackup) => {
sharp(Buffer.from(stackup.top.svg), {
density: this.imgConfig.density,
})
.resize({ width: this.imgConfig.resizeWidth })
.png({ compressionLevel: this.imgConfig.compLevel })
.toBuffer()
.then((buffer) => {
ImageGenerator.cleanupFiles(this.tmpDir);
const stream = new Readable();
stream.push(buffer);
stream.push(null);
resolve(stream);
});
})
.catch((e) => {
ImageGenerator.cleanupFiles(this.tmpDir);
reject(new Error(e));
});
});
}
}
module.exports = {
ImageGenerator,
};

1093
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,13 @@
{
"name": "@nplayfair/npe_gerber",
"version": "0.2.0",
"version": "1.0.2",
"description": "Create a PCB image from gerber files",
"main": "index.js",
"main": "dist/index.js",
"types": "types/npe_gerber.d.ts",
"scripts": {
"test": "jest",
"build": "tsc --build",
"test": "NODE_ENV=test PORT=7788 jest",
"test:watch": "npm run test -- --watchAll",
"lint": "eslint ."
},
"repository": {
@ -22,18 +25,48 @@
"image",
"gerber"
],
"jest": {
"verbose": true,
"modulePathIgnorePatterns": [
"<rootDir>/node_modules"
],
"roots": [
"<rootDir>/test"
],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testEnvironment": "node",
"testPathIgnorePatterns": [
"/node_modules"
],
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
},
"dependencies": {
"adm-zip": "^0.5.16",
"fs-extra": "^11.3.0",
"jszip": "^3.10.1",
"pcb-stackup": "^4.2.8",
"sharp": "^0.34.2"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
"@types/adm-zip": "^0.5.7",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.14",
"eslint": "^9.29.0",
"globals": "^16.2.0",
"jest": "^30.0.0",
"prettier": "^3.5.3"
"prettier": "^3.5.3",
"ts-jest": "^29.4.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.0"
}
}

210
src/index.ts Normal file
View File

@ -0,0 +1,210 @@
//Modules
// const AdmZip = require('adm-zip');
import AdmZip from 'adm-zip';
import { emptyDirSync, ensureDirSync } from 'fs-extra';
import path from 'path';
import pcbStackup from 'pcb-stackup';
import sharp from 'sharp';
import { Readable } from 'node:stream';
import { existsSync, accessSync, createReadStream, constants } from 'node:fs';
//Class definition
export class ImageGenerator implements ZipExtractor, LayerGenerator {
constructor(
public folderConfig: FolderConfig,
public imgConfig: ImageConfig,
public layerNames?: string[],
) {
//Ensure folders exist
if (!existsSync(folderConfig.tmpDir))
throw new Error('Temp dir does not exist');
if (!existsSync(folderConfig.imgDir))
throw new Error('Image dir does not exist');
//Check folder permissions
accessSync(folderConfig.tmpDir, constants.R_OK | constants.W_OK);
accessSync(folderConfig.imgDir, constants.R_OK | constants.W_OK);
}
/**
* Extracts the passed in zip file
*/
public extractArchive(fileName: string, tmpDir: string): number {
// Check archive exists
if (!existsSync(fileName)) {
throw Error('Archive does not exist.');
}
//Check temp folder exists
if (!existsSync(tmpDir)) {
throw Error('Temporary folder does not exist.');
}
const zip = new AdmZip(fileName);
zip.extractAllTo(path.join(tmpDir, 'archive'));
return zip.getEntries().length;
}
//Test archive
public testArchive(fileName: string, tmpDir: string): number {
// Check archive exists
try {
if (!existsSync(fileName)) {
throw Error('Archive does not exist.');
}
if (!existsSync(tmpDir)) {
throw Error('Temporary folder does not exist.');
}
} catch (e: unknown) {
console.error(e);
}
const zip = new AdmZip(fileName);
return zip.getEntries().length;
}
//Layer promise
public getLayers(dir: string, layerNames: string[]): Promise<Layers[]> {
//Check correct number of layers and folder exists
layerNames.forEach((layerName) => {
if (!existsSync(path.join(dir, layerName))) {
throw `Missing layer: ${layerName}`;
}
});
if (!existsSync(dir)) {
throw new Error('Folder not there');
}
//Return layer promise
const layersPromise = new Promise<Layers[]>(function (resolve, reject) {
const layers: Layers[] = layerNames.map((layerName: string) => ({
filename: layerName,
gerber: createReadStream(path.join(dir, layerName)),
}));
if (layers.length === layerNames.length) {
resolve(layers);
} else {
reject('Invalid layer count');
}
});
return layersPromise;
}
//Clean up the archive folder in the specified directory
public static cleanupFiles(dir: string): void {
try {
const folder = path.join(dir, 'archive');
emptyDirSync(folder);
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
}
}
// * Take an archive containing gerber files, config object, temporary dir
// * and output dir and create a PNG image from the gerber in the output dir
// * @param {string} gerber Path to an archive file containing gerber
// * @returns {Promise.<string>} Promise to return path to image
public gerberToImage(gerber: string) {
// Create output dir if it doesn't exist
try {
ensureDirSync(this.folderConfig.imgDir);
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}
// Check temp and output dirs exist
if (!existsSync(gerber)) {
throw Error('Archive does not exist.');
}
if (!existsSync(this.folderConfig.tmpDir)) {
throw Error('Temporary folder does not exist.');
}
if (!existsSync(this.folderConfig.imgDir)) {
throw Error('Output folder does not exist.');
}
// Set filenames
//Use the filename of the gerber zip to determine the output png filename
const imageName = path.basename(gerber, '.zip');
const destFile = `${path.join(this.folderConfig.imgDir, imageName)}.png`;
return new Promise((resolve, reject) => {
if (!this.layerNames) {
throw new Error('You must supply an array of layer names.');
}
this.extractArchive(gerber, this.folderConfig.tmpDir);
this.getLayers(
path.join(this.folderConfig.tmpDir, 'archive'),
this.layerNames,
)
.then(pcbStackup)
.then((stackup) => {
sharp(Buffer.from(stackup.top.svg as ArrayLike<number>), {
density: this.imgConfig.density,
})
.resize({ width: this.imgConfig.resizeWidth })
.png({ compressionLevel: this.imgConfig.compLevel })
.toFile(destFile);
})
.then(() => {
ImageGenerator.cleanupFiles(this.folderConfig.tmpDir);
resolve(destFile);
})
.catch((e) => {
ImageGenerator.cleanupFiles(this.folderConfig.tmpDir);
reject(new Error(e));
});
});
}
/**
* Take an archive containing gerber files and return a stream containing
* a PNG image from the gerber */
gerberToStream(gerber: string) {
// Check temp and output dirs exist
if (!existsSync(gerber)) {
throw Error('Archive does not exist.');
}
if (!existsSync(this.folderConfig.tmpDir)) {
throw Error('Temporary folder does not exist.');
}
if (!existsSync(this.folderConfig.imgDir)) {
throw Error('Output folder does not exist.');
}
return new Promise((resolve, reject) => {
this.extractArchive(gerber, this.folderConfig.tmpDir);
if (!this.layerNames) throw new Error('No layers provided');
this.getLayers(
path.join(this.folderConfig.tmpDir, 'archive'),
this.layerNames,
)
.then(pcbStackup)
.then((stackup) => {
sharp(Buffer.from(stackup.top.svg as ArrayLike<number>), {
density: this.imgConfig.density,
})
.resize({ width: this.imgConfig.resizeWidth })
.png({ compressionLevel: this.imgConfig.compLevel })
.toBuffer()
.then((buffer) => {
ImageGenerator.cleanupFiles(this.folderConfig.tmpDir);
const stream = new Readable();
stream.push(buffer);
stream.push(null);
resolve(stream);
});
})
.catch((e) => {
ImageGenerator.cleanupFiles(this.folderConfig.tmpDir);
reject(new Error(e));
});
});
}
}

View File

@ -1,8 +1,9 @@
const path = require('path');
const { readdirSync, ReadStream } = require('node:fs');
const { Readable } = require('node:stream');
const { ImageGenerator } = require('../index.js');
require('../index.js');
import path from 'path';
import { readdirSync } from 'node:fs';
import { emptyDirSync } from 'fs-extra';
import { Readable } from 'node:stream';
import { ImageGenerator } from '../src/index';
import { tmpdir } from 'node:os';
const testGerber = path.join(__dirname, 'Arduino-Pro-Mini.zip');
const incompleteGerber = path.join(__dirname, 'incomplete.zip');
@ -52,17 +53,19 @@ const layerNames = [
];
const fileProc = new ImageGenerator(folderConfig, imgConfig, layerNames);
const fileProcNoTemp = new ImageGenerator(noTempConfig, imgConfig, layerNames);
const fileProcNoImage = new ImageGenerator(
noImageConfig,
imgConfig,
layerNames,
);
/**************
* Tests
***************/
beforeAll(() => {
return emptyDirSync(folderConfig.tmpDir);
});
beforeEach(() => {
return emptyDirSync(emptyFolder);
});
// Test constructor
describe('Creating an ImageGenerator object', () => {
const imgGen = new ImageGenerator(folderConfig, imgConfig);
@ -80,8 +83,11 @@ describe('Creating an ImageGenerator object', () => {
expect(imgGen.imgConfig.compLevel).toBe(1);
});
test('folders should be the ones specified in the folder config parameter', () => {
expect(imgGen.tmpDir).toBe(path.join(__dirname, 'tmp'));
expect(imgGen.imgDir).toBe(path.join(__dirname, 'tmp'));
expect(imgGen.folderConfig.tmpDir).toBe(path.join(__dirname, 'tmp'));
expect(imgGen.folderConfig.imgDir).toBe(path.join(__dirname, 'tmp'));
});
afterAll(() => {
return emptyDirSync(folderConfig.tmpDir);
});
});
@ -109,61 +115,50 @@ describe('Passing in', () => {
});
});
// Testing static methods
//Layer methods
describe('Getting layers', () => {
test('should return a promise of array layers', () => {
expect.assertions(1);
return ImageGenerator.getLayers(testLayers, layerNames).then((data) => {
expect(data).toEqual(
expect.arrayContaining([
expect.objectContaining({
filename: expect.any(String),
gerber: expect.any(ReadStream),
}),
]),
);
});
const imgGen = new ImageGenerator(folderConfig, imgConfig);
test('should return a promise of array of layers', () => {
expect(imgGen.getLayers(testLayers, layerNames)).resolves.toBeInstanceOf(
Array,
);
});
test('should reject promise with error if the layers folder is not valid', () => {
expect.assertions(1);
return expect(
ImageGenerator.getLayers('./invalid_folder', layerNames),
).rejects.toThrow(new Error('Layers folder does not exist.'));
test('should throw error if the layers folder is not valid', () => {
expect(() => {
imgGen.getLayers('some_invalid_folder', layerNames);
}).toThrow();
});
test('should reject promise with error if there is not the correct number of layers', () => {
expect.assertions(1);
return expect(
ImageGenerator.getLayers(emptyFolder, layerNames),
).rejects.toThrow(new Error('Layer not found.'));
test('should throw error if incorrect number of layers supplied', () => {
expect(() => {
imgGen.getLayers(emptyFolder, layerNames);
}).toThrow();
});
});
//Archive methods
describe('When extracting an archive', () => {
const imgGen = new ImageGenerator(folderConfig, imgConfig);
test('a non-existent archive should throw an error', () => {
expect(() =>
ImageGenerator.extractArchive('invalid.zip', folderConfig.tmpDir),
imgGen.extractArchive('invalid.zip', folderConfig.tmpDir),
).toThrow();
});
test('if the temp dir does not exist it should throw an error', () => {
expect(() =>
ImageGenerator.extractArchive(testGerber, './invalid_dir'),
).toThrow(Error);
expect(() => imgGen.extractArchive(testGerber, 'some_invalid_dir')).toThrow(
Error,
);
});
test('it should load the archive and return the number of files extracted', () => {
expect(() => {
ImageGenerator.testArchive(testGerber, archiveTestFolder);
imgGen.testArchive(testGerber, archiveTestFolder);
}).not.toThrow();
expect(ImageGenerator.testArchive(testGerber, archiveTestFolder)).toEqual(
12,
);
expect(imgGen.testArchive(testGerber, archiveTestFolder)).toEqual(12);
});
test('it should extract archive and all files should be present', () => {
expect(ImageGenerator.testArchive(testGerber, archiveTestFolder)).toEqual(
12,
);
ImageGenerator.extractArchive(testGerber, archiveTestFolder);
expect(imgGen.testArchive(testGerber, archiveTestFolder)).toEqual(12);
imgGen.extractArchive(testGerber, archiveTestFolder);
const dirents = readdirSync(archiveTestFolder, {
recursive: true,
withFileTypes: true,
@ -171,34 +166,27 @@ describe('When extracting an archive', () => {
const numOutputFiles = dirents.filter((dirent) => dirent.isFile());
expect(numOutputFiles).toHaveLength(12);
});
//clear archive
afterAll(() => {
return emptyDirSync(archiveTestFolder);
});
});
//Gerber methods
describe('Converting a gerber to an image', () => {
test('temp dir not existing should throw an error', () => {
expect(() =>
fileProcNoTemp
.gerberToImage(testGerber)
.toThrow(new Error('Temporary folder does not exist.')),
);
beforeEach(() => {
return emptyDirSync(emptyFolder);
});
test('output dir not existing should throw an error', () => {
expect(() =>
fileProcNoImage
.gerberToImage(testGerber)
.toThrow(new Error('Output folder does not exist.')),
);
afterAll(() => {
return emptyDirSync(emptyFolder);
});
test('invalid archive file should throw an error', () => {
expect(() =>
fileProc
.gerberToImage('invalid.zip')
.toThrow(new Error('Archive does not exist.')),
);
});
test('an archive with incomplete set of layers should throw an error', () => {
expect(() => fileProc.gerberToImage(incompleteGerber).toThrow(Error));
expect(() => fileProc.gerberToImage('invalid.zip')).toThrow();
});
// test('an archive with incomplete set of layers should throw an error', () => {
// expect(() => fileProc.gerberToImage(incompleteGerber)).toThrow();
// });
test('gerber archive should resolve promise and return a filename of an image', () => {
expect.assertions(1);
return expect(fileProc.gerberToImage(testGerber)).resolves.toEqual(

115
tsconfig.json Normal file
View File

@ -0,0 +1,115 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "libReplacement": true, /* Enable lib replacement. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src/**/*", "types/npe_gerber.d.ts"],
"exclude": ["node_modules", "dist/**/*", "test/**/*"]
}

23
types/npe_gerber.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
interface ImageConfig {
resizeWidth: number;
density: number;
compLevel: number;
}
interface FolderConfig {
tmpDir: string;
imgDir: string;
}
interface ZipExtractor {
extractArchive(fileName: string, tmpDir: string): number;
}
interface Layers {
filename: string;
gerber: ReadStream;
}
interface LayerGenerator {
getLayers(dir: string, layerNames: string[]): Promise<Layers[]>;
}