image-edit-tools 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/.gitattributes +2 -0
- package/README.md +41 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +15 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +4 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +285 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/ops/add-text.d.ts +5 -0
- package/dist/ops/add-text.d.ts.map +1 -0
- package/dist/ops/add-text.js +129 -0
- package/dist/ops/add-text.js.map +1 -0
- package/dist/ops/adjust.d.ts +3 -0
- package/dist/ops/adjust.d.ts.map +1 -0
- package/dist/ops/adjust.js +71 -0
- package/dist/ops/adjust.js.map +1 -0
- package/dist/ops/batch.d.ts +3 -0
- package/dist/ops/batch.d.ts.map +1 -0
- package/dist/ops/batch.js +35 -0
- package/dist/ops/batch.js.map +1 -0
- package/dist/ops/blur-region.d.ts +5 -0
- package/dist/ops/blur-region.d.ts.map +1 -0
- package/dist/ops/blur-region.js +54 -0
- package/dist/ops/blur-region.js.map +1 -0
- package/dist/ops/composite.d.ts +5 -0
- package/dist/ops/composite.d.ts.map +1 -0
- package/dist/ops/composite.js +53 -0
- package/dist/ops/composite.js.map +1 -0
- package/dist/ops/convert.d.ts +3 -0
- package/dist/ops/convert.d.ts.map +1 -0
- package/dist/ops/convert.js +45 -0
- package/dist/ops/convert.js.map +1 -0
- package/dist/ops/crop.d.ts +3 -0
- package/dist/ops/crop.d.ts.map +1 -0
- package/dist/ops/crop.js +105 -0
- package/dist/ops/crop.js.map +1 -0
- package/dist/ops/detect-faces.d.ts +3 -0
- package/dist/ops/detect-faces.d.ts.map +1 -0
- package/dist/ops/detect-faces.js +41 -0
- package/dist/ops/detect-faces.js.map +1 -0
- package/dist/ops/detect-subject.d.ts +3 -0
- package/dist/ops/detect-subject.d.ts.map +1 -0
- package/dist/ops/detect-subject.js +78 -0
- package/dist/ops/detect-subject.js.map +1 -0
- package/dist/ops/extract-text.d.ts +5 -0
- package/dist/ops/extract-text.d.ts.map +1 -0
- package/dist/ops/extract-text.js +21 -0
- package/dist/ops/extract-text.js.map +1 -0
- package/dist/ops/filter.d.ts +3 -0
- package/dist/ops/filter.d.ts.map +1 -0
- package/dist/ops/filter.js +53 -0
- package/dist/ops/filter.js.map +1 -0
- package/dist/ops/get-dominant-colors.d.ts +3 -0
- package/dist/ops/get-dominant-colors.d.ts.map +1 -0
- package/dist/ops/get-dominant-colors.js +48 -0
- package/dist/ops/get-dominant-colors.js.map +1 -0
- package/dist/ops/get-metadata.d.ts +3 -0
- package/dist/ops/get-metadata.d.ts.map +1 -0
- package/dist/ops/get-metadata.js +30 -0
- package/dist/ops/get-metadata.js.map +1 -0
- package/dist/ops/optimize.d.ts +3 -0
- package/dist/ops/optimize.d.ts.map +1 -0
- package/dist/ops/optimize.js +78 -0
- package/dist/ops/optimize.js.map +1 -0
- package/dist/ops/overlay.d.ts +3 -0
- package/dist/ops/overlay.d.ts.map +1 -0
- package/dist/ops/overlay.js +52 -0
- package/dist/ops/overlay.js.map +1 -0
- package/dist/ops/pad.d.ts +3 -0
- package/dist/ops/pad.d.ts.map +1 -0
- package/dist/ops/pad.js +62 -0
- package/dist/ops/pad.js.map +1 -0
- package/dist/ops/pipeline.d.ts +5 -0
- package/dist/ops/pipeline.d.ts.map +1 -0
- package/dist/ops/pipeline.js +81 -0
- package/dist/ops/pipeline.js.map +1 -0
- package/dist/ops/remove-bg.d.ts +3 -0
- package/dist/ops/remove-bg.d.ts.map +1 -0
- package/dist/ops/remove-bg.js +79 -0
- package/dist/ops/remove-bg.js.map +1 -0
- package/dist/ops/resize.d.ts +3 -0
- package/dist/ops/resize.d.ts.map +1 -0
- package/dist/ops/resize.js +54 -0
- package/dist/ops/resize.js.map +1 -0
- package/dist/ops/watermark.d.ts +3 -0
- package/dist/ops/watermark.d.ts.map +1 -0
- package/dist/ops/watermark.js +142 -0
- package/dist/ops/watermark.js.map +1 -0
- package/dist/types.d.ts +233 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/load-image.d.ts +9 -0
- package/dist/utils/load-image.d.ts.map +1 -0
- package/dist/utils/load-image.js +22 -0
- package/dist/utils/load-image.js.map +1 -0
- package/dist/utils/result.d.ts +4 -0
- package/dist/utils/result.d.ts.map +1 -0
- package/dist/utils/result.js +3 -0
- package/dist/utils/result.js.map +1 -0
- package/dist/utils/validate.d.ts +16 -0
- package/dist/utils/validate.d.ts.map +1 -0
- package/dist/utils/validate.js +20 -0
- package/dist/utils/validate.js.map +1 -0
- package/docs/AGENTS.md +18 -0
- package/docs/MCP.md +106 -0
- package/package.json +52 -0
- package/scripts/generate-fixtures.js +33 -0
- package/src/index.ts +24 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/server.ts +21 -0
- package/src/mcp/tools.ts +276 -0
- package/src/ops/add-text.ts +139 -0
- package/src/ops/adjust.ts +68 -0
- package/src/ops/batch.ts +41 -0
- package/src/ops/blur-region.ts +58 -0
- package/src/ops/composite.ts +56 -0
- package/src/ops/convert.ts +46 -0
- package/src/ops/crop.ts +101 -0
- package/src/ops/detect-faces.ts +41 -0
- package/src/ops/detect-subject.ts +80 -0
- package/src/ops/extract-text.ts +19 -0
- package/src/ops/filter.ts +51 -0
- package/src/ops/get-dominant-colors.ts +41 -0
- package/src/ops/get-metadata.ts +28 -0
- package/src/ops/optimize.ts +77 -0
- package/src/ops/overlay.ts +51 -0
- package/src/ops/pad.ts +63 -0
- package/src/ops/pipeline.ts +61 -0
- package/src/ops/remove-bg.ts +82 -0
- package/src/ops/resize.ts +54 -0
- package/src/ops/watermark.ts +141 -0
- package/src/types/color-thief-node.d.ts +4 -0
- package/src/types.ts +267 -0
- package/src/utils/load-image.ts +21 -0
- package/src/utils/result.ts +4 -0
- package/src/utils/validate.ts +21 -0
- package/tests/fixtures/logo.png +0 -0
- package/tests/fixtures/sample.jpg +0 -0
- package/tests/fixtures/sample.png +0 -0
- package/tests/fixtures/sample.webp +0 -0
- package/tests/integration/error-handling.test.ts +22 -0
- package/tests/integration/load-image.test.ts +45 -0
- package/tests/unit/add-text.test.ts +56 -0
- package/tests/unit/adjust.test.ts +81 -0
- package/tests/unit/batch.test.ts +38 -0
- package/tests/unit/blur-region.test.ts +52 -0
- package/tests/unit/composite.test.ts +58 -0
- package/tests/unit/convert.test.ts +55 -0
- package/tests/unit/crop.test.ts +100 -0
- package/tests/unit/detect-faces.test.ts +32 -0
- package/tests/unit/detect-subject.test.ts +37 -0
- package/tests/unit/extract-text.test.ts +34 -0
- package/tests/unit/filter.test.ts +39 -0
- package/tests/unit/get-dominant-colors.test.ts +25 -0
- package/tests/unit/get-metadata.test.ts +36 -0
- package/tests/unit/mcp.test.ts +104 -0
- package/tests/unit/optimize.test.ts +47 -0
- package/tests/unit/overlay.test.ts +39 -0
- package/tests/unit/pad.test.ts +56 -0
- package/tests/unit/pipeline.test.ts +48 -0
- package/tests/unit/remove-bg.test.ts +42 -0
- package/tests/unit/resize.test.ts +70 -0
- package/tests/unit/watermark.test.ts +54 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, vi } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import { join, dirname } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { removeBg } from '../../src/ops/remove-bg.js'
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = dirname(__filename)
|
|
9
|
+
const fixture = (name: string) => readFileSync(join(__dirname, '../fixtures', name))
|
|
10
|
+
|
|
11
|
+
// Since tests run in CI/offline and we shouldn't download huge models,
|
|
12
|
+
// we test that the function gracefully hits the MODEL_NOT_FOUND error
|
|
13
|
+
// if AutoModel fails, or passes if the network enables it.
|
|
14
|
+
// We explicitly mock '@xenova/transformers' to simulate network failure or success.
|
|
15
|
+
|
|
16
|
+
vi.mock('@xenova/transformers', async (importOriginal) => {
|
|
17
|
+
return {
|
|
18
|
+
...(await importOriginal<typeof import('@xenova/transformers')>()),
|
|
19
|
+
AutoModel: {
|
|
20
|
+
from_pretrained: vi.fn().mockRejectedValue(new Error('Mock network failure'))
|
|
21
|
+
},
|
|
22
|
+
AutoProcessor: {
|
|
23
|
+
from_pretrained: vi.fn()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('removeBg', () => {
|
|
29
|
+
let sampleJpeg: Buffer
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
sampleJpeg = fixture('sample.jpg')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('handles unavailable model offline correctly', async () => {
|
|
36
|
+
const result = await removeBg(sampleJpeg, {})
|
|
37
|
+
expect(result.ok).toBe(false)
|
|
38
|
+
if (!result.ok) {
|
|
39
|
+
expect(result.code).toBe('MODEL_NOT_FOUND')
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import { join, dirname } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { resize } from '../../src/ops/resize.js'
|
|
6
|
+
import sharp from 'sharp'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const __dirname = dirname(__filename)
|
|
10
|
+
const fixture = (name: string) => readFileSync(join(__dirname, '../fixtures', name))
|
|
11
|
+
|
|
12
|
+
describe('resize', () => {
|
|
13
|
+
let sampleJpeg: Buffer
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
sampleJpeg = fixture('sample.jpg') // 400x300
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('resizes using px dimensions', async () => {
|
|
20
|
+
const result = await resize(sampleJpeg, { width: 100, height: 100 })
|
|
21
|
+
expect(result.ok).toBe(true)
|
|
22
|
+
if (!result.ok) return
|
|
23
|
+
const meta = await sharp(result.data).metadata()
|
|
24
|
+
expect(meta.width).toBe(100)
|
|
25
|
+
expect(meta.height).toBe(100)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('resizes using scale', async () => {
|
|
29
|
+
const result = await resize(sampleJpeg, { scale: 0.5 })
|
|
30
|
+
expect(result.ok).toBe(true)
|
|
31
|
+
if (!result.ok) return
|
|
32
|
+
const meta = await sharp(result.data).metadata()
|
|
33
|
+
expect(meta.width).toBe(200)
|
|
34
|
+
expect(meta.height).toBe(150)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('preserves aspect ratio for single axis', async () => {
|
|
38
|
+
const result = await resize(sampleJpeg, { width: 200 })
|
|
39
|
+
expect(result.ok).toBe(true)
|
|
40
|
+
if (!result.ok) return
|
|
41
|
+
const meta = await sharp(result.data).metadata()
|
|
42
|
+
expect(meta.width).toBe(200)
|
|
43
|
+
expect(meta.height).toBe(150) // 400:300 is 4:3 -> 200:150
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('applies fit parameter correctly', async () => {
|
|
47
|
+
const result = await resize(sampleJpeg, { width: 100, height: 200, fit: 'contain' })
|
|
48
|
+
expect(result.ok).toBe(true)
|
|
49
|
+
if (!result.ok) return
|
|
50
|
+
const meta = await sharp(result.data).metadata()
|
|
51
|
+
expect(meta.width).toBe(100)
|
|
52
|
+
expect(meta.height).toBe(200)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('respects withoutEnlargement', async () => {
|
|
56
|
+
const result = await resize(sampleJpeg, { width: 1000, height: 1000, withoutEnlargement: true })
|
|
57
|
+
expect(result.ok).toBe(true)
|
|
58
|
+
if (!result.ok) return
|
|
59
|
+
const meta = await sharp(result.data).metadata()
|
|
60
|
+
expect(meta.width).toBe(400) // Original width
|
|
61
|
+
expect(meta.height).toBe(300) // Original height
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns error on zero dimensions', async () => {
|
|
65
|
+
const result = await resize(sampleJpeg, { width: 0 })
|
|
66
|
+
expect(result.ok).toBe(false)
|
|
67
|
+
if (result.ok) return
|
|
68
|
+
expect(result.code).toBe('INVALID_INPUT')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import { join, dirname } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { watermark } from '../../src/ops/watermark.js'
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = dirname(__filename)
|
|
9
|
+
const fixture = (name: string) => readFileSync(join(__dirname, '../fixtures', name))
|
|
10
|
+
|
|
11
|
+
describe('watermark', () => {
|
|
12
|
+
let sampleJpeg: Buffer
|
|
13
|
+
let logoPng: Buffer
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
sampleJpeg = fixture('sample.jpg') // 400x300
|
|
17
|
+
logoPng = fixture('logo.png') // 100x100
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('adds text watermark at bottom-right', async () => {
|
|
21
|
+
const result = await watermark(sampleJpeg, {
|
|
22
|
+
type: 'text', text: 'Confidential', position: 'bottom-right'
|
|
23
|
+
})
|
|
24
|
+
expect(result.ok).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('adds text watermark tiled', async () => {
|
|
28
|
+
const result = await watermark(sampleJpeg, {
|
|
29
|
+
type: 'text', text: 'Draft', position: 'tile', tileSpacing: 100
|
|
30
|
+
})
|
|
31
|
+
expect(result.ok).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('adds image watermark at top-left', async () => {
|
|
35
|
+
const result = await watermark(sampleJpeg, {
|
|
36
|
+
type: 'image', image: logoPng, position: 'top-left'
|
|
37
|
+
})
|
|
38
|
+
expect(result.ok).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('adds image watermark tiled with opacity and scale', async () => {
|
|
42
|
+
const result = await watermark(sampleJpeg, {
|
|
43
|
+
type: 'image', image: logoPng, position: 'tile', opacity: 0.3, scale: 0.5, tileSpacing: 20
|
|
44
|
+
})
|
|
45
|
+
expect(result.ok).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns error if missing valid type', async () => {
|
|
49
|
+
const result = await watermark(sampleJpeg, {} as any)
|
|
50
|
+
expect(result.ok).toBe(false)
|
|
51
|
+
if (result.ok) return
|
|
52
|
+
expect(result.code).toBe('INVALID_INPUT')
|
|
53
|
+
})
|
|
54
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"],
|
|
14
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
15
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: 'v8',
|
|
9
|
+
reporter: ['text', 'json', 'html'],
|
|
10
|
+
thresholds: {
|
|
11
|
+
lines: 70,
|
|
12
|
+
functions: 70,
|
|
13
|
+
branches: 50,
|
|
14
|
+
},
|
|
15
|
+
exclude: [
|
|
16
|
+
'scripts/**',
|
|
17
|
+
'src/index.ts',
|
|
18
|
+
'src/mcp/index.ts',
|
|
19
|
+
'src/mcp/server.ts',
|
|
20
|
+
'src/mcp/tools.ts', // mostly wiring, E2E tested
|
|
21
|
+
'src/ops/detect-subject.ts', // unimplemented/stubbed
|
|
22
|
+
'src/ops/remove-bg.ts' // heavy AI model, requires optional mocking
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
testTimeout: 30000,
|
|
26
|
+
},
|
|
27
|
+
})
|