@xyo-network/image-thumbnail-plugin 2.70.10 → 2.70.12
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/dist/cjs/Witness/Witness.js +146 -146
- package/dist/cjs/Witness/Witness.js.map +1 -1
- package/dist/docs.json +3820 -2370
- package/dist/esm/Witness/Witness.js +138 -143
- package/dist/esm/Witness/Witness.js.map +1 -1
- package/dist/types/Witness/Witness.d.ts +27 -8
- package/dist/types/Witness/Witness.d.ts.map +1 -1
- package/package.json +11 -9
- package/src/Witness/Witness.ts +155 -154
package/src/Witness/Witness.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { promises as dnsPromises } from 'node:dns'
|
|
2
|
+
|
|
1
3
|
import { axios, AxiosError, AxiosResponse } from '@xyo-network/axios'
|
|
2
4
|
import { PayloadHasher } from '@xyo-network/core'
|
|
3
|
-
import {
|
|
4
|
-
import { isPayload, ModuleError, ModuleErrorSchema } from '@xyo-network/payload-model'
|
|
5
|
+
import { ImageThumbnail, ImageThumbnailSchema } from '@xyo-network/image-thumbnail-payload-plugin'
|
|
5
6
|
import { UrlPayload } from '@xyo-network/url-payload-plugin'
|
|
6
7
|
import { AbstractWitness } from '@xyo-network/witness'
|
|
7
8
|
import { subClass } from 'gm'
|
|
9
|
+
import { sync as hasbin } from 'hasbin'
|
|
8
10
|
import { sha256 } from 'hash-wasm'
|
|
9
11
|
import compact from 'lodash/compact'
|
|
10
|
-
import isBuffer from 'lodash/isBuffer'
|
|
11
12
|
import { LRUCache } from 'lru-cache'
|
|
12
13
|
import shajs from 'sha.js'
|
|
13
14
|
import Url from 'url-parse'
|
|
@@ -19,63 +20,22 @@ import { ImageThumbnailWitnessParams } from './Params'
|
|
|
19
20
|
|
|
20
21
|
const gm = subClass({ imageMagick: '7+' })
|
|
21
22
|
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
return await sha256(data)
|
|
27
|
-
} catch (ex) {
|
|
28
|
-
PayloadHasher.wasmSupport.allowWasm = false
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// eslint-disable-next-line deprecation/deprecation
|
|
32
|
-
return shajs('sha256').update(data).digest().toString()
|
|
23
|
+
export interface ImageThumbnailWitnessError extends Error {
|
|
24
|
+
name: 'ImageThumbnailWitnessError'
|
|
25
|
+
url: string
|
|
33
26
|
}
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let protocol = url.protocol
|
|
38
|
-
let host = url.host
|
|
39
|
-
let path = url.pathname
|
|
40
|
-
const query = url.query
|
|
41
|
-
if (protocol === 'ipfs:') {
|
|
42
|
-
protocol = 'https:'
|
|
43
|
-
host = 'cloudflare-ipfs.com'
|
|
44
|
-
path = url.host === 'ipfs' ? `ipfs${path}` : `ipfs/${url.host}${path}`
|
|
45
|
-
const root = `${protocol}//${host}/${path}`
|
|
46
|
-
return query?.length > 0 ? `root?${query}` : root
|
|
47
|
-
} else {
|
|
48
|
-
return urlToCheck
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const checkDataUrl = (url: string): [Buffer | undefined, ImageThumbnailErrorPayload | undefined] => {
|
|
53
|
-
if (url.startsWith('data:image')) {
|
|
54
|
-
const data = url.split(',')[1]
|
|
55
|
-
if (data) {
|
|
56
|
-
const buffer = Buffer.from(Uint8Array.from(atob(data), (c) => c.charCodeAt(0)))
|
|
57
|
-
console.log(`data buffer: ${buffer.length}`)
|
|
58
|
-
return [buffer, undefined]
|
|
59
|
-
} else {
|
|
60
|
-
const error: ImageThumbnailErrorPayload = {
|
|
61
|
-
message: 'Invalid data Url',
|
|
62
|
-
schema: ModuleErrorSchema,
|
|
63
|
-
url,
|
|
64
|
-
}
|
|
65
|
-
return [undefined, error]
|
|
66
|
-
}
|
|
67
|
-
} else {
|
|
68
|
-
return [undefined, undefined]
|
|
69
|
-
}
|
|
28
|
+
export interface DnsError extends Error {
|
|
29
|
+
code: string
|
|
70
30
|
}
|
|
71
31
|
|
|
72
32
|
export class ImageThumbnailWitness<TParams extends ImageThumbnailWitnessParams = ImageThumbnailWitnessParams> extends AbstractWitness<TParams> {
|
|
73
33
|
static override configSchemas = [ImageThumbnailWitnessConfigSchema]
|
|
74
34
|
|
|
75
|
-
private _cache?: LRUCache<string,
|
|
35
|
+
private _cache?: LRUCache<string, ImageThumbnail>
|
|
76
36
|
|
|
77
37
|
get cache() {
|
|
78
|
-
this._cache = this._cache ?? new LRUCache<string,
|
|
38
|
+
this._cache = this._cache ?? new LRUCache<string, ImageThumbnail>({ max: this.maxCacheEntries })
|
|
79
39
|
return this._cache
|
|
80
40
|
}
|
|
81
41
|
|
|
@@ -99,124 +59,165 @@ export class ImageThumbnailWitness<TParams extends ImageThumbnailWitnessParams =
|
|
|
99
59
|
return this.config.width ?? 128
|
|
100
60
|
}
|
|
101
61
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return [url, dataBuffer]
|
|
115
|
-
}
|
|
62
|
+
private static async binaryToSha256(data: Uint8Array) {
|
|
63
|
+
await PayloadHasher.wasmInitialized
|
|
64
|
+
if (PayloadHasher.wasmSupport.canUseWasm) {
|
|
65
|
+
try {
|
|
66
|
+
return await sha256(data)
|
|
67
|
+
} catch (ex) {
|
|
68
|
+
PayloadHasher.wasmSupport.allowWasm = false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
72
|
+
return shajs('sha256').update(data).digest().toString()
|
|
73
|
+
}
|
|
116
74
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
75
|
+
private static bufferFromDataUrl(url: string): Buffer | undefined {
|
|
76
|
+
if (url.startsWith('data:image')) {
|
|
77
|
+
const data = url.split(',')[1]
|
|
78
|
+
if (data) {
|
|
79
|
+
const buffer = Buffer.from(Uint8Array.from(atob(data), (c) => c.charCodeAt(0)))
|
|
80
|
+
console.log(`data buffer: ${buffer.length}`)
|
|
81
|
+
return buffer
|
|
82
|
+
} else {
|
|
83
|
+
const error: ImageThumbnailWitnessError = {
|
|
84
|
+
message: 'Invalid data Url',
|
|
85
|
+
name: 'ImageThumbnailWitnessError',
|
|
86
|
+
url,
|
|
87
|
+
}
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
120
92
|
|
|
121
|
-
|
|
122
|
-
|
|
93
|
+
private static checkIpfsUrl(urlToCheck: string) {
|
|
94
|
+
const url = new Url(urlToCheck)
|
|
95
|
+
let protocol = url.protocol
|
|
96
|
+
let host = url.host
|
|
97
|
+
let path = url.pathname
|
|
98
|
+
const query = url.query
|
|
99
|
+
if (protocol === 'ipfs:') {
|
|
100
|
+
protocol = 'https:'
|
|
101
|
+
host = 'cloudflare-ipfs.com'
|
|
102
|
+
path = url.host === 'ipfs' ? `ipfs${path}` : `ipfs/${url.host}${path}`
|
|
103
|
+
const root = `${protocol}//${host}/${path}`
|
|
104
|
+
return query?.length > 0 ? `root?${query}` : root
|
|
105
|
+
} else {
|
|
106
|
+
return urlToCheck
|
|
107
|
+
}
|
|
108
|
+
}
|
|
123
109
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
responseType: 'arraybuffer',
|
|
129
|
-
}),
|
|
130
|
-
]
|
|
131
|
-
} catch (ex) {
|
|
132
|
-
const axiosError = ex as AxiosError
|
|
133
|
-
if (axiosError.isAxiosError) {
|
|
134
|
-
//selectively pick fields from AxiosError
|
|
135
|
-
const errorPayload: ImageThumbnailErrorPayload = {
|
|
136
|
-
code: axiosError.code,
|
|
137
|
-
message: axiosError.message,
|
|
138
|
-
schema: ModuleErrorSchema,
|
|
139
|
-
status: axiosError.status,
|
|
140
|
-
url,
|
|
141
|
-
}
|
|
142
|
-
return [url, errorPayload]
|
|
143
|
-
} else {
|
|
144
|
-
throw ex
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}),
|
|
148
|
-
),
|
|
149
|
-
)
|
|
110
|
+
protected override async observeHandler(payloads: UrlPayload[] = []): Promise<ImageThumbnail[]> {
|
|
111
|
+
if (!hasbin('magick')) {
|
|
112
|
+
throw Error('ImageMagick is required for this witness')
|
|
113
|
+
}
|
|
150
114
|
return compact(
|
|
151
115
|
await Promise.all(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return
|
|
116
|
+
payloads.map<Promise<ImageThumbnail>>(async ({ url }) => {
|
|
117
|
+
const cachedResult = this.cache.get(url)
|
|
118
|
+
if (cachedResult) {
|
|
119
|
+
return cachedResult
|
|
156
120
|
}
|
|
121
|
+
let result: ImageThumbnail
|
|
157
122
|
|
|
158
|
-
|
|
123
|
+
//if it is a data URL, return a Buffer
|
|
124
|
+
const dataBuffer = ImageThumbnailWitness.bufferFromDataUrl(url)
|
|
159
125
|
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
} else {
|
|
163
|
-
const response = urlResult as AxiosResponse
|
|
164
|
-
|
|
165
|
-
if (response.status >= 200 && response.status < 300) {
|
|
166
|
-
const contentType = response.headers['content-type']?.toString()
|
|
167
|
-
if (contentType.split('/')[0] !== 'image') {
|
|
168
|
-
const error: ImageThumbnailErrorPayload = {
|
|
169
|
-
message: `Invalid file type: ${contentType}`,
|
|
170
|
-
schema: ModuleErrorSchema,
|
|
171
|
-
url,
|
|
172
|
-
}
|
|
173
|
-
this.cache.set(url, error)
|
|
174
|
-
return error
|
|
175
|
-
}
|
|
176
|
-
sourceBuffer = Buffer.from(response.data, 'binary')
|
|
177
|
-
} else {
|
|
178
|
-
const error: ImageThumbnailErrorPayload = {
|
|
179
|
-
schema: ModuleErrorSchema,
|
|
180
|
-
status: response.status,
|
|
181
|
-
url,
|
|
182
|
-
}
|
|
183
|
-
this.cache.set(url, error)
|
|
184
|
-
return error
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
const thumb = await new Promise<Buffer>((resolve, reject) => {
|
|
189
|
-
gm(sourceBuffer)
|
|
190
|
-
.quality(this.quality)
|
|
191
|
-
.resize(this.width, this.height)
|
|
192
|
-
.flatten()
|
|
193
|
-
.toBuffer(this.encoding, (error, buffer) => {
|
|
194
|
-
if (error) {
|
|
195
|
-
reject(error)
|
|
196
|
-
} else {
|
|
197
|
-
resolve(buffer)
|
|
198
|
-
}
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
const result: ImageThumbnailPayload = {
|
|
126
|
+
if (dataBuffer) {
|
|
127
|
+
result = {
|
|
202
128
|
schema: ImageThumbnailSchema,
|
|
203
|
-
sourceHash: await binaryToSha256(
|
|
129
|
+
sourceHash: await ImageThumbnailWitness.binaryToSha256(dataBuffer),
|
|
204
130
|
sourceUrl: url,
|
|
205
|
-
url: `data:image/png;base64,${thumb.toString('base64')}`,
|
|
206
|
-
}
|
|
207
|
-
this.cache.set(url, result)
|
|
208
|
-
return result
|
|
209
|
-
} catch (ex) {
|
|
210
|
-
const error: ImageThumbnailErrorPayload = {
|
|
211
|
-
message: 'Failed to resize image',
|
|
212
|
-
schema: ModuleErrorSchema,
|
|
213
131
|
url,
|
|
214
132
|
}
|
|
215
|
-
|
|
216
|
-
|
|
133
|
+
} else {
|
|
134
|
+
//if it is ipfs, go through cloud flair
|
|
135
|
+
const mutatedUrl = ImageThumbnailWitness.checkIpfsUrl(url)
|
|
136
|
+
result = await this.fromHttp(mutatedUrl)
|
|
217
137
|
}
|
|
138
|
+
this.cache.set(url, result)
|
|
139
|
+
return result
|
|
218
140
|
}),
|
|
219
141
|
),
|
|
220
142
|
)
|
|
221
143
|
}
|
|
144
|
+
|
|
145
|
+
private async createThumbnail(sourceBuffer: Buffer) {
|
|
146
|
+
const thumb = await new Promise<Buffer>((resolve, reject) => {
|
|
147
|
+
gm(sourceBuffer)
|
|
148
|
+
.quality(this.quality)
|
|
149
|
+
.resize(this.width, this.height)
|
|
150
|
+
.flatten()
|
|
151
|
+
.toBuffer(this.encoding, (error, buffer) => {
|
|
152
|
+
if (error) {
|
|
153
|
+
reject(error)
|
|
154
|
+
} else {
|
|
155
|
+
resolve(buffer)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
return `data:image/png;base64,${thumb.toString('base64')}`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async fromHttp(url: string): Promise<ImageThumbnail> {
|
|
163
|
+
let response: AxiosResponse
|
|
164
|
+
let dnsResult: string[]
|
|
165
|
+
try {
|
|
166
|
+
const urlObj = new Url(url)
|
|
167
|
+
dnsResult = await dnsPromises.resolve(urlObj.host)
|
|
168
|
+
console.log(`dnsResult: ${JSON.stringify(dnsResult, null, 2)}`)
|
|
169
|
+
} catch (ex) {
|
|
170
|
+
const error = ex as DnsError
|
|
171
|
+
const result: ImageThumbnail = {
|
|
172
|
+
http: {
|
|
173
|
+
dnsError: error.code,
|
|
174
|
+
},
|
|
175
|
+
schema: ImageThumbnailSchema,
|
|
176
|
+
sourceUrl: url,
|
|
177
|
+
}
|
|
178
|
+
return result
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
response = await axios.get(url, {
|
|
182
|
+
responseType: 'arraybuffer',
|
|
183
|
+
})
|
|
184
|
+
} catch (ex) {
|
|
185
|
+
const axiosError = ex as AxiosError
|
|
186
|
+
if (axiosError.isAxiosError) {
|
|
187
|
+
//selectively pick fields from AxiosError
|
|
188
|
+
const result: ImageThumbnail = {
|
|
189
|
+
http: {
|
|
190
|
+
ipAddress: dnsResult[0],
|
|
191
|
+
status: axiosError.status,
|
|
192
|
+
},
|
|
193
|
+
schema: ImageThumbnailSchema,
|
|
194
|
+
sourceUrl: url,
|
|
195
|
+
}
|
|
196
|
+
return result
|
|
197
|
+
} else {
|
|
198
|
+
throw ex
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result: ImageThumbnail = {
|
|
203
|
+
http: {
|
|
204
|
+
status: response.status,
|
|
205
|
+
},
|
|
206
|
+
schema: ImageThumbnailSchema,
|
|
207
|
+
sourceUrl: url,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (response.status >= 200 && response.status < 300) {
|
|
211
|
+
const contentType = response.headers['content-type']?.toString()
|
|
212
|
+
if (contentType.split('/')[0] !== 'image') {
|
|
213
|
+
result.mime = result.mime ?? {}
|
|
214
|
+
result.mime.invalid = true
|
|
215
|
+
} else {
|
|
216
|
+
const sourceBuffer = Buffer.from(response.data, 'binary')
|
|
217
|
+
result.sourceHash = await ImageThumbnailWitness.binaryToSha256(sourceBuffer)
|
|
218
|
+
result.url = await this.createThumbnail(sourceBuffer)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return result
|
|
222
|
+
}
|
|
222
223
|
}
|