diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..74b3dde --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + node_version: [10, 12, 14] + name: ${{ matrix.os }} / Node v${{ matrix.node_version }} + runs-on: ${{ matrix.os }} + steps: + - run: git config --global core.autocrlf false + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node_version }} + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Restore yarn cache + uses: actions/cache@v1 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-node${{ matrix.node_version }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node${{ matrix.node_version }}-yarn- + - name: Install dependencies + run: yarn --frozen-lockfile + - name: Check code style (tslint) + run: yarn lint + - name: Check code style (Prettier) + run: yarn checkstyle + - name: Build + run: yarn build + - name: Run tests + run: yarn test --coverage + - name: Upload coverage + uses: coverallsapp/github-action@v1.1.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.huskyrc.json b/.huskyrc.json new file mode 100644 index 0000000..4d077c8 --- /dev/null +++ b/.huskyrc.json @@ -0,0 +1,5 @@ +{ + "hooks": { + "pre-commit": "lint-staged" + } +} diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..3389bd8 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,3 @@ +{ + "*.{js,jsx,ts,tsx,json}": ["prettier --write", "tslint --fix"] +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..de66fcc --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +/coverage/ +/lib/ +/mod/ +__test__/ +__snapshots__/ +src/**/__test__ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7f41367 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "trailingComma": "all", + "arrowParens": "always", + "singleQuote": true +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fef58ef..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -node_js: - - "10" - - "12" -script: npm run test:coveralls diff --git a/bench.ts b/bench.ts index 2c2ebe5..256e007 100644 --- a/bench.ts +++ b/bench.ts @@ -8,9 +8,7 @@ import * as imageCodecTxi from '.'; import { join } from 'path'; const inputPNG = PNG.sync.read( - fs.readFileSync( - join(__dirname, 'src', '__test__', 'transparency-rgba.png'), - ), + fs.readFileSync(join(__dirname, 'src', '__test__', 'transparency-rgba.png')), ); const inputImage = { @@ -21,35 +19,37 @@ const inputImage = { const suite = new benchmark.Suite(); -suite.add('RGBA8888', () => { - imageCodecTxi.encode(inputImage, { - rle: 'auto', - outputFormat: imageCodecTxi.TXIOutputFormat.RGBA8888, - }); -}).add('RGBA6666', () => { - imageCodecTxi.encode(inputImage, { - rle: 'auto', - outputFormat: imageCodecTxi.TXIOutputFormat.RGBA6666, - }); -}) -.add('RGB565', () => { - imageCodecTxi.encode(inputImage, { - rle: 'auto', - outputFormat: imageCodecTxi.TXIOutputFormat.RGB565, - }); -}) -.add('A8', () => { - imageCodecTxi.encode(inputImage, { - rle: 'auto', - outputFormat: imageCodecTxi.TXIOutputFormat.A8, - }); -}) -.on('cycle', (event: any) => { - console.log(String(event.target)); - console.log(`${(event.target.stats.mean * 1000).toFixed(2)} ms/run`); -}) -.on('error', (event: any) => { - console.error(`Error running ${event.target.name}:`); - console.error(event.target.error); -}) -.run({ async: true }); +suite + .add('RGBA8888', () => { + imageCodecTxi.encode(inputImage, { + rle: 'auto', + outputFormat: imageCodecTxi.TXIOutputFormat.RGBA8888, + }); + }) + .add('RGBA6666', () => { + imageCodecTxi.encode(inputImage, { + rle: 'auto', + outputFormat: imageCodecTxi.TXIOutputFormat.RGBA6666, + }); + }) + .add('RGB565', () => { + imageCodecTxi.encode(inputImage, { + rle: 'auto', + outputFormat: imageCodecTxi.TXIOutputFormat.RGB565, + }); + }) + .add('A8', () => { + imageCodecTxi.encode(inputImage, { + rle: 'auto', + outputFormat: imageCodecTxi.TXIOutputFormat.A8, + }); + }) + .on('cycle', (event: any) => { + console.log(String(event.target)); + console.log(`${(event.target.stats.mean * 1000).toFixed(2)} ms/run`); + }) + .on('error', (event: any) => { + console.error(`Error running ${event.target.name}:`); + console.error(event.target.error); + }) + .run({ async: true }); diff --git a/jest.config.js b/jest.config.js index df079e7..70f18ca 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,10 +2,7 @@ module.exports = { transform: { '^.+\\.(ts|js)$': 'ts-jest', }, - moduleFileExtensions: [ - 'ts', - 'js', - ], + moduleFileExtensions: ['ts', 'js'], testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|js)$', testPathIgnorePatterns: [ '/node_modules', @@ -14,9 +11,7 @@ module.exports = { '/mod', ], testEnvironment: 'node', - coverageDirectory: "/coverage", + coverageDirectory: '/coverage', collectCoverage: true, - collectCoverageFrom: [ - "src/**/*.ts", - ], + collectCoverageFrom: ['src/**/*.ts'], }; diff --git a/package.json b/package.json index b9901d6..4b0aa28 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "homepage": "https://github.com/Fitbit/image-codec-txi#readme", "scripts": { "build": "rm -rf lib mod && tsc -p tsconfig.build.json && tsc -p tsconfig.module.json", - "lint": "tslint -c tslint.json -p tsconfig.json", + "lint": "tslint -c tslint.json -p tsconfig.json --format code-frame", + "checkstyle": "prettier --list-different \"**/*.{js,jsx,ts,tsx,json}\"", "test": "npm run lint && jest", "test:coveralls": "npm run lint && jest --coverage --coverageReporters=text-lcov | coveralls", "prepublishOnly": "npm run test && npm run build", @@ -32,10 +33,12 @@ "jest": "^25.1.0", "microtime": "^3.0.0", "pngjs": "^3.4.0", + "prettier": "^2.1.2", "ts-jest": "^25.2.1", "ts-node": "^8.7.0", "tslint": "^6.1.0", "tslint-config-airbnb": "^5.11.2", + "tslint-config-prettier": "^1.18.0", "typescript": "^3.8.3" } } diff --git a/src/RunLengthEncoder.ts b/src/RunLengthEncoder.ts index 543e1a9..0e5b2e2 100644 --- a/src/RunLengthEncoder.ts +++ b/src/RunLengthEncoder.ts @@ -12,7 +12,6 @@ function comparePixel(a: Pixel, b: Pixel) { const MAX_SECTION_LENGTH = 127; export default class RunLengthEncoder { - private lastPixel: Pixel; private lastPixelValid = false; private pixelCount = 0; @@ -49,7 +48,7 @@ export default class RunLengthEncoder { private internalFlush() { if (this.pixelCount > 0) { - let headerByte = this.willCompress ? (MAX_SECTION_LENGTH + 1) : 0; + let headerByte = this.willCompress ? MAX_SECTION_LENGTH + 1 : 0; headerByte |= this.pixelCount & MAX_SECTION_LENGTH; this.destination.array[this.headerIndex] = headerByte; } diff --git a/src/encoder.test.ts b/src/encoder.test.ts index 4516274..44e224c 100644 --- a/src/encoder.test.ts +++ b/src/encoder.test.ts @@ -31,7 +31,9 @@ function checkImage( height: uncompressed.height, }; - const actualCompressed = Buffer.from(encode(inputImage, { rle, outputFormat })); + const actualCompressed = Buffer.from( + encode(inputImage, { rle, outputFormat }), + ); let expectedCompressedPath = `${filename}.txi.${outputFormat.toLowerCase()}`; if (rle !== false && expectRLEIsSmaller) expectedCompressedPath += '.rle'; @@ -43,45 +45,46 @@ function checkImage( for (const withRLE of [true, false]) { describe(`with RLE encoding ${withRLE ? 'enabled' : 'disabled'}`, () => { - it('converts an RGB PNG to RGBA8888', () => checkImage('rgb_image', withRLE)); + it('converts an RGB PNG to RGBA8888', () => + checkImage('rgb_image', withRLE)); it('converts an RGB PNG to RGB565', () => checkImage('rgb_image', withRLE, TXIOutputFormat.RGB565)); it('converts an RGB PNG to RGBA6666', () => checkImage('rgb_image', withRLE, TXIOutputFormat.RGBA6666)); it('converts a paletted PNG', () => checkImage('palette', withRLE)); - it('converts a 1-bit PNG', () => checkImage('1bit', withRLE, TXIOutputFormat.A8)); + it('converts a 1-bit PNG', () => + checkImage('1bit', withRLE, TXIOutputFormat.A8)); it('converts a very small image', () => checkImage('tiny', withRLE)); - it('converts an image that exceeds the worst case size when padded', - () => checkImage('rle_increases_size', withRLE)); - it('converts a PNG that leaves no RLE leftover bytes to flush', - () => checkImage('rle_no_leftovers', withRLE)); - it('converts an image that is larger when RLE encoded than unencoded', - () => checkImage('greyscale_bands', withRLE, TXIOutputFormat.A8)); + it('converts an image that exceeds the worst case size when padded', () => + checkImage('rle_increases_size', withRLE)); + it('converts a PNG that leaves no RLE leftover bytes to flush', () => + checkImage('rle_no_leftovers', withRLE)); + it('converts an image that is larger when RLE encoded than unencoded', () => + checkImage('greyscale_bands', withRLE, TXIOutputFormat.A8)); }); } -it('returns the smaller output if RLE mode is set to auto', - () => checkImage('greyscale_bands', 'auto', TXIOutputFormat.A8, false)); +it('returns the smaller output if RLE mode is set to auto', () => + checkImage('greyscale_bands', 'auto', TXIOutputFormat.A8, false)); const PNG_SUITE_DIR = 'PngSuite-2017jul19'; describe('readPNG', () => { - const pngSuite = fs.readdirSync(testResourcePath(PNG_SUITE_DIR)) - .filter(file => file.endsWith('.png')); - const validPNGs = pngSuite.filter(file => !file.startsWith('x')); - const corruptPNGs = pngSuite.filter(file => file.startsWith('x')); + const pngSuite = fs + .readdirSync(testResourcePath(PNG_SUITE_DIR)) + .filter((file) => file.endsWith('.png')); + const validPNGs = pngSuite.filter((file) => !file.startsWith('x')); + const corruptPNGs = pngSuite.filter((file) => file.startsWith('x')); describe('given a valid PNG file', () => { it.each(validPNGs)('reads %s', (vector) => { - expect(readPNG(loadTestResource(PNG_SUITE_DIR, vector))) - .toBeDefined(); + expect(readPNG(loadTestResource(PNG_SUITE_DIR, vector))).toBeDefined(); }); }); describe('given a corrupt PNG file', () => { it.each(corruptPNGs)('rejects %s', (vector) => { - expect(() => readPNG(loadTestResource(PNG_SUITE_DIR, vector))) - .toThrow(); + expect(() => readPNG(loadTestResource(PNG_SUITE_DIR, vector))).toThrow(); }); }); @@ -103,8 +106,6 @@ describe('readPNG', () => { describe('given an ArrayBuffer', () => { it('reads the file', () => - expect(readPNG(loadTestResource('tiny.png'))) - .toBeDefined(), - ); + expect(readPNG(loadTestResource('tiny.png'))).toBeDefined()); }); }); diff --git a/src/encoder.ts b/src/encoder.ts index 2eee62c..2e76726 100644 --- a/src/encoder.ts +++ b/src/encoder.ts @@ -7,14 +7,14 @@ enum TextureFormat { BGR565 = 0x01100565, BGRA8888 = 0x01208888, ABGR6666 = 0x02186666, - ABGR8888 = 0x02208888, + ABGR8888 = 0x02208888, } const enum TextureCompression { RLE = 0x10000000, } -const TXI_FILE_TYPE = 0x0A697874; +const TXI_FILE_TYPE = 0x0a697874; const TXI_FILE_VERSION = 0x20000028; const TXI_HEADER_LENGTH = 40; const INPUT_FORMAT_BPP = 4; @@ -55,8 +55,8 @@ const pixelEncoders: { [format: number]: PixelEncoder } = { const g6 = rescaleColor(data[offset + 1], 63); const b5 = rescaleColor(data[offset + 2], 31); - output[0] = 0xFF & ((g6 << 5) | b5); // gggbbbbb - output[1] = 0xFF & ((g6 >> 3) | (r5 << 3)); // rrrrrggg + output[0] = 0xff & ((g6 << 5) | b5); // gggbbbbb + output[1] = 0xff & ((g6 >> 3) | (r5 << 3)); // rrrrrggg }, [TextureFormat.ABGR6666]: (data, offset, output) => { if (data[offset + 3] === 0) { @@ -71,9 +71,9 @@ const pixelEncoders: { [format: number]: PixelEncoder } = { const b = rescaleColor(data[offset + 2], 63); const a = rescaleColor(data[offset + 3], 63); - output[0] = 0xFF & ((b << 6) | a); // bbaaaaaa - output[1] = 0xFF & ((g << 4) | (b >> 2)); // ggggbbbb - output[2] = 0xFF & ((r << 2) | (g >> 4)); // rrrrrrgg + output[0] = 0xff & ((b << 6) | a); // bbaaaaaa + output[1] = 0xff & ((g << 4) | (b >> 2)); // ggggbbbb + output[2] = 0xff & ((r << 2) | (g >> 4)); // rrrrrrgg }, }; @@ -82,12 +82,17 @@ function findTextureFormat(outputFormat: TXIOutputFormat, rle: boolean) { return rle ? TextureFormat.ABGR8888 : TextureFormat.BGRA8888; } switch (outputFormat) { - case TXIOutputFormat.A8: return TextureFormat.A8; - case TXIOutputFormat.RGB565: return TextureFormat.BGR565; - case TXIOutputFormat.RGBA6666: return TextureFormat.ABGR6666; + case TXIOutputFormat.A8: + return TextureFormat.A8; + case TXIOutputFormat.RGB565: + return TextureFormat.BGR565; + case TXIOutputFormat.RGBA6666: + return TextureFormat.ABGR6666; } - throw new Error(`No known texture format for TXI output format ${outputFormat}`); + throw new Error( + `No known texture format for TXI output format ${outputFormat}`, + ); } function maxOutputSize( @@ -107,14 +112,15 @@ function maxOutputSize( // - For each row, the final pixel is duplicated, so width + 1. // - Each row is padded up to a 32-bit boundary, so add a possible // 3 bytes for each row we write. - const maxBytesWithoutRLE = ((width + 1) * (height + 1) * bpp) + ((height + 1) * 3); + const maxBytesWithoutRLE = + (width + 1) * (height + 1) * bpp + (height + 1) * 3; return (withRLE ? maxBytesWithRLE : maxBytesWithoutRLE) + TXI_HEADER_LENGTH; } function encodeWithFixedRLE( image: ImageData, - options: { outputFormat: TXIOutputFormat, rle: boolean}, + options: { outputFormat: TXIOutputFormat; rle: boolean }, ) { const textureFormat = findTextureFormat(options.outputFormat, options.rle); const bpp = textureBPP[textureFormat]; @@ -123,7 +129,9 @@ function encodeWithFixedRLE( const imageData = new Uint8Array(image.data.buffer); const { width, height } = image; - const cursor = new BufferCursor(maxOutputSize(image, options.outputFormat, options.rle)); + const cursor = new BufferCursor( + maxOutputSize(image, options.outputFormat, options.rle), + ); cursor.seek(TXI_HEADER_LENGTH); const rle = options.rle ? new RunLengthEncoder(cursor, bpp) : undefined; @@ -177,7 +185,7 @@ function encodeWithFixedRLE( width, height, imageDataLen, - 0xDEADBEEF, + 0xdeadbeef, ].forEach((val, index) => dv.setUint32(index * 4, val, true)); } diff --git a/tsconfig.build.json b/tsconfig.build.json index 3da5762..da4af7c 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,7 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": [ - "**/*.test.ts", - "**/*.spec.ts" - ] + "exclude": ["**/*.test.ts", "**/*.spec.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 1ad479c..a52405c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,19 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "lib": ["es6"], - "strict": true, - "moduleResolution": "node", - "sourceMap": true, - "esModuleInterop": true, - "downlevelIteration": true, - "declaration": true, - "outDir": "lib", - "baseUrl": ".", - "paths": { - "*": [ - "src/types/*", - "node_modules/*" - ] - } - }, - "include": [ - "src/**/*" - ] + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": ["es6"], + "strict": true, + "moduleResolution": "node", + "sourceMap": true, + "esModuleInterop": true, + "downlevelIteration": true, + "declaration": true, + "outDir": "lib", + "baseUrl": ".", + "paths": { + "*": ["src/types/*", "node_modules/*"] + } + }, + "include": ["src/**/*"] } diff --git a/tsconfig.module.json b/tsconfig.module.json index 1080906..4cdf88c 100644 --- a/tsconfig.module.json +++ b/tsconfig.module.json @@ -1,4 +1,3 @@ - { "extends": "./tsconfig.build.json", "compilerOptions": { diff --git a/tslint.json b/tslint.json index 88a1c5b..52a391b 100644 --- a/tslint.json +++ b/tslint.json @@ -1,5 +1,5 @@ { - "extends": "tslint-config-airbnb", + "extends": ["tslint-config-airbnb", "tslint-config-prettier"], "rules": { "no-inferrable-types": true } diff --git a/yarn.lock b/yarn.lock index e91c866..6ce0ea4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2703,6 +2703,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== + pretty-format@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8" @@ -3337,6 +3342,11 @@ tslint-config-airbnb@^5.11.2: tslint-eslint-rules "^5.4.0" tslint-microsoft-contrib "~5.2.1" +tslint-config-prettier@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" + integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== + tslint-consistent-codestyle@^1.14.1: version "1.16.0" resolved "https://registry.yarnpkg.com/tslint-consistent-codestyle/-/tslint-consistent-codestyle-1.16.0.tgz#52348ea899a7e025b37cc6545751c6a566a19077"