Compare commits

...

11 Commits

Author SHA1 Message Date
Nick Playfair
0391559000 0.3.0 2025-06-14 22:18:56 +01:00
Nick Playfair
99793599dd Remove most dependency on fs-extra 2025-06-14 22:14:45 +01:00
Nick Playfair
d0544aa518 Properly test extraction methods 2025-06-14 21:12:42 +01:00
Nick Playfair
fab6f685cc replace all fs.existsSync with native node 2025-06-13 22:11:46 +01:00
Nick Playfair
809f1204b6 Refactored tests 2025-06-13 22:02:55 +01:00
Nick Playfair
30d873048b start refactoring tests 2025-06-13 21:12:39 +01:00
Nick Playfair
5a0641904a Use default node fs for dir checking 2025-06-13 21:12:24 +01:00
Nick Playfair
0b41a4e56f Fix formatting and add a few comments 2025-06-13 19:37:22 +01:00
Nick Playfair
2b38c1256b Update README 2025-06-13 19:37:10 +01:00
Nick Playfair
55127056e9 New eslint config 2025-06-13 18:55:24 +01:00
Nick Playfair
2660d9ba40 Update deps 2025-06-13 18:55:11 +01:00
8 changed files with 5421 additions and 6898 deletions

View File

@ -1,11 +0,0 @@
{
"extends": ["airbnb-base", "prettier", "plugin:node/recommended"],
"env": {
"node": true
},
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error",
"no-console": "off"
}
}

3
.gitignore vendored
View File

@ -106,4 +106,5 @@ dist
.DS_Store
gerber/
hello.txt
test/tmp/*
test/tmp/*
test/archiveTest

View File

@ -5,10 +5,12 @@
**This is still being developed and isn't ready for production!**
Only tested with gerbers generated by EAGLE.
Requires node version 10 or higher.
node version 20 or higher recommended.
## Usage
### Save a PNG file
```
const { ImageGenerator } = require('@nplayfair/npe_gerber');
@ -41,7 +43,9 @@ fileProc.gerberToImage(gerberArchive)
console.log(`Generated image ${filename}`);
})
```
### Return a PNG stream
```
const { ImageGenerator } = require('@nplayfair/npe_gerber');
@ -74,6 +78,7 @@ fileProc.gerberToStream(gerberArchive)
// Do something with the stream
})
```
## Layer Names
The constructor must be passed an array of filenames that correspond to the layers in the gerber archives you expect to be uploaded. The example above shows which names are used by EAGLE but other applications may use different filenames.

17
eslint.config.mjs Normal file
View File

@ -0,0 +1,17 @@
import js from '@eslint/js';
import globals from 'globals';
import { defineConfig, globalIgnores } from 'eslint/config';
export default defineConfig([
globalIgnores(['test/**/*', 'node_modules/**/*']),
{
files: ['**/*.{js,mjs,cjs}'],
plugins: { js },
extends: ['js/recommended'],
},
{ files: ['**/*.js'], languageOptions: { sourceType: 'commonjs' } },
{
files: ['**/*.{js,mjs,cjs}'],
languageOptions: { globals: globals.browser },
},
]);

View File

@ -1,10 +1,29 @@
//Modules
const AdmZip = require('adm-zip');
const fs = require('fs-extra');
const { emptyDirSync } = require('fs-extra');
const path = require('path');
const pcbStackup = require('pcb-stackup');
const sharp = require('sharp');
const { Readable } = require('stream');
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;
@ -12,10 +31,18 @@ class ImageGenerator {
this.imgConfig = imgConfig;
this.layerNames = layerNames;
// Ensure that the folders exist
//Ensure folders exist
try {
fs.ensureDirSync(this.tmpDir);
fs.ensureDirSync(this.imgDir);
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);
}
@ -25,15 +52,15 @@ class ImageGenerator {
* 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 {Promise} Promise object represents number of files extracted
* @returns {number} Number of objects contained in the archive
*/
static extractArchive(fileName, tmpDir) {
// Check archive exists
try {
if (!fs.existsSync(fileName)) {
if (!existsSync(fileName)) {
throw Error('Archive does not exist.');
}
if (!fs.existsSync(tmpDir)) {
if (!existsSync(tmpDir)) {
throw Error('Temporary folder does not exist.');
}
} catch (e) {
@ -42,6 +69,34 @@ class ImageGenerator {
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);
}
}
/**
@ -53,19 +108,19 @@ class ImageGenerator {
static getLayers(dir, layerNames) {
return new Promise((resolve, reject) => {
// Make sure the directory exists
if (!fs.existsSync(dir)) {
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 (!fs.existsSync(path.join(dir, layer))) layersValid = false;
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: fs.createReadStream(path.join(dir, layerName)),
gerber: createReadStream(path.join(dir, layerName)),
}));
return resolve(layers);
});
@ -78,7 +133,7 @@ class ImageGenerator {
static cleanupFiles(dir) {
try {
const folder = path.join(dir, 'archive');
fs.emptyDirSync(folder);
emptyDirSync(folder);
} catch (err) {
throw new Error(err);
}
@ -93,20 +148,21 @@ class ImageGenerator {
gerberToImage(gerber) {
// Create output dir if it doesn't exist
try {
fs.ensureDirSync(this.imgDir, 0o644);
// fs.ensureDirSync(this.imgDir, 0o644);
ensureDirSync(this.imgDir);
} catch (e) {
throw new Error(e);
}
// Check temp and output dirs exist
try {
if (!fs.existsSync(gerber)) {
if (!existsSync(gerber)) {
throw Error('Archive does not exist.');
}
if (!fs.existsSync(this.tmpDir)) {
if (!existsSync(this.tmpDir)) {
throw Error('Temporary folder does not exist.');
}
if (!fs.existsSync(this.imgDir)) {
if (!existsSync(this.imgDir)) {
throw Error('Output folder does not exist.');
}
} catch (e) {
@ -121,7 +177,7 @@ class ImageGenerator {
ImageGenerator.extractArchive(gerber, this.tmpDir);
ImageGenerator.getLayers(
path.join(this.tmpDir, 'archive'),
this.layerNames
this.layerNames,
)
.then(pcbStackup)
.then((stackup) => {
@ -152,13 +208,13 @@ class ImageGenerator {
gerberToStream(gerber) {
// Check temp and output dirs exist
try {
if (!fs.existsSync(gerber)) {
if (!existsSync(gerber)) {
throw Error('Archive does not exist.');
}
if (!fs.existsSync(this.tmpDir)) {
if (!existsSync(this.tmpDir)) {
throw Error('Temporary folder does not exist.');
}
if (!fs.existsSync(this.imgDir)) {
if (!existsSync(this.imgDir)) {
throw Error('Output folder does not exist.');
}
} catch (e) {
@ -169,7 +225,7 @@ class ImageGenerator {
ImageGenerator.extractArchive(gerber, this.tmpDir);
ImageGenerator.getLayers(
path.join(this.tmpDir, 'archive'),
this.layerNames
this.layerNames,
)
.then(pcbStackup)
.then((stackup) => {

11896
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,11 @@
{
"name": "@nplayfair/npe_gerber",
"version": "0.2.0",
"version": "0.3.0",
"description": "Create a PCB image from gerber files",
"main": "index.js",
"scripts": {
"test": "jest"
"test": "jest",
"lint": "eslint ."
},
"repository": {
"type": "git",
@ -22,22 +23,17 @@
"gerber"
],
"dependencies": {
"adm-zip": "^0.5.2",
"fs-extra": "^9.1.0",
"jszip": "^3.5.0",
"pcb-stackup": "^4.2.5",
"sharp": "^0.27.0"
"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": "^7.19.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-node": "^4.1.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"jest": "^26.6.3",
"prettier": "^2.2.1"
"@eslint/js": "^9.29.0",
"eslint": "^9.29.0",
"globals": "^16.2.0",
"jest": "^30.0.0",
"prettier": "^3.5.3"
}
}

View File

@ -1,7 +1,6 @@
/* eslint-disable */
const path = require('path');
const fs = require('fs-extra');
const Readable = require('stream').Readable;
const { readdirSync, ReadStream } = require('node:fs');
const { Readable } = require('node:stream');
const { ImageGenerator } = require('../index.js');
require('../index.js');
@ -9,6 +8,7 @@ const testGerber = path.join(__dirname, 'Arduino-Pro-Mini.zip');
const incompleteGerber = path.join(__dirname, 'incomplete.zip');
const testLayers = path.join(__dirname, 'layers');
const emptyFolder = path.join(__dirname, 'layers', 'Empty');
const archiveTestFolder = path.join(__dirname, 'archiveTest');
const folderConfig = {
tmpDir: path.join(__dirname, 'tmp'),
imgDir: path.join(__dirname, 'tmp'),
@ -17,6 +17,22 @@ const noTempConfig = {
tmpDir: emptyFolder,
imgDir: path.join(__dirname, 'tmp'),
};
const tmpNotExist = {
tmpDir: path.join(__dirname, 'InvalidFolderName'),
imgDir: path.join(__dirname, 'tmp'),
};
const imgNotExist = {
tmpDir: path.join(__dirname, 'tmp'),
imgDir: path.join(__dirname, 'InvalidFolderName'),
};
const tmpBadPerms = {
tmpDir: path.join(__dirname, 'badPerms'),
imgDir: path.join(__dirname, 'tmp'),
};
const imgBadPerms = {
tmpDir: path.join(__dirname, 'tmp'),
imgDir: path.join(__dirname, 'badPerms'),
};
const noImageConfig = {
tmpDir: path.join(__dirname, 'tmp'),
imgDir: emptyFolder,
@ -37,115 +53,162 @@ const layerNames = [
const fileProc = new ImageGenerator(folderConfig, imgConfig, layerNames);
const fileProcNoTemp = new ImageGenerator(noTempConfig, imgConfig, layerNames);
const fileProcNoImage = new ImageGenerator(noImageConfig, imgConfig, layerNames);
const fileProcNoImage = new ImageGenerator(
noImageConfig,
imgConfig,
layerNames,
);
/**************
* Tests
***************/
// Test constructor
test('Create ImageGenerator object with the passed in config values', () => {
describe('Creating an ImageGenerator object', () => {
const imgGen = new ImageGenerator(folderConfig, imgConfig);
expect(imgGen).toBeInstanceOf(ImageGenerator);
test('should create a valid object when passed the correct files and configuration', () => {
expect(imgGen).toBeInstanceOf(ImageGenerator);
});
// Image processing configuration
expect(imgGen.imgConfig.resizeWidth).toBe(600);
expect(imgGen.imgConfig.density).toBe(1000);
expect(imgGen.imgConfig.compLevel).toBe(1);
// Folders
expect(imgGen.tmpDir).toBe(path.join(__dirname, 'tmp'));
expect(imgGen.imgDir).toBe(path.join(__dirname, 'tmp'));
})
// getLayers
test('Promise of an array of layers from a given folder', () => {
expect.assertions(1);
return ImageGenerator.getLayers(testLayers, layerNames).then((data) => {
expect(data).toEqual(
expect.arrayContaining([
expect.objectContaining({
filename: expect.any(String),
gerber: expect.any(fs.ReadStream),
}),
])
);
test('image width should be 600', () => {
expect(imgGen.imgConfig.resizeWidth).toBe(600);
});
test('image density should be 1000', () => {
expect(imgGen.imgConfig.density).toBe(1000);
});
test('image compression level should be 1', () => {
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'));
});
});
test('Non-existent folder should reject promise with error', () => {
expect.assertions(1);
return expect(ImageGenerator.getLayers('./invalid_folder', layerNames)).rejects.toThrow(
new Error('Layers folder does not exist.')
);
// Testing folder config
describe('Passing in', () => {
test('a non-existent tmp folder should throw error', () => {
expect(() => {
new ImageGenerator(tmpNotExist, imgConfig);
}).toThrow();
});
test('a tmp folder with invalid permissions should throw error', () => {
expect(() => {
new ImageGenerator(tmpBadPerms, imgConfig);
}).toThrow();
});
test('a non-existent img folder should throw error', () => {
expect(() => {
new ImageGenerator(imgNotExist, imgConfig);
}).toThrow();
});
test('an img folder with invalid permissions should throw error', () => {
expect(() => {
new ImageGenerator(imgBadPerms, imgConfig);
}).toThrow();
});
});
test('Folder with incorrect number of layers should reject promise with error', () => {
expect.assertions(1);
return expect(ImageGenerator.getLayers(emptyFolder, layerNames)).rejects.toThrow(
new Error('Layer not found.')
);
// 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),
}),
]),
);
});
});
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 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.'));
});
});
// extractArchive
test('Non-existent archive should throw an error', () => {
expect(() =>
ImageGenerator.extractArchive('invalid.zip', folderConfig.tmpDir).toThrow(Error)
);
//Archive methods
describe('When extracting an archive', () => {
test('a non-existent archive should throw an error', () => {
expect(() =>
ImageGenerator.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);
});
test('it should load the archive and return the number of files extracted', () => {
expect(() => {
ImageGenerator.testArchive(testGerber, archiveTestFolder);
}).not.toThrow();
expect(ImageGenerator.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);
const dirents = readdirSync(archiveTestFolder, {
recursive: true,
withFileTypes: true,
});
const numOutputFiles = dirents.filter((dirent) => dirent.isFile());
expect(numOutputFiles).toHaveLength(12);
});
});
test('Temp dir not existing should throw an error', () => {
expect(() =>
ImageGenerator.extractArchive(testGerber, './invalid_dir').toThrow(Error)
);
});
test('Should extract archive and resolve with the number of files extracted', () => {
expect(() => ImageGenerator.extractArchive(testGerber, folderConfig.tmpDir).toBe(12));
});
// gerberToImage
test('Temp dir not existing should throw an error', () => {
expect(() =>
fileProcNoTemp
.gerberToImage(testGerber)
.toThrow(new Error('Temporary folder does not exist.'))
);
});
test('Output dir not existing should throw an error', () => {
expect(() =>
fileProcNoImage
.gerberToImage(testGerber)
.toThrow(new Error('Output folder does not exist.'))
);
});
test('Invalid archive file should throw an error', () => {
expect(() =>
fileProc
.gerberToImage('invalid.zip')
.toThrow(new Error('Archive does not exist.'))
);
});
test('Archive with incomplete set of layers should throw an error', () => {
expect(() =>
fileProc
.gerberToImage(incompleteGerber)
.toThrow(Error)
);
});
test('Gerber archive should resolve promise and return a filename of an image', () => {
expect.assertions(1);
return expect(
fileProc.gerberToImage(testGerber)
).resolves.toEqual(expect.stringContaining('Arduino-Pro-Mini.png'));
});
// gerberToStream
test('Gerber archive should resolve promise and return a png stream', () => {
expect.assertions(1);
return expect(
fileProc.gerberToStream(testGerber)
).resolves.toBeInstanceOf(Readable);
//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.')),
);
});
test('output dir not existing should throw an error', () => {
expect(() =>
fileProcNoImage
.gerberToImage(testGerber)
.toThrow(new Error('Output folder does not exist.')),
);
});
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));
});
test('gerber archive should resolve promise and return a filename of an image', () => {
expect.assertions(1);
return expect(fileProc.gerberToImage(testGerber)).resolves.toEqual(
expect.stringContaining('Arduino-Pro-Mini.png'),
);
});
test('Gerber archive should resolve promise and return a png stream', () => {
expect.assertions(1);
return expect(fileProc.gerberToStream(testGerber)).resolves.toBeInstanceOf(
Readable,
);
});
});