gotodev-image-optimizer 0.1.0 → 0.1.2

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/src/index.ts DELETED
@@ -1,13 +0,0 @@
1
- export type { GImageProps } from './components/GImage.tsx'
2
- export { default as GImage } from './components/GImage.tsx'
3
- export type {
4
- BuildManifest,
5
- DeviceFingerprint,
6
- ImageFormat,
7
- ImageMetadata,
8
- ManifestEntry,
9
- OutputFormat,
10
- PluginOptions,
11
- QualityTier,
12
- } from './core/types.ts'
13
- export { default as gotodevImageOptimizer } from './vite-plugin.ts'
@@ -1,66 +0,0 @@
1
- export function computeEntropy(pixels: Uint8Array): number {
2
- const histogram = new Float64Array(256)
3
- const len = pixels.length
4
-
5
- for (let i = 0; i < len; i++) {
6
- const val = pixels[i]
7
- if (val !== undefined) {
8
- histogram[val] = (histogram[val] ?? 0) + 1
9
- }
10
- }
11
-
12
- let entropy = 0
13
- const lenF = len
14
- for (let i = 0; i < 256; i++) {
15
- const count = histogram[i]
16
- if (count && count > 0) {
17
- const p = count / lenF
18
- entropy -= p * Math.log2(p)
19
- }
20
- }
21
-
22
- return entropy
23
- }
24
-
25
- export function computeEdgeDensity(pixels: Uint8Array, width: number, height: number): number {
26
- let edges = 0
27
- let total = 0
28
-
29
- for (let y = 1; y < height - 1; y++) {
30
- for (let x = 1; x < width - 1; x++) {
31
- const idx = y * width + x
32
- const center = pixels[idx] ?? 0
33
- const right = pixels[idx + 1] ?? 0
34
- const down = pixels[idx + width] ?? 0
35
- const dx = Math.abs(center - right)
36
- const dy = Math.abs(center - down)
37
-
38
- if (dx > 20 || dy > 20) {
39
- edges++
40
- }
41
- total++
42
- }
43
- }
44
-
45
- return total > 0 ? edges / total : 0
46
- }
47
-
48
- export function computeSaliency(pixels: Uint8Array, width: number, height: number): number {
49
- const entropy = computeEntropy(pixels)
50
- const edgeDensity = computeEdgeDensity(pixels, width, height)
51
- const normalizedEntropy = entropy / 8.0
52
- return normalizedEntropy * 0.5 + edgeDensity * 0.5
53
- }
54
-
55
- export function computeCenterWeight(
56
- tileX: number,
57
- tileY: number,
58
- tilesX: number,
59
- tilesY: number,
60
- ): number {
61
- const cx = (tileX + 0.5) / tilesX - 0.5
62
- const cy = (tileY + 0.5) / tilesY - 0.5
63
- const dist = Math.sqrt(cx * cx + cy * cy)
64
- const normalizedDist = Math.min(1, dist / Math.SQRT1_2)
65
- return 1.0 - normalizedDist * 0.4
66
- }
package/src/utils/hash.ts DELETED
@@ -1,10 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
-
3
- export function contentHash(buffer: Buffer): string {
4
- return createHash('sha384').update(buffer).digest('base64url').slice(0, 16)
5
- }
6
-
7
- export function sriHash(buffer: Buffer): string {
8
- const hash = createHash('sha384').update(buffer).digest('base64')
9
- return `sha384-${hash}`
10
- }
package/src/utils/ssim.ts DELETED
@@ -1,56 +0,0 @@
1
- export function computeSSIM(
2
- original: Uint8Array,
3
- compressed: Uint8Array,
4
- width: number,
5
- height: number,
6
- ): number {
7
- const K1 = 0.01
8
- const K2 = 0.03
9
- const L = 255
10
- const C1 = (K1 * L) ** 2
11
- const C2 = (K2 * L) ** 2
12
-
13
- const windowSize = 8
14
- const step = 4
15
- let totalSSIM = 0
16
- let windows = 0
17
-
18
- for (let y = 0; y <= height - windowSize; y += step) {
19
- for (let x = 0; x <= width - windowSize; x += step) {
20
- let sumX = 0
21
- let sumY = 0
22
- let sumX2 = 0
23
- let sumY2 = 0
24
- let sumXY = 0
25
- let count = 0
26
-
27
- for (let wy = 0; wy < windowSize; wy++) {
28
- for (let wx = 0; wx < windowSize; wx++) {
29
- const idx = (y + wy) * width + (x + wx)
30
- const ox = original[idx] ?? 0
31
- const cy = compressed[idx] ?? 0
32
- sumX += ox
33
- sumY += cy
34
- sumX2 += ox * ox
35
- sumY2 += cy * cy
36
- sumXY += ox * cy
37
- count++
38
- }
39
- }
40
-
41
- const muX = sumX / count
42
- const muY = sumY / count
43
- const sigmaX2 = sumX2 / count - muX * muX
44
- const sigmaY2 = sumY2 / count - muY * muY
45
- const sigmaXY = sumXY / count - muX * muY
46
-
47
- const numerator = (2 * muX * muY + C1) * (2 * sigmaXY + C2)
48
- const denominator = (muX * muX + muY * muY + C1) * (sigmaX2 + sigmaY2 + C2)
49
-
50
- totalSSIM += denominator > 0 ? numerator / denominator : 1
51
- windows++
52
- }
53
- }
54
-
55
- return windows > 0 ? totalSSIM / windows : 1
56
- }
@@ -1,73 +0,0 @@
1
- import { availableParallelism } from 'node:os'
2
-
3
- export type WorkerTask<T> = () => Promise<T>
4
-
5
- interface QueueItem {
6
- task: WorkerTask<unknown>
7
- resolve: (value: unknown) => void
8
- reject: (reason: unknown) => void
9
- }
10
-
11
- const POOL_SIZE = Math.max(1, availableParallelism() - 1)
12
-
13
- export class WorkerPool {
14
- private queue: QueueItem[] = []
15
- private active = 0
16
- private maxWorkers: number
17
-
18
- constructor(maxWorkers: number = POOL_SIZE) {
19
- this.maxWorkers = Math.max(1, maxWorkers)
20
- }
21
-
22
- async run<T>(task: WorkerTask<T>): Promise<T> {
23
- if (this.active < this.maxWorkers) {
24
- this.active++
25
- try {
26
- const result = await task()
27
- this.active--
28
- this.drain()
29
- return result
30
- } catch (error) {
31
- this.active--
32
- this.drain()
33
- throw error
34
- }
35
- }
36
-
37
- return new Promise<T>((resolve, reject) => {
38
- this.queue.push({
39
- task: task as WorkerTask<unknown>,
40
- resolve: resolve as (value: unknown) => void,
41
- reject,
42
- })
43
- })
44
- }
45
-
46
- private drain(): void {
47
- while (this.active < this.maxWorkers && this.queue.length > 0) {
48
- const item = this.queue.shift()
49
- if (!item) break
50
- this.active++
51
- item
52
- .task()
53
- .then((result) => {
54
- this.active--
55
- item.resolve(result)
56
- this.drain()
57
- })
58
- .catch((error) => {
59
- this.active--
60
- item.reject(error)
61
- this.drain()
62
- })
63
- }
64
- }
65
-
66
- get pending(): number {
67
- return this.queue.length
68
- }
69
-
70
- get running(): number {
71
- return this.active
72
- }
73
- }
@@ -1,111 +0,0 @@
1
- import { existsSync, mkdirSync } from 'node:fs'
2
- import { basename, extname, resolve } from 'node:path'
3
- import type { Plugin, ResolvedConfig } from 'vite'
4
- import { encodeImage } from './core/encoder.ts'
5
- import { addToManifest, createManifest, writeManifest } from './core/manifest.ts'
6
- import type { PluginOptions, QualityTier, TierConfig } from './core/types.ts'
7
-
8
- const IMAGE_EXTENSIONS = /\.(jpe?g|png|webp|avif|gif|svg|bmp|tiff?|ico)$/i
9
-
10
- const DEFAULT_TIERS: Record<QualityTier, TierConfig> = {
11
- ultra: { quality: 90, widths: [480, 768, 1024, 1920] },
12
- high: { quality: 80, widths: [480, 768, 1024] },
13
- medium: { quality: 60, widths: [480, 768] },
14
- low: { quality: 40, widths: [480] },
15
- }
16
-
17
- export default function gotodevImageOptimizer(userOptions: PluginOptions = {}): Plugin {
18
- let config: ResolvedConfig
19
- let manifest = createManifest()
20
- const options: PluginOptions = {
21
- tiers: { ...DEFAULT_TIERS, ...userOptions.tiers },
22
- adaptive: userOptions.adaptive ?? true,
23
- autoTune: userOptions.autoTune ?? true,
24
- preprocess: userOptions.preprocess ?? true,
25
- faceDetection: userOptions.faceDetection ?? true,
26
- formats: userOptions.formats ?? ['avif', 'webp', 'jpeg'],
27
- maxFileSize: userOptions.maxFileSize ?? 50 * 1024 * 1024,
28
- verbose: userOptions.verbose ?? false,
29
- }
30
-
31
- return {
32
- name: 'gotodev-image-optimizer',
33
- enforce: 'post',
34
-
35
- configResolved(resolved: ResolvedConfig) {
36
- config = resolved
37
- },
38
-
39
- async buildStart() {
40
- manifest = createManifest()
41
- },
42
-
43
- async transform(_code: string, id: string) {
44
- if (!IMAGE_EXTENSIONS.test(id)) return
45
- if (id.includes('node_modules')) return
46
-
47
- const outDir = resolve(config.root, config.build.outDir ?? 'dist', 'assets')
48
- if (!existsSync(outDir)) {
49
- mkdirSync(outDir, { recursive: true })
50
- }
51
-
52
- const tiers = options.tiers as Record<QualityTier, TierConfig>
53
-
54
- const formats = options.formats ?? (['avif', 'webp', 'jpeg'] as const)
55
-
56
- try {
57
- const entry = await encodeImage(id, {
58
- widths: tiers.high.widths,
59
- formats: [...formats],
60
- tiers,
61
- autoTune: options.autoTune ?? true,
62
- adaptive: options.adaptive ?? true,
63
- preprocess: options.preprocess ?? true,
64
- faceDetection: options.faceDetection ?? true,
65
- outDir,
66
- })
67
-
68
- const key = basename(id)
69
- addToManifest(manifest, key, entry)
70
-
71
- if (options.verbose) {
72
- console.log(`[gotodev-image-optimizer] Optimized: ${key}`)
73
- }
74
-
75
- const manifestData = JSON.stringify(entry)
76
- const manifestPath = resolve(outDir, 'gimage-manifest.json')
77
- writeManifest(manifest, manifestPath)
78
-
79
- return {
80
- code: `export default ${manifestData};`,
81
- map: null,
82
- }
83
- } catch (error) {
84
- if (options.verbose) {
85
- console.error(`[gotodev-image-optimizer] Failed to optimize ${id}:`, error)
86
- }
87
- return {
88
- code: `export default ${JSON.stringify({
89
- src: basename(id),
90
- width: 0,
91
- height: 0,
92
- format: extname(id).slice(1),
93
- placeholder: '',
94
- variants: [],
95
- tiers: {},
96
- })};`,
97
- map: null,
98
- }
99
- }
100
- },
101
-
102
- closeBundle() {
103
- const outDir = resolve(config.root, config.build.outDir ?? 'dist', 'assets')
104
- if (!existsSync(outDir)) {
105
- mkdirSync(outDir, { recursive: true })
106
- }
107
- const manifestPath = resolve(outDir, 'gimage-manifest.json')
108
- writeManifest(manifest, manifestPath)
109
- },
110
- }
111
- }