diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..13ec1f2 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +other/ +dist/ +lib/ diff --git a/.github/workflows/manual_bump_version.yml b/.github/workflows/manual_bump_version.yml index 3e2f6aa..0a52c81 100644 --- a/.github/workflows/manual_bump_version.yml +++ b/.github/workflows/manual_bump_version.yml @@ -42,10 +42,10 @@ jobs: - name: Major version if: ${{ inputs.semver == 'major' }} - run: npm run bump-major + run: npm run bump:major - name: Minor version if: ${{ inputs.semver == 'minor' }} - run: npm run bump-minor + run: npm run bump:minor - name: Patch version if: ${{ inputs.semver == 'patch' }} - run: npm run bump-patch + run: npm run bump:patch diff --git a/.github/workflows/test_on_pull_request.yml b/.github/workflows/test_on_pull_request.yml index 6dab164..ede84dd 100644 --- a/.github/workflows/test_on_pull_request.yml +++ b/.github/workflows/test_on_pull_request.yml @@ -10,13 +10,13 @@ jobs: strategy: fail-fast: false matrix: - node-version: [12.x, 14.x, 16.x, 18.x] + node-version: [14.x, 16.x, 18.x] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - cache: 'npm' + # cache: 'npm' - run: npm install - run: npm run lint - run: npm run build diff --git a/.gitignore b/.gitignore index 758e21c..6fcea33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -.vscode -*.code-workspace +other/ +.vscode/ node_modules -dist -lib +dist/ +lib/ package-lock.json *.tgz diff --git a/.prettierignore b/.prettierignore index 48f3833..be73855 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,7 @@ -dist -lib -.vscode -*.code-workspace +other/ +dist/ +lib/ +.vscode/ node_modules package-lock.json *.tgz diff --git a/package.json b/package.json index 3345199..a540c5d 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ } ], "type": "module", - "main": "./dist/index.cjs.js", + "main": "./dist/index.js", "module": "./dist/index.esm.js", "types": "./dist/index.d.ts", "exports": { @@ -71,28 +71,44 @@ }, "scripts": { "lint": "eslint ./src/", - "lint:ci": "eslint ./src/ --fix", - "dev": "cross-env NODE_ENV=development tsup", - "build": "cross-env NODE_ENV=production tsup", + "lint:fix": "eslint ./src/ --fix", + "build:watch": "cross-env NODE_ENV=development rollup --config rollup.config.js --watch --sourcemap inline", + "build:dev": "cross-env NODE_ENV=development rollup --config rollup.config.js --sourcemap inline", + "build": "cross-env NODE_ENV=production rollup --config rollup.config.js", "test": "ava", - "bump-patch": "npm version patch -m \"Patch version %s\"", - "bump-minor": "npm version minor -m \"Minor version %s\"", - "bump-major": "npm version major -m \"Major version %s\"", + "server": "nodemon --watch ./src/serve.js --ext js ./src/serve.js", + "start": "concurrently --names \"ROLLUP,SERVER\" -c \"bgRed.bold,bgBlue.bold\" \"npm:build:watch\" \"npm:server\"", + "bump:patch": "npm version patch -m \"Patch version %s\"", + "bump:minor": "npm version minor -m \"Minor version %s\"", + "bump:major": "npm version major -m \"Major version %s\"", "preversion": "npm run lint && npm run build && npm run test", "version": "git add .", "postversion": "git push --follow-tags" }, "devDependencies": { + "@rollup/plugin-typescript": "^11.0.0", + "@types/connect-livereload": "^0.6.0", + "@types/express": "^4.17.15", + "@types/livereload": "^0.9.2", "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "ava": "^5.1.0", + "concurrently": "^7.6.0", + "connect-livereload": "^0.6.1", "cross-env": "^7.0.3", + "esbuild": "^0.16.16", "eslint": "^8.31.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-prettier": "^4.2.1", + "express": "^4.18.2", + "livereload": "^0.9.3", + "nodemon": "^2.0.20", "prettier": "^2.8.2", - "tsup": "^6.5.0", + "rollup": "^3.9.1", + "rollup-plugin-dts": "^5.1.1", + "rollup-plugin-esbuild": "^5.0.0", + "tslib": "^2.4.1", "typescript": "^4.9.4" }, "publishConfig": { diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..f24959a --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,92 @@ +// Install: +// npm install -D @rollup/plugin-terser @rollup/plugin-commonjs +// import typescript from '@rollup/plugin-typescript' +// import terser from '@rollup/plugin-terser' +// import commonjs from '@rollup/plugin-commonjs' +import esbuild from 'rollup-plugin-esbuild' +import dts from 'rollup-plugin-dts' +// import {RollupOptions} from 'rollup' + +const isProduction = process.env.NODE_ENV === 'production' + +// See: https://rollupjs.org/guide/en/#outputformat + +export default [ + { + input: 'src/index.ts', + // plugins: [typescript(), isProduction && terser()], + plugins: [ + esbuild({ + sourceMap: !isProduction, + minify: isProduction, + }), + ], + output: [ + { + file: `${isProduction ? 'dist' : 'lib'}/index.esm.js`, + format: 'esm', + // sourcemap: isProduction, + }, + { + name: 'isApng', + file: `${isProduction ? 'dist' : 'lib'}/index.cjs.js`, + format: 'cjs', + // interop: 'auto', + // sourcemap: isProduction, + }, + { + name: 'isApng', + file: `${isProduction ? 'dist' : 'lib'}/index.js`, + format: 'umd', + // sourcemap: isProduction, + }, + ], + }, + // { + // input: 'src/index.ts', + // plugins: [ + // esbuild({ + // // All options are optional + // include: /\.[jt]sx?$/, // default, inferred from `loaders` option + // exclude: /node_modules/, // default + // sourceMap: true, // default + // // minify: true, + // target: 'es2017', // default, or 'es20XX', 'esnext' + // // jsx: 'transform', // default, or 'preserve' + // // jsxFactory: 'React.createElement', + // // jsxFragment: 'React.Fragment', + // // Like @rollup/plugin-replace + // // define: { + // // __VERSION__: '"x.y.z"', + // // }, + // // tsconfig: 'tsconfig.json', // default + // // Add extra loaders + // // loaders: { + // // // Add .json files support + // // // require @rollup/plugin-commonjs + // // '.json': 'json', + // // // Enable JSX in .js files too + // // '.js': 'jsx', + // // }, + // }), + // // typescript(), + // // commonjs(), + // // isProduction && terser() + // ], + // output: { + // name: 'isApng', + // file: `${isProduction ? 'dist' : 'lib'}/index.cjs.js`, + // format: 'cjs', + // // interop: 'default', + // interop: 'auto', + // }, + // }, + { + input: `src/index.ts`, + plugins: [dts()], + output: { + file: `${isProduction ? 'dist' : 'lib'}/index.d.ts`, + format: 'es', + }, + }, +] diff --git a/src/images/static.png b/src/images/static.png index bd1479d..8597e68 100644 Binary files a/src/images/static.png and b/src/images/static.png differ diff --git a/src/index.ts b/src/index.ts index 8a11b07..5c00e55 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,112 +1,12 @@ -import { Buffer } from 'node:buffer' - -/** - * Returns the index of the first occurrence of a sequence in an typed array, or -1 if it is not present. - * - * Works similar to `Array.prototype.indexOf()`, but it searches for a sequence of array values (bytes). - * The bytes in the `haystack` array are decoded (UTF-8) and then used to search for `needle`. - * - * @param haystack `Uint8Array` - * Array to search in. - * - * @param needle `string | RegExp` - * The value to locate in the array. - * - * @param fromIndex `number` - * The array index at which to begin the search. - * - * @param upToIndex `number` - * The array index up to which to search. - * If omitted, search until the end. - * - * @param chunksize `number` - * Size of the chunks used when searching (default 1024). - * - * @returns boolean - * Whether the array holds Animated PNG data. - */ -function indexOfSubstring( - haystack: Uint8Array, - needle: string | RegExp, - fromIndex: number, - upToIndex?: number, - chunksize = 1024 /* Bytes */, -) { - /** - * Adopted from: https://stackoverflow.com/a/67771214/2142071 - */ - - if (!needle) { - return -1 - } - needle = new RegExp(needle, 'g') - - // The needle could get split over two chunks. - // So, at every chunk we prepend the last few characters - // of the last chunk. - const needle_length = needle.source.length - const decoder = new TextDecoder() - - // Handle search offset in line with - // `Array.prototype.indexOf()` and `TypedArray.prototype.subarray()`. - const full_haystack_length = haystack.length - if (typeof upToIndex === 'undefined') { - upToIndex = full_haystack_length - } - if ( - fromIndex >= full_haystack_length || - upToIndex <= 0 || - fromIndex >= upToIndex - ) { - return -1 - } - haystack = haystack.subarray(fromIndex, upToIndex) - - let position = -1 - let current_index = 0 - let full_length = 0 - let needle_buffer = '' - - outer: while (current_index < haystack.length) { - const next_index = current_index + chunksize - // subarray doesn't copy - const chunk = haystack.subarray(current_index, next_index) - const decoded = decoder.decode(chunk, { stream: true }) - - const text = needle_buffer + decoded - - let match: RegExpExecArray | null - let last_index = -1 - while ((match = needle.exec(text)) !== null) { - last_index = match.index - needle_buffer.length - position = full_length + last_index - break outer - } - - current_index = next_index - full_length += decoded.length - - // Check that the buffer doesn't itself include the needle - // this would cause duplicate finds (we could also use a Set to avoid that). - const needle_index = - last_index > -1 - ? last_index + needle_length - : decoded.length - needle_length - needle_buffer = decoded.slice(needle_index) - } - - // Correct for search offset. - if (position >= 0) { - position += fromIndex >= 0 ? fromIndex : full_haystack_length + fromIndex - } - - return position -} +// import { Buffer } from 'node:buffer' -export default function isApng(buffer: Buffer | Uint8Array) { +export default function isApng(buffer: Buffer | Uint8Array): boolean { if ( !buffer || - !(Buffer.isBuffer(buffer) || buffer instanceof Uint8Array) || + !( + (typeof Buffer !== 'undefined' && Buffer.isBuffer(buffer)) || + buffer instanceof Uint8Array + ) || buffer.length < 16 ) { return false @@ -126,6 +26,109 @@ export default function isApng(buffer: Buffer | Uint8Array) { return false } + /** + * Returns the index of the first occurrence of a sequence in an typed array, or -1 if it is not present. + * + * Works similar to `Array.prototype.indexOf()`, but it searches for a sequence of array values (bytes). + * The bytes in the `haystack` array are decoded (UTF-8) and then used to search for `needle`. + * + * @param haystack `Uint8Array` + * Array to search in. + * + * @param needle `string | RegExp` + * The value to locate in the array. + * + * @param fromIndex `number` + * The array index at which to begin the search. + * + * @param upToIndex `number` + * The array index up to which to search. + * If omitted, search until the end. + * + * @param chunksize `number` + * Size of the chunks used when searching (default 1024). + * + * @returns boolean + * Whether the array holds Animated PNG data. + */ + function indexOfSubstring( + haystack: Uint8Array, + needle: string | RegExp, + fromIndex: number, + upToIndex?: number, + chunksize = 1024 /* Bytes */, + ) { + /** + * Adopted from: https://stackoverflow.com/a/67771214/2142071 + */ + + if (!needle) { + return -1 + } + needle = new RegExp(needle, 'g') + + // The needle could get split over two chunks. + // So, at every chunk we prepend the last few characters + // of the last chunk. + const needle_length = needle.source.length + const decoder = new TextDecoder() + + // Handle search offset in line with + // `Array.prototype.indexOf()` and `TypedArray.prototype.subarray()`. + const full_haystack_length = haystack.length + if (typeof upToIndex === 'undefined') { + upToIndex = full_haystack_length + } + if ( + fromIndex >= full_haystack_length || + upToIndex <= 0 || + fromIndex >= upToIndex + ) { + return -1 + } + haystack = haystack.subarray(fromIndex, upToIndex) + + let position = -1 + let current_index = 0 + let full_length = 0 + let needle_buffer = '' + + outer: while (current_index < haystack.length) { + const next_index = current_index + chunksize + // subarray doesn't copy + const chunk = haystack.subarray(current_index, next_index) + const decoded = decoder.decode(chunk, { stream: true }) + + const text = needle_buffer + decoded + + let match: RegExpExecArray | null + let last_index = -1 + while ((match = needle.exec(text)) !== null) { + last_index = match.index - needle_buffer.length + position = full_length + last_index + break outer + } + + current_index = next_index + full_length += decoded.length + + // Check that the buffer doesn't itself include the needle + // this would cause duplicate finds (we could also use a Set to avoid that). + const needle_index = + last_index > -1 + ? last_index + needle_length + : decoded.length - needle_length + needle_buffer = decoded.slice(needle_index) + } + + // Correct for search offset. + if (position >= 0) { + position += fromIndex >= 0 ? fromIndex : full_haystack_length + fromIndex + } + + return position + } + // APNGs have an animation control chunk ('acTL') preceding the IDATs. // See: https://en.wikipedia.org/wiki/APNG#File_format const arr = new Uint8Array(buffer) @@ -138,6 +141,8 @@ export default function isApng(buffer: Buffer | Uint8Array) { return false } +// globalThis.isApng = isApng + // (new TextEncoder()).encode('IDAT') // Decimal: [73, 68, 65, 84] // Hex: [0x49, 0x44, 0x41, 0x54] diff --git a/src/serve.js b/src/serve.js new file mode 100644 index 0000000..4a50654 --- /dev/null +++ b/src/serve.js @@ -0,0 +1,30 @@ +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import express from 'express' +import livereload from 'livereload' +import connectLiveReload from 'connect-livereload' + +const dir = + typeof __dirname !== 'undefined' + ? __dirname + : dirname(fileURLToPath(import.meta.url)) + +const liveReloadServer = livereload.createServer() +liveReloadServer.server.once('connection', () => { + setTimeout(() => { + liveReloadServer.refresh('/') + }, 100) +}) + +liveReloadServer.watch(join(dir, 'test-browser')) + +const app = express() +const port = 3000 + +app.use(connectLiveReload()) +app.use(express.static(resolve(dir, './test-browser/'))) +app.use('/js', express.static(resolve(dir, '../lib/'))) + +app.listen(port, () => { + console.log(`Example app listening on http://localhost:${port}/`) +}) diff --git a/src/test-browser/img/animated.gif b/src/test-browser/img/animated.gif new file mode 100644 index 0000000..e85642f Binary files /dev/null and b/src/test-browser/img/animated.gif differ diff --git a/src/test-browser/img/animated.png b/src/test-browser/img/animated.png new file mode 100644 index 0000000..c2f45d9 Binary files /dev/null and b/src/test-browser/img/animated.png differ diff --git a/src/test-browser/img/animated.webp b/src/test-browser/img/animated.webp new file mode 100644 index 0000000..5b44046 Binary files /dev/null and b/src/test-browser/img/animated.webp differ diff --git a/src/test-browser/img/broken.png b/src/test-browser/img/broken.png new file mode 100644 index 0000000..9cc0bb1 Binary files /dev/null and b/src/test-browser/img/broken.png differ diff --git a/src/test-browser/img/static.jpg b/src/test-browser/img/static.jpg new file mode 100644 index 0000000..9625248 Binary files /dev/null and b/src/test-browser/img/static.jpg differ diff --git a/src/test-browser/img/static.png b/src/test-browser/img/static.png new file mode 100644 index 0000000..8597e68 Binary files /dev/null and b/src/test-browser/img/static.png differ diff --git a/src/test-browser/index.html b/src/test-browser/index.html new file mode 100644 index 0000000..af39bb6 --- /dev/null +++ b/src/test-browser/index.html @@ -0,0 +1,476 @@ + + +
+ + + +