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