imgcap 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.commitlintrc +5 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/cd.yml +85 -0
- package/.github/workflows/ci.yml +65 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.prettierrc +6 -0
- package/.releaserc +13 -0
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +54 -0
- package/__tests__/.prettierrc +6 -0
- package/__tests__/eslint.config.ts +18 -0
- package/__tests__/package.json +33 -0
- package/__tests__/tsconfig.json +14 -0
- package/__tests__/unit/imgcap.test.ts +195 -0
- package/__tests__/utils/index.ts +18 -0
- package/__tests__/vitest.config.ts +11 -0
- package/core/.prettierrc +6 -0
- package/core/dist/index.d.ts +35 -0
- package/core/dist/index.js +2 -0
- package/core/dist/index.js.map +1 -0
- package/core/eslint.config.ts +18 -0
- package/core/package.json +29 -0
- package/core/src/index.ts +104 -0
- package/core/tsconfig.json +24 -0
- package/package.json +54 -0
- package/pnpm-workspace.yaml +3 -0
package/.commitlintrc
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# https://github.com/dependabot/dependabot-core/issues/1736
|
|
2
|
+
|
|
3
|
+
version: 2
|
|
4
|
+
updates:
|
|
5
|
+
# Enable version updates for npm
|
|
6
|
+
- package-ecosystem: 'npm'
|
|
7
|
+
# Look for `package.json` and `lock` files in the `root` directory
|
|
8
|
+
directory: '/'
|
|
9
|
+
# Check the npm registry for updates every day (weekdays)
|
|
10
|
+
schedule:
|
|
11
|
+
interval: 'weekly'
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: CD
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
jobs:
|
|
9
|
+
setup:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
outputs:
|
|
12
|
+
cache-hit: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: lts/*
|
|
18
|
+
- uses: pnpm/action-setup@v4
|
|
19
|
+
with:
|
|
20
|
+
version: latest
|
|
21
|
+
- name: Cache pnpm store
|
|
22
|
+
uses: actions/cache@v3
|
|
23
|
+
id: pnpm-cache
|
|
24
|
+
with:
|
|
25
|
+
path: |
|
|
26
|
+
~/.pnpm-store
|
|
27
|
+
node_modules
|
|
28
|
+
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
29
|
+
restore-keys: |
|
|
30
|
+
${{ runner.os }}-pnpm-
|
|
31
|
+
linter:
|
|
32
|
+
needs: [setup]
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: actions/setup-node@v4
|
|
37
|
+
with:
|
|
38
|
+
node-version: lts/*
|
|
39
|
+
- uses: pnpm/action-setup@v4
|
|
40
|
+
with:
|
|
41
|
+
version: latest
|
|
42
|
+
- name: Restore dependencies
|
|
43
|
+
if: needs.setup.outputs.cache-hit != 'true'
|
|
44
|
+
run: pnpm install --frozen-lockfile
|
|
45
|
+
- run: pnpm run build
|
|
46
|
+
- run: pnpm run lint
|
|
47
|
+
- run: pnpm run check
|
|
48
|
+
|
|
49
|
+
tests:
|
|
50
|
+
needs: [setup]
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
- uses: actions/setup-node@v4
|
|
55
|
+
with:
|
|
56
|
+
node-version: lts/*
|
|
57
|
+
- uses: pnpm/action-setup@v4
|
|
58
|
+
with:
|
|
59
|
+
version: latest
|
|
60
|
+
- name: Restore dependencies
|
|
61
|
+
if: needs.setup.outputs.cache-hit != 'true'
|
|
62
|
+
run: pnpm install --frozen-lockfile
|
|
63
|
+
- run: pnpm run build
|
|
64
|
+
- run: pnpm run test
|
|
65
|
+
release:
|
|
66
|
+
needs: [setup, linter, tests]
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
with:
|
|
71
|
+
persist-credentials: false
|
|
72
|
+
- uses: actions/setup-node@v4
|
|
73
|
+
with:
|
|
74
|
+
node-version: lts/*
|
|
75
|
+
- uses: pnpm/action-setup@v4
|
|
76
|
+
with:
|
|
77
|
+
version: latest
|
|
78
|
+
- name: Restore dependencies
|
|
79
|
+
if: needs.setup.outputs.cache-hit != 'true'
|
|
80
|
+
run: pnpm install --frozen-lockfile --ignore-scripts
|
|
81
|
+
- run: pnpm run build
|
|
82
|
+
- run: pnpm semantic-release
|
|
83
|
+
env:
|
|
84
|
+
GH_TOKEN: ${{ secrets.IMGCAP_GITHUB_TOKEN }}
|
|
85
|
+
NPM_TOKEN: ${{ secrets.IMGCAP_NPM_TOKEN }}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [develop]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
setup:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
outputs:
|
|
13
|
+
cache-hit: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: lts/*
|
|
19
|
+
- uses: pnpm/action-setup@v4
|
|
20
|
+
with:
|
|
21
|
+
version: latest
|
|
22
|
+
- name: Cache pnpm store
|
|
23
|
+
uses: actions/cache@v3
|
|
24
|
+
id: pnpm-cache
|
|
25
|
+
with:
|
|
26
|
+
path: |
|
|
27
|
+
~/.pnpm-store
|
|
28
|
+
node_modules
|
|
29
|
+
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
30
|
+
restore-keys: |
|
|
31
|
+
${{ runner.os }}-pnpm-
|
|
32
|
+
linter:
|
|
33
|
+
needs: [setup]
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: actions/setup-node@v4
|
|
38
|
+
with:
|
|
39
|
+
node-version: lts/*
|
|
40
|
+
- uses: pnpm/action-setup@v4
|
|
41
|
+
with:
|
|
42
|
+
version: latest
|
|
43
|
+
- name: Restore dependencies
|
|
44
|
+
if: needs.setup.outputs.cache-hit != 'true'
|
|
45
|
+
run: pnpm install --frozen-lockfile
|
|
46
|
+
- run: pnpm run build
|
|
47
|
+
- run: pnpm run lint
|
|
48
|
+
- run: pnpm run check
|
|
49
|
+
|
|
50
|
+
tests:
|
|
51
|
+
needs: [setup]
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
- uses: actions/setup-node@v4
|
|
56
|
+
with:
|
|
57
|
+
node-version: lts/*
|
|
58
|
+
- uses: pnpm/action-setup@v4
|
|
59
|
+
with:
|
|
60
|
+
version: latest
|
|
61
|
+
- name: Restore dependencies
|
|
62
|
+
if: needs.setup.outputs.cache-hit != 'true'
|
|
63
|
+
run: pnpm install --frozen-lockfile
|
|
64
|
+
- run: pnpm run build
|
|
65
|
+
- run: pnpm run test
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pnpm commitlint --edit "$1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pnpm lint && pnpm check
|
package/.prettierrc
ADDED
package/.releaserc
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": [
|
|
3
|
+
"master"
|
|
4
|
+
],
|
|
5
|
+
"plugins": [
|
|
6
|
+
"@semantic-release/commit-analyzer",
|
|
7
|
+
"@semantic-release/release-notes-generator",
|
|
8
|
+
"@semantic-release/changelog",
|
|
9
|
+
"@semantic-release/github",
|
|
10
|
+
"@semantic-release/npm",
|
|
11
|
+
"@semantic-release/git"
|
|
12
|
+
]
|
|
13
|
+
}
|
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 John Wu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# ImgCap
|
|
2
|
+
|
|
3
|
+
> Automatically compress images to exact file size using binary search algorithm. No more "file too large" errors.
|
|
4
|
+
|
|
5
|
+
## Why ImgCap?
|
|
6
|
+
|
|
7
|
+
Users often encounter "File too large" errors when uploading images, forcing them to manually compress files using external tools. This creates friction and leads to user dropout. imgcap solves this by automatically compressing images to exact size requirements - no user intervention needed.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// Before: User sees error, leaves frustrated
|
|
11
|
+
❌ "File too large: Image upload size cannot exceed 2MB"
|
|
12
|
+
|
|
13
|
+
// After: Seamless auto-compression
|
|
14
|
+
✅ await imgcap(userPhoto, { targetSize: 2 * 1024 * 1024 })
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm install imgcap
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import imgcap from 'imgcap'
|
|
27
|
+
|
|
28
|
+
// Social media avatar (500KB limit)
|
|
29
|
+
const avatar = await imgcap(file, { targetSize: 500 * 1024 })
|
|
30
|
+
|
|
31
|
+
// With format conversion
|
|
32
|
+
const webp = await imgcap(imageFile, {
|
|
33
|
+
targetSize: 300 * 1024,
|
|
34
|
+
outputType: 'image/webp'
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
interface Options {
|
|
42
|
+
targetSize: number // Target file size in bytes
|
|
43
|
+
toleranceSize?: number // Size tolerance (default: -1024)
|
|
44
|
+
outputType?: ImageType // Output format (default: same as input)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ImageType = 'image/jpeg' | 'image/png' | 'image/webp' | 'image/avif'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Browser only** - Requires OffscreenCanvas support (modern browsers).
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT © [molvqingtai](https://github.com/molvqingtai)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import globals from 'globals'
|
|
2
|
+
import pluginJs from '@eslint/js'
|
|
3
|
+
import tseslint from 'typescript-eslint'
|
|
4
|
+
import prettierPlugin from 'eslint-plugin-prettier/recommended'
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
{ files: ['**/*.{js,mjs,cjs,ts}'] },
|
|
8
|
+
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
|
|
9
|
+
pluginJs.configs.recommended,
|
|
10
|
+
...tseslint.configs.recommended,
|
|
11
|
+
prettierPlugin,
|
|
12
|
+
{
|
|
13
|
+
ignores: ['**/dist/*']
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
rules: {}
|
|
17
|
+
}
|
|
18
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__tests__",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest --browser.headless",
|
|
8
|
+
"lint": "eslint --fix --cache",
|
|
9
|
+
"check": "tsc --noEmit",
|
|
10
|
+
"postinstall": "playwright install --with-deps chromium"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "molvqingtai",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"imgcap": "workspace:*"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"vitest": "^3.2.4",
|
|
20
|
+
"@vitest/browser": "^3.2.4",
|
|
21
|
+
"playwright": "^1.49.1",
|
|
22
|
+
"@eslint/js": "^9.31.0",
|
|
23
|
+
"eslint": "^9.31.0",
|
|
24
|
+
"eslint-config-prettier": "^10.1.5",
|
|
25
|
+
"eslint-plugin-prettier": "^5.5.1",
|
|
26
|
+
"globals": "^16.3.0",
|
|
27
|
+
"typescript": "^5.8.3",
|
|
28
|
+
"typescript-eslint": "^8.36.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
/* Basic Options */
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"rootDir": ".",
|
|
6
|
+
/* Strict Type-Checking Options */
|
|
7
|
+
"strict": true /* Enable all strict type-checking options. */,
|
|
8
|
+
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
|
9
|
+
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
|
10
|
+
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
|
11
|
+
"moduleResolution": "Node",
|
|
12
|
+
"isolatedModules": true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
2
|
+
import { createTestImageBlob } from '../utils'
|
|
3
|
+
import { imgcap } from 'imgcap'
|
|
4
|
+
|
|
5
|
+
describe('imgcap', () => {
|
|
6
|
+
let testImageBlob: Blob
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
testImageBlob = await createTestImageBlob(200, 200, 'image/png')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('Input Validation', () => {
|
|
13
|
+
it('should throw error for unsupported image type', async () => {
|
|
14
|
+
const invalidBlob = new Blob(['test'], { type: 'text/plain' })
|
|
15
|
+
|
|
16
|
+
await expect(imgcap(invalidBlob, { targetSize: 10000 })).rejects.toThrow(
|
|
17
|
+
'Only PNG, JPEG, WebP and AVIF images are supported.'
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should throw error for tolerance size less than 1024', async () => {
|
|
22
|
+
await expect(imgcap(testImageBlob, { targetSize: 10000, toleranceSize: 512 })).rejects.toThrow(
|
|
23
|
+
'Tolerance size must be at least ±1024 bytes.'
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should throw error for negative tolerance size less than -1024', async () => {
|
|
28
|
+
await expect(imgcap(testImageBlob, { targetSize: 10000, toleranceSize: -512 })).rejects.toThrow(
|
|
29
|
+
'Tolerance size must be at least ±1024 bytes.'
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should accept tolerance size of exactly ±1024', async () => {
|
|
34
|
+
await expect(imgcap(testImageBlob, { targetSize: 10000, toleranceSize: 1024 })).resolves.toBeDefined()
|
|
35
|
+
|
|
36
|
+
await expect(imgcap(testImageBlob, { targetSize: 10000, toleranceSize: -1024 })).resolves.toBeDefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should accept tolerance size greater than ±1024', async () => {
|
|
40
|
+
await expect(imgcap(testImageBlob, { targetSize: 10000, toleranceSize: 2048 })).resolves.toBeDefined()
|
|
41
|
+
|
|
42
|
+
await expect(imgcap(testImageBlob, { targetSize: 10000, toleranceSize: -2048 })).resolves.toBeDefined()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('Basic Functionality', () => {
|
|
47
|
+
it('should return original blob if already smaller than target size', async () => {
|
|
48
|
+
const smallBlob = await createTestImageBlob(10, 10, 'image/png')
|
|
49
|
+
const result = await imgcap(smallBlob, { targetSize: 100000 })
|
|
50
|
+
|
|
51
|
+
expect(result).toBe(smallBlob)
|
|
52
|
+
expect(result.size).toBe(smallBlob.size)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should compress image to approximate target size', async () => {
|
|
56
|
+
const targetSize = 5000
|
|
57
|
+
const result = await imgcap(testImageBlob, {
|
|
58
|
+
targetSize,
|
|
59
|
+
toleranceSize: -1024
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(result).toBeInstanceOf(Blob)
|
|
63
|
+
expect(result.size).toBeLessThanOrEqual(targetSize)
|
|
64
|
+
// 允许更大的容差范围,因为压缩算法可能产生更小的文件
|
|
65
|
+
expect(result.size).toBeGreaterThan(500)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should respect tolerance range', async () => {
|
|
69
|
+
const targetSize = 3000
|
|
70
|
+
const toleranceSize = -1024
|
|
71
|
+
const result = await imgcap(testImageBlob, {
|
|
72
|
+
targetSize,
|
|
73
|
+
toleranceSize
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const lowerBound = targetSize + Math.min(0, toleranceSize)
|
|
77
|
+
const upperBound = targetSize + Math.max(0, toleranceSize)
|
|
78
|
+
|
|
79
|
+
expect(result.size).toBeLessThanOrEqual(upperBound)
|
|
80
|
+
// 对于负容差,只检查上界,因为压缩可能产生比期望更小的文件
|
|
81
|
+
if (toleranceSize < 0) {
|
|
82
|
+
expect(result.size).toBeGreaterThan(500) // 确保不会过小
|
|
83
|
+
} else {
|
|
84
|
+
expect(result.size).toBeGreaterThanOrEqual(lowerBound)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('Output Format Control', () => {
|
|
90
|
+
it('should maintain original format when no outputType specified', async () => {
|
|
91
|
+
const jpegBlob = await createTestImageBlob(100, 100, 'image/jpeg')
|
|
92
|
+
const result = await imgcap(jpegBlob, { targetSize: 10000 })
|
|
93
|
+
|
|
94
|
+
expect(result.type).toBe('image/jpeg')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should convert to specified output format', async () => {
|
|
98
|
+
const result = await imgcap(testImageBlob, {
|
|
99
|
+
targetSize: 10000,
|
|
100
|
+
outputType: 'image/jpeg'
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(result.type).toBe('image/jpeg')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should handle PNG to WebP conversion', async () => {
|
|
107
|
+
const result = await imgcap(testImageBlob, {
|
|
108
|
+
targetSize: 10000,
|
|
109
|
+
outputType: 'image/webp'
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
expect(result.type).toBe('image/webp')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('Compression Quality', () => {
|
|
117
|
+
it('should compress large images significantly', async () => {
|
|
118
|
+
const largeBlob = await createTestImageBlob(500, 500, 'image/png')
|
|
119
|
+
const targetSize = 5000
|
|
120
|
+
const result = await imgcap(largeBlob, { targetSize })
|
|
121
|
+
|
|
122
|
+
expect(result.size).toBeLessThan(largeBlob.size)
|
|
123
|
+
expect(result.size).toBeLessThanOrEqual(targetSize)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should handle very small target sizes', async () => {
|
|
127
|
+
const targetSize = 2000
|
|
128
|
+
const result = await imgcap(testImageBlob, {
|
|
129
|
+
targetSize,
|
|
130
|
+
toleranceSize: -1024
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
expect(result.size).toBeLessThanOrEqual(targetSize)
|
|
134
|
+
expect(result.size).toBeGreaterThan(500) // Should not be too small
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should handle edge case with very large tolerance', async () => {
|
|
138
|
+
const targetSize = 5000
|
|
139
|
+
const toleranceSize = 10000
|
|
140
|
+
const result = await imgcap(testImageBlob, {
|
|
141
|
+
targetSize,
|
|
142
|
+
toleranceSize
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(result.size).toBeLessThanOrEqual(targetSize + toleranceSize)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('Error Handling', () => {
|
|
150
|
+
it('should handle invalid image data gracefully', async () => {
|
|
151
|
+
const invalidImageBlob = new Blob(['not an image'], { type: 'text/plain' })
|
|
152
|
+
|
|
153
|
+
await expect(imgcap(invalidImageBlob, { targetSize: 10000 })).rejects.toThrow()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should handle zero target size', async () => {
|
|
157
|
+
await expect(imgcap(testImageBlob, { targetSize: 0 })).rejects.toThrow()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should handle negative target size', async () => {
|
|
161
|
+
await expect(imgcap(testImageBlob, { targetSize: -1000 })).rejects.toThrow()
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('Format Support', () => {
|
|
166
|
+
it('should handle JPEG input', async () => {
|
|
167
|
+
const jpegBlob = await createTestImageBlob(150, 150, 'image/jpeg')
|
|
168
|
+
const result = await imgcap(jpegBlob, { targetSize: 8000 })
|
|
169
|
+
|
|
170
|
+
expect(result).toBeInstanceOf(Blob)
|
|
171
|
+
expect(result.type).toBe('image/jpeg')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should handle WebP format', async () => {
|
|
175
|
+
const webpBlob = await createTestImageBlob(150, 150, 'image/webp')
|
|
176
|
+
const result = await imgcap(webpBlob, { targetSize: 8000 })
|
|
177
|
+
|
|
178
|
+
expect(result).toBeInstanceOf(Blob)
|
|
179
|
+
expect(result.type).toBe('image/webp')
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('Performance', () => {
|
|
184
|
+
it('should handle concurrent compressions', async () => {
|
|
185
|
+
const promises = Array.from({ length: 3 }, (_, i) => imgcap(testImageBlob, { targetSize: 4000 + i * 1000 }))
|
|
186
|
+
|
|
187
|
+
const results = await Promise.all(promises)
|
|
188
|
+
|
|
189
|
+
results.forEach((result, i) => {
|
|
190
|
+
expect(result).toBeInstanceOf(Blob)
|
|
191
|
+
expect(result.size).toBeLessThanOrEqual(4000 + i * 1000)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ImageType } from 'imgcap'
|
|
2
|
+
|
|
3
|
+
// Helper function to create a test image blob
|
|
4
|
+
export const createTestImageBlob = async (width: number = 100, height: number = 100, type: ImageType = 'image/png') => {
|
|
5
|
+
const canvas = new OffscreenCanvas(width, height)
|
|
6
|
+
const ctx = canvas.getContext('2d')!
|
|
7
|
+
// Draw a simple pattern
|
|
8
|
+
ctx.fillStyle = '#ff0000'
|
|
9
|
+
ctx.fillRect(0, 0, width / 2, height / 2)
|
|
10
|
+
ctx.fillStyle = '#00ff00'
|
|
11
|
+
ctx.fillRect(width / 2, 0, width / 2, height / 2)
|
|
12
|
+
ctx.fillStyle = '#0000ff'
|
|
13
|
+
ctx.fillRect(0, height / 2, width / 2, height / 2)
|
|
14
|
+
ctx.fillStyle = '#ffff00'
|
|
15
|
+
ctx.fillRect(width / 2, height / 2, width / 2, height / 2)
|
|
16
|
+
|
|
17
|
+
return await canvas.convertToBlob({ type })
|
|
18
|
+
}
|
package/core/.prettierrc
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
/** Supported image formats */
|
|
3
|
+
type ImageType = 'image/jpeg' | 'image/png' | 'image/webp' | 'image/avif';
|
|
4
|
+
/** Compression options */
|
|
5
|
+
interface Options {
|
|
6
|
+
/** Target file size in bytes */
|
|
7
|
+
targetSize: number;
|
|
8
|
+
/** Size tolerance in bytes (default: -1024) */
|
|
9
|
+
toleranceSize?: number;
|
|
10
|
+
/** Output image format (default: same as input) */
|
|
11
|
+
outputType?: ImageType;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compress an image to exact target file size using binary search algorithm.
|
|
15
|
+
*
|
|
16
|
+
* @param input - Image blob/file to compress
|
|
17
|
+
* @param options - Compression options
|
|
18
|
+
* @returns Promise that resolves to compressed image blob
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* // Basic usage - compress to 500KB
|
|
23
|
+
* const compressed = await imgcap(imageFile, { targetSize: 500 * 1024 })
|
|
24
|
+
*
|
|
25
|
+
* // With format conversion
|
|
26
|
+
* const webp = await imgcap(imageFile, {
|
|
27
|
+
* targetSize: 300 * 1024,
|
|
28
|
+
* outputType: 'image/webp'
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare const imgcap: (input: Blob, options: Options) => Promise<Blob>;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { ImageType, Options, imgcap as default, imgcap };
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=async(t,n,r,i,a,o)=>{let s=(r+i)/2,c=Math.round(t.width*s),l=Math.round(t.height*s),u=new OffscreenCanvas(c,l),d=u.getContext(`2d`);d.drawImage(t,0,0,t.width,t.height,0,0,c,l);let f=await u.convertToBlob({type:o,quality:s}),p=f.size,m=n+Math.min(0,a),h=n+Math.max(0,a);return p>=m&&p<=h||(i-r)/i<.001?f:p>n?await e(t,n,r,s,a,o):await e(t,n,s,i,a,o)},t=async(t,n)=>{let{targetSize:r,toleranceSize:i=-1024}=n;if(![`image/jpeg`,`image/png`,`image/webp`,`image/avif`].includes(t.type))throw Error(`Only PNG, JPEG, WebP and AVIF images are supported.`);if(Math.abs(i)<1024)throw Error(`Tolerance size must be at least ±1024 bytes.`);let a=n.outputType||t.type;if(t.size<=r&&t.type===a)return t;let o=await createImageBitmap(t),s=0,c=1;return await e(o,r,s,c,i,a)};var n=t;export{n as default,t as imgcap};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["imageBitmap: ImageBitmap","targetSize: number","low: number","high: number","toleranceSize: number","outputType: ImageType","input: Blob","options: Options"],"sources":["../src/index.ts"],"sourcesContent":["/** Supported image formats */\nexport type ImageType = 'image/jpeg' | 'image/png' | 'image/webp' | 'image/avif'\n\n/** Compression options */\nexport interface Options {\n /** Target file size in bytes */\n targetSize: number\n /** Size tolerance in bytes (default: -1024) */\n toleranceSize?: number\n /** Output image format (default: same as input) */\n outputType?: ImageType\n}\n\nconst compress = async (\n imageBitmap: ImageBitmap,\n targetSize: number,\n low: number,\n high: number,\n toleranceSize: number,\n outputType: ImageType\n): Promise<Blob> => {\n // Calculate the middle quality value\n const mid = (low + high) / 2\n\n // Calculate the width and height after scaling\n const width = Math.round(imageBitmap.width * mid)\n const height = Math.round(imageBitmap.height * mid)\n\n const offscreenCanvas = new OffscreenCanvas(width, height)\n const offscreenContext = offscreenCanvas.getContext('2d')!\n\n // Draw the scaled image\n offscreenContext.drawImage(imageBitmap, 0, 0, imageBitmap.width, imageBitmap.height, 0, 0, width, height)\n\n const outputBlob = await offscreenCanvas.convertToBlob({ type: outputType, quality: mid })\n\n const currentSize = outputBlob.size\n\n // Check if current size is within tolerance range\n const lowerBound = targetSize + Math.min(0, toleranceSize)\n const upperBound = targetSize + Math.max(0, toleranceSize)\n\n if (currentSize >= lowerBound && currentSize <= upperBound) {\n return outputBlob\n }\n\n // Use relative error\n if ((high - low) / high < 0.001) {\n return outputBlob\n }\n\n if (currentSize > targetSize) {\n return await compress(imageBitmap, targetSize, low, mid, toleranceSize, outputType)\n } else {\n return await compress(imageBitmap, targetSize, mid, high, toleranceSize, outputType)\n }\n}\n\n/**\n * Compress an image to exact target file size using binary search algorithm.\n *\n * @param input - Image blob/file to compress\n * @param options - Compression options\n * @returns Promise that resolves to compressed image blob\n *\n * @example\n * ```typescript\n * // Basic usage - compress to 500KB\n * const compressed = await imgcap(imageFile, { targetSize: 500 * 1024 })\n *\n * // With format conversion\n * const webp = await imgcap(imageFile, {\n * targetSize: 300 * 1024,\n * outputType: 'image/webp'\n * })\n * ```\n */\nexport const imgcap = async (input: Blob, options: Options) => {\n const { targetSize, toleranceSize = -1024 } = options\n\n if (!['image/jpeg', 'image/png', 'image/webp', 'image/avif'].includes(input.type)) {\n throw new Error('Only PNG, JPEG, WebP and AVIF images are supported.')\n }\n\n if (Math.abs(toleranceSize) < 1024) {\n throw new Error('Tolerance size must be at least ±1024 bytes.')\n }\n\n const outputType = options.outputType || (input.type as ImageType)\n\n if (input.size <= targetSize && input.type === outputType) {\n return input\n }\n\n const imageBitmap = await createImageBitmap(input)\n\n // Initialize quality range\n const low = 0\n const high = 1\n\n return await compress(imageBitmap, targetSize, low, high, toleranceSize, outputType)\n}\n\nexport default imgcap\n"],"mappings":"AAaA,MAAM,EAAW,MACfA,EACAC,EACAC,EACAC,EACAC,EACAC,IACkB,CAElB,IAAM,GAAO,EAAM,GAAQ,EAGrB,EAAQ,KAAK,MAAM,EAAY,MAAQ,EAAI,CAC3C,EAAS,KAAK,MAAM,EAAY,OAAS,EAAI,CAE7C,EAAkB,IAAI,gBAAgB,EAAO,GAC7C,EAAmB,EAAgB,WAAW,KAAK,CAGzD,EAAiB,UAAU,EAAa,EAAG,EAAG,EAAY,MAAO,EAAY,OAAQ,EAAG,EAAG,EAAO,EAAO,CAEzG,IAAM,EAAa,KAAM,GAAgB,cAAc,CAAE,KAAM,EAAY,QAAS,CAAK,EAAC,CAEpF,EAAc,EAAW,KAGzB,EAAa,EAAa,KAAK,IAAI,EAAG,EAAc,CACpD,EAAa,EAAa,KAAK,IAAI,EAAG,EAAc,CAcxD,OAZE,GAAe,GAAc,GAAe,IAK3C,EAAO,GAAO,EAAO,KACjB,EAGL,EAAc,EACT,KAAM,GAAS,EAAa,EAAY,EAAK,EAAK,EAAe,EAAW,CAE5E,KAAM,GAAS,EAAa,EAAY,EAAK,EAAM,EAAe,EAAW,AAEvF,EAqBY,EAAS,MAAOC,EAAaC,IAAqB,CAC7D,GAAM,CAAE,aAAY,gBAAgB,MAAO,CAAG,EAE9C,IAAK,CAAC,aAAc,YAAa,aAAc,YAAa,EAAC,SAAS,EAAM,KAAK,CAC/E,KAAM,CAAI,MAAM,sDAAA,CAGlB,GAAI,KAAK,IAAI,EAAc,CAAG,KAC5B,KAAM,CAAI,MAAM,+CAAA,CAGlB,IAAM,EAAa,EAAQ,YAAe,EAAM,KAEhD,GAAI,EAAM,MAAQ,GAAc,EAAM,OAAS,EAC7C,OAAO,EAGT,IAAM,EAAc,KAAM,mBAAkB,EAAM,CAG5C,EAAM,EACN,EAAO,EAEb,OAAO,KAAM,GAAS,EAAa,EAAY,EAAK,EAAM,EAAe,EAAW,AACrF,EAED,IAAA,EAAe"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import globals from 'globals'
|
|
2
|
+
import pluginJs from '@eslint/js'
|
|
3
|
+
import tseslint from 'typescript-eslint'
|
|
4
|
+
import prettierPlugin from 'eslint-plugin-prettier/recommended'
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
{ files: ['**/*.{js,mjs,cjs,ts}'] },
|
|
8
|
+
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
|
|
9
|
+
pluginJs.configs.recommended,
|
|
10
|
+
...tseslint.configs.recommended,
|
|
11
|
+
prettierPlugin,
|
|
12
|
+
{
|
|
13
|
+
ignores: ['**/dist/*']
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
rules: {}
|
|
17
|
+
}
|
|
18
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Precisely cap your image size.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsdown src/index.ts --dts --format esm --sourcemap --watch",
|
|
9
|
+
"build": "tsdown src/index.ts --dts --format esm --sourcemap --minify --clean",
|
|
10
|
+
"lint": "eslint --fix --cache",
|
|
11
|
+
"check": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [],
|
|
14
|
+
"author": "molvqingtai",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@eslint/js": "^9.31.0",
|
|
18
|
+
"eslint": "^9.31.0",
|
|
19
|
+
"eslint-config-prettier": "^10.1.5",
|
|
20
|
+
"eslint-plugin-prettier": "^5.5.1",
|
|
21
|
+
"globals": "^16.3.0",
|
|
22
|
+
"tsdown": "^0.12.9",
|
|
23
|
+
"typescript": "^5.8.3",
|
|
24
|
+
"typescript-eslint": "^8.36.0"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Supported image formats */
|
|
2
|
+
export type ImageType = 'image/jpeg' | 'image/png' | 'image/webp' | 'image/avif'
|
|
3
|
+
|
|
4
|
+
/** Compression options */
|
|
5
|
+
export interface Options {
|
|
6
|
+
/** Target file size in bytes */
|
|
7
|
+
targetSize: number
|
|
8
|
+
/** Size tolerance in bytes (default: -1024) */
|
|
9
|
+
toleranceSize?: number
|
|
10
|
+
/** Output image format (default: same as input) */
|
|
11
|
+
outputType?: ImageType
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const compress = async (
|
|
15
|
+
imageBitmap: ImageBitmap,
|
|
16
|
+
targetSize: number,
|
|
17
|
+
low: number,
|
|
18
|
+
high: number,
|
|
19
|
+
toleranceSize: number,
|
|
20
|
+
outputType: ImageType
|
|
21
|
+
): Promise<Blob> => {
|
|
22
|
+
// Calculate the middle quality value
|
|
23
|
+
const mid = (low + high) / 2
|
|
24
|
+
|
|
25
|
+
// Calculate the width and height after scaling
|
|
26
|
+
const width = Math.round(imageBitmap.width * mid)
|
|
27
|
+
const height = Math.round(imageBitmap.height * mid)
|
|
28
|
+
|
|
29
|
+
const offscreenCanvas = new OffscreenCanvas(width, height)
|
|
30
|
+
const offscreenContext = offscreenCanvas.getContext('2d')!
|
|
31
|
+
|
|
32
|
+
// Draw the scaled image
|
|
33
|
+
offscreenContext.drawImage(imageBitmap, 0, 0, imageBitmap.width, imageBitmap.height, 0, 0, width, height)
|
|
34
|
+
|
|
35
|
+
const outputBlob = await offscreenCanvas.convertToBlob({ type: outputType, quality: mid })
|
|
36
|
+
|
|
37
|
+
const currentSize = outputBlob.size
|
|
38
|
+
|
|
39
|
+
// Check if current size is within tolerance range
|
|
40
|
+
const lowerBound = targetSize + Math.min(0, toleranceSize)
|
|
41
|
+
const upperBound = targetSize + Math.max(0, toleranceSize)
|
|
42
|
+
|
|
43
|
+
if (currentSize >= lowerBound && currentSize <= upperBound) {
|
|
44
|
+
return outputBlob
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Use relative error
|
|
48
|
+
if ((high - low) / high < 0.001) {
|
|
49
|
+
return outputBlob
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (currentSize > targetSize) {
|
|
53
|
+
return await compress(imageBitmap, targetSize, low, mid, toleranceSize, outputType)
|
|
54
|
+
} else {
|
|
55
|
+
return await compress(imageBitmap, targetSize, mid, high, toleranceSize, outputType)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Compress an image to exact target file size using binary search algorithm.
|
|
61
|
+
*
|
|
62
|
+
* @param input - Image blob/file to compress
|
|
63
|
+
* @param options - Compression options
|
|
64
|
+
* @returns Promise that resolves to compressed image blob
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // Basic usage - compress to 500KB
|
|
69
|
+
* const compressed = await imgcap(imageFile, { targetSize: 500 * 1024 })
|
|
70
|
+
*
|
|
71
|
+
* // With format conversion
|
|
72
|
+
* const webp = await imgcap(imageFile, {
|
|
73
|
+
* targetSize: 300 * 1024,
|
|
74
|
+
* outputType: 'image/webp'
|
|
75
|
+
* })
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export const imgcap = async (input: Blob, options: Options) => {
|
|
79
|
+
const { targetSize, toleranceSize = -1024 } = options
|
|
80
|
+
|
|
81
|
+
if (!['image/jpeg', 'image/png', 'image/webp', 'image/avif'].includes(input.type)) {
|
|
82
|
+
throw new Error('Only PNG, JPEG, WebP and AVIF images are supported.')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (Math.abs(toleranceSize) < 1024) {
|
|
86
|
+
throw new Error('Tolerance size must be at least ±1024 bytes.')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const outputType = options.outputType || (input.type as ImageType)
|
|
90
|
+
|
|
91
|
+
if (input.size <= targetSize && input.type === outputType) {
|
|
92
|
+
return input
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const imageBitmap = await createImageBitmap(input)
|
|
96
|
+
|
|
97
|
+
// Initialize quality range
|
|
98
|
+
const low = 0
|
|
99
|
+
const high = 1
|
|
100
|
+
|
|
101
|
+
return await compress(imageBitmap, targetSize, low, high, toleranceSize, outputType)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default imgcap
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
/* Basic Options */
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
|
|
7
|
+
"module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
|
|
8
|
+
"lib": ["ESNext", "Dom"] /* Specify library files to be included in the compilation. */,
|
|
9
|
+
"outDir": "dist" /* Redirect output structure to the directory. */,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"declaration": true /* Generates corresponding '.d.ts' file. */,
|
|
12
|
+
/* Strict Type-Checking Options */
|
|
13
|
+
"strict": true /* Enable all strict type-checking options. */,
|
|
14
|
+
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
|
15
|
+
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
|
16
|
+
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
|
17
|
+
"moduleResolution": "Node",
|
|
18
|
+
"isolatedModules": true,
|
|
19
|
+
"paths": {
|
|
20
|
+
"@/*": ["src/*"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"]
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "imgcap",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Precisely cap your image size.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "core/dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "pnpm --filter core dev",
|
|
9
|
+
"build": "pnpm --filter core build",
|
|
10
|
+
"lint": "npm-run-all -p lint:*",
|
|
11
|
+
"check": "npm-run-all -p check:*",
|
|
12
|
+
"test": "pnpm --filter __tests__ test",
|
|
13
|
+
"lint:core": "pnpm --filter core lint",
|
|
14
|
+
"lint:tests": "pnpm --filter __tests__ lint",
|
|
15
|
+
"check:core": "pnpm --filter core check",
|
|
16
|
+
"check:tests": "pnpm --filter __tests__ check",
|
|
17
|
+
"prepare": "husky"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"image",
|
|
21
|
+
"compression",
|
|
22
|
+
"compress",
|
|
23
|
+
"resize",
|
|
24
|
+
"optimize",
|
|
25
|
+
"binary-search",
|
|
26
|
+
"file-size",
|
|
27
|
+
"target-size",
|
|
28
|
+
"precise-size",
|
|
29
|
+
"webp",
|
|
30
|
+
"avif",
|
|
31
|
+
"jpeg",
|
|
32
|
+
"png",
|
|
33
|
+
"browser",
|
|
34
|
+
"typescript",
|
|
35
|
+
"upload",
|
|
36
|
+
"quality",
|
|
37
|
+
"canvas",
|
|
38
|
+
"offscreen-canvas"
|
|
39
|
+
],
|
|
40
|
+
"author": "molvqingtai",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@commitlint/cli": "^19.8.1",
|
|
44
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
45
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
46
|
+
"@semantic-release/git": "^10.0.1",
|
|
47
|
+
"husky": "^9.1.7",
|
|
48
|
+
"npm-run-all": "^4.1.5",
|
|
49
|
+
"semantic-release": "^24.2.7"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|