@stream44.studio/t44-blockchaincommons.com 0.1.0-rc.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/.dco-signatures +9 -0
- package/.github/workflows/dco.yml +12 -0
- package/.github/workflows/gordian-open-integrity.yml +13 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +25 -0
- package/DCO.md +34 -0
- package/README.md +210 -0
- package/action.yml +47 -0
- package/bin/oi +152 -0
- package/caps/GordianOpenIntegrity.test.ts +879 -0
- package/caps/GordianOpenIntegrity.ts +821 -0
- package/caps/XidDocumentLedger.test.ts +687 -0
- package/caps/XidDocumentLedger.ts +545 -0
- package/caps/__snapshots__/XidDocumentLedger.test.ts.snap +11 -0
- package/caps/__snapshots__/XidLedger.test.ts.snap +11 -0
- package/caps/lifehash.test.ts +302 -0
- package/caps/lifehash.ts +142 -0
- package/caps/open-integrity-js.test.ts +252 -0
- package/caps/open-integrity-js.ts +485 -0
- package/caps/open-integrity-sh.test.ts +188 -0
- package/caps/open-integrity-sh.ts +187 -0
- package/caps/open-integrity.test.ts +259 -0
- package/caps/provenance-mark-cli.test.ts +387 -0
- package/caps/provenance-mark-cli.ts +174 -0
- package/caps/provenance-mark.test.ts +233 -0
- package/caps/provenance-mark.ts +223 -0
- package/caps/xid.test.ts +828 -0
- package/caps/xid.ts +565 -0
- package/examples/01-XID-DocumentLedger/__snapshots__/main.test.ts.snap +10 -0
- package/examples/01-XID-DocumentLedger/main.test.ts +182 -0
- package/examples/02-XID-Rotate-InceptionKey/__snapshots__/main.test.ts.snap +53 -0
- package/examples/02-XID-Rotate-InceptionKey/main.test.ts +232 -0
- package/examples/03-GordianOpenIntegrity/main.test.ts +176 -0
- package/examples/04-GordianOpenIntegrityCli/main.test.ts +119 -0
- package/package.json +37 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env bun test
|
|
2
|
+
|
|
3
|
+
import * as bunTest from 'bun:test'
|
|
4
|
+
import { run } from 't44/workspace-rt'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import { rm, mkdir, writeFile } from 'fs/promises'
|
|
7
|
+
|
|
8
|
+
const WORK_DIR = join(import.meta.dir, '.~lifehash')
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
test: { describe, it, expect },
|
|
12
|
+
lifehash,
|
|
13
|
+
provenanceMark,
|
|
14
|
+
} = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
|
|
15
|
+
const spine = await encapsulate({
|
|
16
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
17
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
18
|
+
'#': {
|
|
19
|
+
test: {
|
|
20
|
+
type: CapsulePropertyTypes.Mapping,
|
|
21
|
+
value: 't44/caps/WorkspaceTest',
|
|
22
|
+
options: {
|
|
23
|
+
'#': {
|
|
24
|
+
bunTest,
|
|
25
|
+
env: {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
lifehash: {
|
|
30
|
+
type: CapsulePropertyTypes.Mapping,
|
|
31
|
+
value: './lifehash'
|
|
32
|
+
},
|
|
33
|
+
provenanceMark: {
|
|
34
|
+
type: CapsulePropertyTypes.Mapping,
|
|
35
|
+
value: './provenance-mark'
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, {
|
|
40
|
+
importMeta: import.meta,
|
|
41
|
+
importStack: makeImportStack(),
|
|
42
|
+
capsuleName: 't44/caps/providers/blockchaincommons.com/lifehash.test'
|
|
43
|
+
})
|
|
44
|
+
return { spine }
|
|
45
|
+
}, async ({ spine, apis }: any) => {
|
|
46
|
+
return apis[spine.capsuleSourceLineRef]
|
|
47
|
+
}, {
|
|
48
|
+
importMeta: import.meta
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
await rm(WORK_DIR, { recursive: true, force: true })
|
|
52
|
+
await mkdir(WORK_DIR, { recursive: true })
|
|
53
|
+
|
|
54
|
+
describe('LifeHash Capsule', function () {
|
|
55
|
+
|
|
56
|
+
// ──────────────────────────────────────────────────────────────────
|
|
57
|
+
// 1. Basic image generation
|
|
58
|
+
// ──────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe('1. makeFromUtf8', function () {
|
|
61
|
+
|
|
62
|
+
it('should generate an image from a string', async function () {
|
|
63
|
+
const image = await lifehash.makeFromUtf8({ input: 'hello world' })
|
|
64
|
+
expect(image.width).toBe(32)
|
|
65
|
+
expect(image.height).toBe(32)
|
|
66
|
+
expect(image.colors).toBeInstanceOf(Uint8Array)
|
|
67
|
+
expect(image.colors.length).toBe(32 * 32 * 3)
|
|
68
|
+
|
|
69
|
+
const ppm = await lifehash.toPPM({ image })
|
|
70
|
+
await writeFile(join(WORK_DIR, 'hello-world.ppm'), ppm)
|
|
71
|
+
const svg = await lifehash.toSVG({ image })
|
|
72
|
+
await writeFile(join(WORK_DIR, 'hello-world.svg'), svg)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should generate a larger image with moduleSize', async function () {
|
|
76
|
+
const image = await lifehash.makeFromUtf8({ input: 'hello world', moduleSize: 4 })
|
|
77
|
+
expect(image.width).toBe(128)
|
|
78
|
+
expect(image.height).toBe(128)
|
|
79
|
+
expect(image.colors.length).toBe(128 * 128 * 3)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should generate an image with alpha channel', async function () {
|
|
83
|
+
const image = await lifehash.makeFromUtf8({ input: 'hello world', hasAlpha: true })
|
|
84
|
+
expect(image.colors.length).toBe(32 * 32 * 4)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should produce deterministic output for the same input', async function () {
|
|
88
|
+
const image1 = await lifehash.makeFromUtf8({ input: 'deterministic' })
|
|
89
|
+
const image2 = await lifehash.makeFromUtf8({ input: 'deterministic' })
|
|
90
|
+
expect(image1.colors).toEqual(image2.colors)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should produce different output for different inputs', async function () {
|
|
94
|
+
const image1 = await lifehash.makeFromUtf8({ input: 'alpha' })
|
|
95
|
+
const image2 = await lifehash.makeFromUtf8({ input: 'beta' })
|
|
96
|
+
expect(image1.colors).not.toEqual(image2.colors)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// ──────────────────────────────────────────────────────────────────
|
|
101
|
+
// 2. Version variants
|
|
102
|
+
// ──────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
describe('2. Version variants', function () {
|
|
105
|
+
|
|
106
|
+
it('should generate version1 image', async function () {
|
|
107
|
+
const image = await lifehash.makeFromUtf8({ input: 'test', version: 'version1' })
|
|
108
|
+
expect(image.width).toBe(32)
|
|
109
|
+
expect(image.height).toBe(32)
|
|
110
|
+
await writeFile(join(WORK_DIR, 'version1.svg'), await lifehash.toSVG({ image }))
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should generate detailed image (32x32)', async function () {
|
|
114
|
+
const image = await lifehash.makeFromUtf8({ input: 'test', version: 'detailed' })
|
|
115
|
+
expect(image.width).toBe(64)
|
|
116
|
+
expect(image.height).toBe(64)
|
|
117
|
+
await writeFile(join(WORK_DIR, 'detailed.svg'), await lifehash.toSVG({ image }))
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should generate fiducial image (32x32)', async function () {
|
|
121
|
+
const image = await lifehash.makeFromUtf8({ input: 'test', version: 'fiducial' })
|
|
122
|
+
expect(image.width).toBe(32)
|
|
123
|
+
expect(image.height).toBe(32)
|
|
124
|
+
await writeFile(join(WORK_DIR, 'fiducial.svg'), await lifehash.toSVG({ image }))
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should generate grayscale fiducial image', async function () {
|
|
128
|
+
const image = await lifehash.makeFromUtf8({ input: 'test', version: 'grayscale_fiducial' })
|
|
129
|
+
expect(image.width).toBe(32)
|
|
130
|
+
expect(image.height).toBe(32)
|
|
131
|
+
await writeFile(join(WORK_DIR, 'grayscale-fiducial.svg'), await lifehash.toSVG({ image }))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should produce different images for different versions', async function () {
|
|
135
|
+
const v1 = await lifehash.makeFromUtf8({ input: 'same', version: 'version1' })
|
|
136
|
+
const v2 = await lifehash.makeFromUtf8({ input: 'same', version: 'version2' })
|
|
137
|
+
expect(v1.colors).not.toEqual(v2.colors)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// ──────────────────────────────────────────────────────────────────
|
|
142
|
+
// 3. makeFromData
|
|
143
|
+
// ──────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
describe('3. makeFromData', function () {
|
|
146
|
+
|
|
147
|
+
it('should generate an image from raw bytes', async function () {
|
|
148
|
+
const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])
|
|
149
|
+
const image = await lifehash.makeFromData({ data })
|
|
150
|
+
expect(image.width).toBe(32)
|
|
151
|
+
expect(image.height).toBe(32)
|
|
152
|
+
expect(image.colors.length).toBe(32 * 32 * 3)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// ──────────────────────────────────────────────────────────────────
|
|
157
|
+
// 4. makeFromDigest
|
|
158
|
+
// ──────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
describe('4. makeFromDigest', function () {
|
|
161
|
+
|
|
162
|
+
it('should generate an image from a 32-byte digest', async function () {
|
|
163
|
+
const { sha256 } = await lifehash.types()
|
|
164
|
+
const digest = sha256(new TextEncoder().encode('hello'))
|
|
165
|
+
expect(digest.length).toBe(32)
|
|
166
|
+
|
|
167
|
+
const image = await lifehash.makeFromDigest({ digest })
|
|
168
|
+
expect(image.width).toBe(32)
|
|
169
|
+
expect(image.height).toBe(32)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should reject a non-32-byte digest', async function () {
|
|
173
|
+
const badDigest = new Uint8Array(16)
|
|
174
|
+
await expect(lifehash.makeFromDigest({ digest: badDigest })).rejects.toThrow('32 bytes')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// ──────────────────────────────────────────────────────────────────
|
|
179
|
+
// 5. LifeHash from provenance mark
|
|
180
|
+
// ──────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
describe('5. LifeHash from provenance mark', function () {
|
|
183
|
+
|
|
184
|
+
let mark: any
|
|
185
|
+
let markIdentifier: string
|
|
186
|
+
|
|
187
|
+
it('should create a provenance mark', async function () {
|
|
188
|
+
const generator = await provenanceMark.createGenerator({
|
|
189
|
+
type: 'passphrase',
|
|
190
|
+
passphrase: 'lifehash-test-chain',
|
|
191
|
+
})
|
|
192
|
+
mark = await provenanceMark.nextMark({
|
|
193
|
+
generator,
|
|
194
|
+
date: new Date(Date.UTC(2025, 0, 1)),
|
|
195
|
+
})
|
|
196
|
+
expect(mark).toBeDefined()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should get mark identifier string', async function () {
|
|
200
|
+
markIdentifier = await provenanceMark.getIdentifier({ mark })
|
|
201
|
+
expect(typeof markIdentifier).toBe('string')
|
|
202
|
+
expect(markIdentifier.length).toBeGreaterThan(0)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should generate a lifehash image from the mark identifier', async function () {
|
|
206
|
+
const image = await lifehash.makeFromUtf8({ input: markIdentifier })
|
|
207
|
+
expect(image.width).toBe(32)
|
|
208
|
+
expect(image.height).toBe(32)
|
|
209
|
+
expect(image.colors).toBeInstanceOf(Uint8Array)
|
|
210
|
+
expect(image.colors.length).toBe(32 * 32 * 3)
|
|
211
|
+
|
|
212
|
+
await writeFile(join(WORK_DIR, 'provenance-mark-identifier.svg'), await lifehash.toSVG({ image }))
|
|
213
|
+
await writeFile(join(WORK_DIR, 'provenance-mark-identifier.ppm'), await lifehash.toPPM({ image }))
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should generate a lifehash from the mark hash bytes', async function () {
|
|
217
|
+
const hashBytes = await provenanceMark.getHash({ mark })
|
|
218
|
+
expect(hashBytes).toBeInstanceOf(Uint8Array)
|
|
219
|
+
|
|
220
|
+
const image = await lifehash.makeFromData({ data: hashBytes })
|
|
221
|
+
expect(image.width).toBe(32)
|
|
222
|
+
expect(image.height).toBe(32)
|
|
223
|
+
|
|
224
|
+
await writeFile(join(WORK_DIR, 'provenance-mark-hash.svg'), await lifehash.toSVG({ image }))
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('should produce a deterministic lifehash for the same mark', async function () {
|
|
228
|
+
const image1 = await lifehash.makeFromUtf8({ input: markIdentifier })
|
|
229
|
+
const image2 = await lifehash.makeFromUtf8({ input: markIdentifier })
|
|
230
|
+
expect(image1.colors).toEqual(image2.colors)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should produce different lifehashes for different marks', async function () {
|
|
234
|
+
const generator = await provenanceMark.createGenerator({
|
|
235
|
+
type: 'passphrase',
|
|
236
|
+
passphrase: 'different-chain',
|
|
237
|
+
})
|
|
238
|
+
const otherMark = await provenanceMark.nextMark({
|
|
239
|
+
generator,
|
|
240
|
+
date: new Date(Date.UTC(2025, 0, 1)),
|
|
241
|
+
})
|
|
242
|
+
const otherIdentifier = await provenanceMark.getIdentifier({ mark: otherMark })
|
|
243
|
+
|
|
244
|
+
const image1 = await lifehash.makeFromUtf8({ input: markIdentifier })
|
|
245
|
+
const image2 = await lifehash.makeFromUtf8({ input: otherIdentifier })
|
|
246
|
+
expect(image1.colors).not.toEqual(image2.colors)
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// ──────────────────────────────────────────────────────────────────
|
|
251
|
+
// 6. Output formats
|
|
252
|
+
// ──────────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
describe('6. Output formats', function () {
|
|
255
|
+
|
|
256
|
+
it('should convert image to PPM format', async function () {
|
|
257
|
+
const image = await lifehash.makeFromUtf8({ input: 'ppm-test' })
|
|
258
|
+
const ppm = await lifehash.toPPM({ image })
|
|
259
|
+
expect(ppm).toBeInstanceOf(Uint8Array)
|
|
260
|
+
|
|
261
|
+
const header = new TextDecoder().decode(ppm.slice(0, 20))
|
|
262
|
+
expect(header.startsWith('P6\n32 32\n255\n')).toBe(true)
|
|
263
|
+
|
|
264
|
+
const headerLength = new TextEncoder().encode('P6\n32 32\n255\n').length
|
|
265
|
+
expect(ppm.length).toBe(headerLength + 32 * 32 * 3)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('should convert image to SVG format', async function () {
|
|
269
|
+
const image = await lifehash.makeFromUtf8({ input: 'svg-test' })
|
|
270
|
+
const svg = await lifehash.toSVG({ image })
|
|
271
|
+
expect(typeof svg).toBe('string')
|
|
272
|
+
expect(svg).toContain('<svg')
|
|
273
|
+
expect(svg).toContain('viewBox="0 0 32 32"')
|
|
274
|
+
expect(svg).toContain('</svg>')
|
|
275
|
+
expect(svg).toContain('<rect')
|
|
276
|
+
expect(svg).toContain('fill="rgb(')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('should produce valid SVG for scaled images', async function () {
|
|
280
|
+
const image = await lifehash.makeFromUtf8({ input: 'svg-scaled', moduleSize: 2 })
|
|
281
|
+
const svg = await lifehash.toSVG({ image })
|
|
282
|
+
expect(svg).toContain('viewBox="0 0 64 64"')
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// ──────────────────────────────────────────────────────────────────
|
|
287
|
+
// 7. Types exposure
|
|
288
|
+
// ──────────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
describe('7. Types', function () {
|
|
291
|
+
|
|
292
|
+
it('should expose library types', async function () {
|
|
293
|
+
const types = await lifehash.types()
|
|
294
|
+
expect(types.Version).toBeDefined()
|
|
295
|
+
expect(types.Pattern).toBeDefined()
|
|
296
|
+
expect(types.sha256).toBeDefined()
|
|
297
|
+
expect(types.dataToHex).toBeDefined()
|
|
298
|
+
expect(types.hexToData).toBeDefined()
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
})
|
package/caps/lifehash.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
makeFromUtf8,
|
|
5
|
+
makeFromData,
|
|
6
|
+
makeFromDigest,
|
|
7
|
+
Version,
|
|
8
|
+
Pattern,
|
|
9
|
+
type Image,
|
|
10
|
+
dataToHex,
|
|
11
|
+
hexToData,
|
|
12
|
+
sha256,
|
|
13
|
+
Color,
|
|
14
|
+
Size,
|
|
15
|
+
} from '@bcts/lifehash'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export async function capsule({
|
|
19
|
+
encapsulate,
|
|
20
|
+
CapsulePropertyTypes,
|
|
21
|
+
makeImportStack
|
|
22
|
+
}: {
|
|
23
|
+
encapsulate: any
|
|
24
|
+
CapsulePropertyTypes: any
|
|
25
|
+
makeImportStack: any
|
|
26
|
+
}) {
|
|
27
|
+
|
|
28
|
+
return encapsulate({
|
|
29
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
30
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
31
|
+
'#': {
|
|
32
|
+
|
|
33
|
+
makeFromUtf8: {
|
|
34
|
+
type: CapsulePropertyTypes.Function,
|
|
35
|
+
value: async function (this: any, context: {
|
|
36
|
+
input: string
|
|
37
|
+
version?: 'version1' | 'version2' | 'detailed' | 'fiducial' | 'grayscale_fiducial'
|
|
38
|
+
moduleSize?: number
|
|
39
|
+
hasAlpha?: boolean
|
|
40
|
+
}): Promise<Image> {
|
|
41
|
+
const version = context.version ? Version[context.version] : Version.version2
|
|
42
|
+
return makeFromUtf8(context.input, version, context.moduleSize ?? 1, context.hasAlpha ?? false)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
makeFromData: {
|
|
47
|
+
type: CapsulePropertyTypes.Function,
|
|
48
|
+
value: async function (this: any, context: {
|
|
49
|
+
data: Uint8Array
|
|
50
|
+
version?: 'version1' | 'version2' | 'detailed' | 'fiducial' | 'grayscale_fiducial'
|
|
51
|
+
moduleSize?: number
|
|
52
|
+
hasAlpha?: boolean
|
|
53
|
+
}): Promise<Image> {
|
|
54
|
+
const version = context.version ? Version[context.version] : Version.version2
|
|
55
|
+
return makeFromData(context.data, version, context.moduleSize ?? 1, context.hasAlpha ?? false)
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
makeFromDigest: {
|
|
60
|
+
type: CapsulePropertyTypes.Function,
|
|
61
|
+
value: async function (this: any, context: {
|
|
62
|
+
digest: Uint8Array
|
|
63
|
+
version?: 'version1' | 'version2' | 'detailed' | 'fiducial' | 'grayscale_fiducial'
|
|
64
|
+
moduleSize?: number
|
|
65
|
+
hasAlpha?: boolean
|
|
66
|
+
}): Promise<Image> {
|
|
67
|
+
const version = context.version ? Version[context.version] : Version.version2
|
|
68
|
+
return makeFromDigest(context.digest, version, context.moduleSize ?? 1, context.hasAlpha ?? false)
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
toPPM: {
|
|
73
|
+
type: CapsulePropertyTypes.Function,
|
|
74
|
+
value: async function (this: any, context: {
|
|
75
|
+
image: Image
|
|
76
|
+
}): Promise<Uint8Array> {
|
|
77
|
+
const { width, height, colors } = context.image
|
|
78
|
+
const header = `P6\n${width} ${height}\n255\n`
|
|
79
|
+
const headerBytes = new TextEncoder().encode(header)
|
|
80
|
+
const pixelCount = width * height
|
|
81
|
+
const pixelData = new Uint8Array(pixelCount * 3)
|
|
82
|
+
const channels = colors.length / (width * height)
|
|
83
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
84
|
+
pixelData[i * 3] = colors[i * channels]
|
|
85
|
+
pixelData[i * 3 + 1] = colors[i * channels + 1]
|
|
86
|
+
pixelData[i * 3 + 2] = colors[i * channels + 2]
|
|
87
|
+
}
|
|
88
|
+
const result = new Uint8Array(headerBytes.length + pixelData.length)
|
|
89
|
+
result.set(headerBytes)
|
|
90
|
+
result.set(pixelData, headerBytes.length)
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
toSVG: {
|
|
96
|
+
type: CapsulePropertyTypes.Function,
|
|
97
|
+
value: async function (this: any, context: {
|
|
98
|
+
image: Image
|
|
99
|
+
}): Promise<string> {
|
|
100
|
+
const { width, height, colors } = context.image
|
|
101
|
+
const channels = colors.length / (width * height)
|
|
102
|
+
const lines: string[] = []
|
|
103
|
+
lines.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" shape-rendering="crispEdges">`)
|
|
104
|
+
for (let y = 0; y < height; y++) {
|
|
105
|
+
for (let x = 0; x < width; x++) {
|
|
106
|
+
const i = (y * width + x) * channels
|
|
107
|
+
const r = colors[i]
|
|
108
|
+
const g = colors[i + 1]
|
|
109
|
+
const b = colors[i + 2]
|
|
110
|
+
lines.push(`<rect x="${x}" y="${y}" width="1" height="1" fill="rgb(${r},${g},${b})"/>`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
lines.push('</svg>')
|
|
114
|
+
return lines.join('\n')
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// Expose library types and utilities for convenience
|
|
119
|
+
types: {
|
|
120
|
+
type: CapsulePropertyTypes.Function,
|
|
121
|
+
value: async function (this: any) {
|
|
122
|
+
return {
|
|
123
|
+
Version,
|
|
124
|
+
Pattern,
|
|
125
|
+
Color,
|
|
126
|
+
Size,
|
|
127
|
+
dataToHex,
|
|
128
|
+
hexToData,
|
|
129
|
+
sha256,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}, {
|
|
137
|
+
importMeta: import.meta,
|
|
138
|
+
importStack: makeImportStack(),
|
|
139
|
+
capsuleName: capsule['#'],
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
capsule['#'] = 't44/caps/providers/blockchaincommons.com/lifehash'
|