Compare commits

..

No commits in common. "0391559000271f77507733103e84e7d040d24005" and "24f0af411b17e5fd020e001f44f17774335aba2a" have entirely different histories.

8 changed files with 6825 additions and 5348 deletions

11
.eslintrc.json Normal file
View File

@ -0,0 +1,11 @@
{
"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,5 +106,4 @@ dist
.DS_Store .DS_Store
gerber/ gerber/
hello.txt hello.txt
test/tmp/* test/tmp/*
test/archiveTest

View File

@ -5,12 +5,10 @@
**This is still being developed and isn't ready for production!** **This is still being developed and isn't ready for production!**
Only tested with gerbers generated by EAGLE. Only tested with gerbers generated by EAGLE.
node version 20 or higher recommended. Requires node version 10 or higher.
## Usage ## Usage
### Save a PNG file ### Save a PNG file
``` ```
const { ImageGenerator } = require('@nplayfair/npe_gerber'); const { ImageGenerator } = require('@nplayfair/npe_gerber');
@ -43,9 +41,7 @@ fileProc.gerberToImage(gerberArchive)
console.log(`Generated image ${filename}`); console.log(`Generated image ${filename}`);
}) })
``` ```
### Return a PNG stream ### Return a PNG stream
``` ```
const { ImageGenerator } = require('@nplayfair/npe_gerber'); const { ImageGenerator } = require('@nplayfair/npe_gerber');
@ -78,7 +74,6 @@ fileProc.gerberToStream(gerberArchive)
// Do something with the stream // Do something with the stream
}) })
``` ```
## Layer Names ## 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. 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.

View File

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

11750
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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