@hellkite/pipkin 0.7.0 → 0.8.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/build/src/lib/bundler.js.map +1 -1
- package/build/src/lib/index.d.ts +0 -1
- package/build/src/lib/index.js +0 -1
- package/build/src/lib/index.js.map +1 -1
- package/build/src/lib/replacement.d.ts +5 -3
- package/build/src/lib/replacement.js +6 -3
- package/build/src/lib/replacement.js.map +1 -1
- package/build/src/lib/template.d.ts +25 -20
- package/build/src/lib/template.js +176 -45
- package/build/src/lib/template.js.map +1 -1
- package/build/src/lib/types/containers.js +2 -7
- package/build/src/lib/types/containers.js.map +1 -1
- package/build/src/lib/types/hypernode.d.ts +1 -1
- package/build/src/lib/types/image.d.ts +6 -5
- package/build/src/lib/types/image.js +2 -7
- package/build/src/lib/types/image.js.map +1 -1
- package/build/src/lib/types/index.d.ts +2 -0
- package/build/src/lib/types/index.js +2 -0
- package/build/src/lib/types/index.js.map +1 -1
- package/build/src/lib/types/layer.d.ts +10 -1
- package/build/src/lib/types/layer.js +7 -0
- package/build/src/lib/types/layer.js.map +1 -1
- package/build/src/lib/types/pdf.d.ts +1 -0
- package/build/src/lib/types/pdf.js +3 -0
- package/build/src/lib/types/pdf.js.map +1 -0
- package/build/src/lib/types/replacement.d.ts +2 -2
- package/build/src/lib/types/template.d.ts +13 -0
- package/build/src/lib/types/template.js +10 -0
- package/build/src/lib/types/template.js.map +1 -0
- package/build/src/lib/types/text.d.ts +2 -2
- package/build/src/lib/types/text.js +3 -10
- package/build/src/lib/types/text.js.map +1 -1
- package/build/src/lib/utils/container.js.map +1 -1
- package/build/src/lib/utils/htmlToImage.js +0 -1
- package/build/src/lib/utils/htmlToImage.js.map +1 -1
- package/build/src/lib/utils/imagesToPdf.d.ts +2 -0
- package/build/src/lib/utils/imagesToPdf.js +31 -0
- package/build/src/lib/utils/imagesToPdf.js.map +1 -0
- package/build/src/lib/utils/index.d.ts +1 -2
- package/build/src/lib/utils/index.js +1 -2
- package/build/src/lib/utils/index.js.map +1 -1
- package/build/src/lib/utils/placeBoundingBox.js +11 -3
- package/build/src/lib/utils/placeBoundingBox.js.map +1 -1
- package/build/src/lib/utils/reduceBoundingBox.d.ts +2 -0
- package/build/src/lib/utils/reduceBoundingBox.js +16 -0
- package/build/src/lib/utils/reduceBoundingBox.js.map +1 -0
- package/build/src/test.js +115 -27
- package/build/src/test.js.map +1 -1
- package/package.json +2 -1
- package/roadmap.md +1 -0
- package/src/lib/bundler.ts +1 -1
- package/src/lib/index.ts +0 -1
- package/src/lib/template.ts +327 -84
- package/src/lib/types/containers.ts +3 -5
- package/src/lib/types/hypernode.ts +1 -2
- package/src/lib/types/image.ts +15 -10
- package/src/lib/types/index.ts +2 -0
- package/src/lib/types/layer.ts +24 -1
- package/src/lib/types/pdf.ts +1 -0
- package/src/lib/types/render.ts +2 -4
- package/src/lib/types/replacement.ts +2 -2
- package/src/lib/types/template.ts +22 -0
- package/src/lib/types/text.ts +6 -7
- package/src/lib/utils/container.ts +0 -1
- package/src/lib/utils/htmlToImage.ts +0 -1
- package/src/lib/utils/imagesToPdf.ts +24 -0
- package/src/lib/utils/index.ts +1 -2
- package/src/lib/utils/placeBoundingBox.ts +33 -13
- package/src/lib/utils/reduceBoundingBox.ts +21 -0
- package/src/lib/replacement.ts +0 -21
- package/src/lib/utils/placeImage.ts +0 -69
- package/src/lib/utils/placeText.ts +0 -110
package/src/lib/template.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import { parse as parseCsv } from 'papaparse';
|
|
3
|
-
import { Jimp
|
|
3
|
+
import { Jimp } from 'jimp';
|
|
4
4
|
import camelCase from 'lodash.camelcase';
|
|
5
5
|
import concat from 'lodash.concat';
|
|
6
6
|
import path from 'path';
|
|
@@ -9,11 +9,9 @@ import {
|
|
|
9
9
|
gridPackingFn,
|
|
10
10
|
hboxPackingFn,
|
|
11
11
|
htmlToImage,
|
|
12
|
-
placeImage,
|
|
13
|
-
placeText,
|
|
14
12
|
vboxPackingFn,
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
boundingBoxToPx,
|
|
14
|
+
toPx,
|
|
17
15
|
} from './utils';
|
|
18
16
|
import {
|
|
19
17
|
DirectionContainerOptions,
|
|
@@ -37,52 +35,41 @@ import {
|
|
|
37
35
|
ElementsFn,
|
|
38
36
|
ElementRef,
|
|
39
37
|
ImageLayerSpecificOptions,
|
|
38
|
+
TextLayerSpecificOptions,
|
|
39
|
+
SCALE_MODE_TO_OBJECT_FIT,
|
|
40
|
+
StaticImageRef,
|
|
41
|
+
TemplateOptions,
|
|
42
|
+
DEFAULT_TEMPLATE_OPTIONS,
|
|
43
|
+
LayerFn,
|
|
44
|
+
ReplacementMap,
|
|
40
45
|
} from './types';
|
|
41
46
|
import merge from 'lodash.merge';
|
|
42
47
|
import { RequiredDeep } from 'type-fest';
|
|
43
48
|
import { h } from 'virtual-dom';
|
|
44
49
|
import flatten from 'lodash.flatten';
|
|
45
|
-
|
|
46
|
-
type RequiredTemplateOptions = {
|
|
47
|
-
height: number;
|
|
48
|
-
width: number;
|
|
49
|
-
color: number;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
type OptionalTemplateOptions = Partial<{
|
|
53
|
-
defaultFontFamily: string;
|
|
54
|
-
defaultAssetsPath: string;
|
|
55
|
-
}>;
|
|
56
|
-
|
|
57
|
-
export type TemplateOptions = RequiredTemplateOptions & OptionalTemplateOptions;
|
|
58
|
-
|
|
59
|
-
const DEFAULT_TEMPLATE_OPTIONS: RequiredTemplateOptions = {
|
|
60
|
-
height: 1050,
|
|
61
|
-
width: 750,
|
|
62
|
-
color: rgbaToInt(255, 255, 255, 255),
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export type LayerFnContext = {
|
|
66
|
-
debugMode: boolean;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export type LayerFn<EntryType> = (
|
|
70
|
-
entry: EntryType,
|
|
71
|
-
context: LayerFnContext,
|
|
72
|
-
) => Promise<Array<HyperNode>>;
|
|
50
|
+
import { Bundler } from './bundler';
|
|
73
51
|
|
|
74
52
|
export type TemplateLayerFn<EntryType extends Record<string, string>> = (
|
|
75
53
|
template: Template<EntryType>,
|
|
76
54
|
) => Template<EntryType>;
|
|
77
55
|
|
|
78
56
|
export class Template<EntryType extends Record<string, string>> {
|
|
79
|
-
|
|
80
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Template data
|
|
59
|
+
*/
|
|
81
60
|
private readonly background: ImageType;
|
|
82
|
-
private
|
|
61
|
+
private readonly layers: LayerFn<EntryType>[] = [];
|
|
62
|
+
private readonly fonts: Record<string, string> = {};
|
|
63
|
+
private renderBoundingBox?: boolean;
|
|
83
64
|
private defaultFontFamily?: string;
|
|
84
65
|
private defaultAssetsPath?: string;
|
|
85
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Render data
|
|
69
|
+
*/
|
|
70
|
+
private readonly debugPoints: Array<DebugPoint> = [];
|
|
71
|
+
private bundler?: Bundler;
|
|
72
|
+
|
|
86
73
|
// disallow constructor initialization
|
|
87
74
|
private constructor(options: TemplateOptions) {
|
|
88
75
|
this.background = new Jimp({
|
|
@@ -127,7 +114,7 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
127
114
|
const template = fn(this.shadowTemplate());
|
|
128
115
|
const image = await template.render(entry);
|
|
129
116
|
return [
|
|
130
|
-
await placeImage({
|
|
117
|
+
await this.placeImage({
|
|
131
118
|
image,
|
|
132
119
|
box: {
|
|
133
120
|
left: 0,
|
|
@@ -145,19 +132,28 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
145
132
|
packingFn: PackingFn,
|
|
146
133
|
options?: ContainerOptions<EntryType>,
|
|
147
134
|
): this =>
|
|
148
|
-
this.layer(async (entry: EntryType
|
|
135
|
+
this.layer(async (entry: EntryType) => {
|
|
149
136
|
const mergedOptions: RequiredDeep<ContainerOptions<EntryType>> =
|
|
150
|
-
merge(
|
|
137
|
+
merge(
|
|
138
|
+
{},
|
|
139
|
+
DEFAULT_CONTAINER_OPTIONS,
|
|
140
|
+
{ renderBoundingBox: this.renderBoundingBox },
|
|
141
|
+
options,
|
|
142
|
+
);
|
|
151
143
|
if (this.shouldSkipLayerForEntry(entry, mergedOptions)) {
|
|
152
144
|
return [];
|
|
153
145
|
}
|
|
154
146
|
|
|
155
147
|
const elementRefs = await elementsFn(entry);
|
|
156
|
-
const elements = await Promise.all(
|
|
148
|
+
const elements = await Promise.all(
|
|
149
|
+
elementRefs.map(elementRef =>
|
|
150
|
+
this.elementFromElementRef(entry, elementRef),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
157
153
|
const result = await packingFn(box, elements);
|
|
158
154
|
|
|
159
155
|
// debug mode
|
|
160
|
-
if (
|
|
156
|
+
if (mergedOptions.renderBoundingBox) {
|
|
161
157
|
const debugImage = await placeBoundingBox(box);
|
|
162
158
|
return [result, debugImage];
|
|
163
159
|
}
|
|
@@ -188,12 +184,15 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
188
184
|
box: BoundingBox,
|
|
189
185
|
options: ImageLayerOptions<EntryType>,
|
|
190
186
|
): this =>
|
|
191
|
-
this.layer(async
|
|
187
|
+
this.layer(async entry => {
|
|
192
188
|
const mergedOptions: RequiredDeep<ImageLayerOptions<EntryType>> =
|
|
193
189
|
merge(
|
|
194
190
|
{},
|
|
195
191
|
DEFAULT_IMAGE_LAYER_OPTIONS,
|
|
196
|
-
{
|
|
192
|
+
{
|
|
193
|
+
assetsPath: this.defaultAssetsPath,
|
|
194
|
+
renderBoundingBox: this.renderBoundingBox,
|
|
195
|
+
},
|
|
197
196
|
options,
|
|
198
197
|
);
|
|
199
198
|
if (this.shouldSkipLayerForEntry(entry, mergedOptions)) {
|
|
@@ -203,16 +202,16 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
203
202
|
const image = await this.imageFromImageRef(
|
|
204
203
|
entry,
|
|
205
204
|
ref,
|
|
206
|
-
mergedOptions,
|
|
205
|
+
mergedOptions.assetsPath,
|
|
207
206
|
);
|
|
208
|
-
const result = await placeImage({
|
|
207
|
+
const result = await this.placeImage({
|
|
209
208
|
image,
|
|
210
209
|
box,
|
|
211
210
|
options: mergedOptions,
|
|
212
211
|
});
|
|
213
212
|
|
|
214
213
|
// debug mode
|
|
215
|
-
if (
|
|
214
|
+
if (mergedOptions.renderBoundingBox) {
|
|
216
215
|
const debugImage = await placeBoundingBox(box);
|
|
217
216
|
return [result, debugImage];
|
|
218
217
|
}
|
|
@@ -230,7 +229,7 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
230
229
|
box: BoundingBox,
|
|
231
230
|
options?: TextLayerOptions<EntryType>,
|
|
232
231
|
): this =>
|
|
233
|
-
this.layer(async
|
|
232
|
+
this.layer(async entry => {
|
|
234
233
|
const mergedOptions = merge(
|
|
235
234
|
{},
|
|
236
235
|
DEFAULT_TEXT_LAYER_OPTIONS,
|
|
@@ -238,6 +237,7 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
238
237
|
font: {
|
|
239
238
|
family: this.defaultFontFamily,
|
|
240
239
|
},
|
|
240
|
+
renderBoundingBox: this.renderBoundingBox,
|
|
241
241
|
} as FontOptions,
|
|
242
242
|
options,
|
|
243
243
|
);
|
|
@@ -246,14 +246,14 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
const text = this.textFromTextRef(entry, ref);
|
|
249
|
-
const result = await placeText({
|
|
249
|
+
const result = await this.placeText({
|
|
250
250
|
text,
|
|
251
251
|
box,
|
|
252
252
|
options: mergedOptions,
|
|
253
253
|
});
|
|
254
254
|
|
|
255
255
|
// debug mode
|
|
256
|
-
if (
|
|
256
|
+
if (mergedOptions.renderBoundingBox) {
|
|
257
257
|
const debugImage = await placeBoundingBox(box);
|
|
258
258
|
return [result, debugImage];
|
|
259
259
|
}
|
|
@@ -267,41 +267,77 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
267
267
|
return this;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
debug(): this {
|
|
271
|
-
this.
|
|
270
|
+
debug = (): this => {
|
|
271
|
+
this.debugPoints.push({
|
|
272
|
+
index: this.layers.length,
|
|
273
|
+
});
|
|
272
274
|
return this;
|
|
273
|
-
}
|
|
275
|
+
};
|
|
274
276
|
|
|
275
277
|
async render(entry: EntryType): Promise<ImageType> {
|
|
276
278
|
const results = await Promise.all(
|
|
277
279
|
this.layers.map(layerFn =>
|
|
278
280
|
layerFn(entry, {
|
|
279
|
-
|
|
281
|
+
renderBoundingBox: this.renderBoundingBox,
|
|
280
282
|
}),
|
|
281
283
|
),
|
|
282
284
|
);
|
|
283
|
-
const
|
|
284
|
-
h('
|
|
285
|
-
h(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
const buildDocument = (children: Array<HyperNode>) =>
|
|
286
|
+
h('html', [
|
|
287
|
+
h('head', [
|
|
288
|
+
h(
|
|
289
|
+
'style',
|
|
290
|
+
// load fonts
|
|
291
|
+
Object.entries(this.fonts)
|
|
292
|
+
.map(
|
|
293
|
+
([name, data]) =>
|
|
294
|
+
`@font-face {
|
|
291
295
|
font-family: '${name}';
|
|
292
296
|
src: url(data:font/ttf;base64,${data}) format('truetype');
|
|
293
297
|
}`,
|
|
294
|
-
|
|
295
|
-
|
|
298
|
+
)
|
|
299
|
+
.join('\n'),
|
|
300
|
+
),
|
|
301
|
+
]),
|
|
302
|
+
// fix chromium headless padding issue
|
|
303
|
+
h(
|
|
304
|
+
'body',
|
|
305
|
+
{
|
|
306
|
+
style: {
|
|
307
|
+
margin: 0,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
[
|
|
311
|
+
h(
|
|
312
|
+
'div',
|
|
313
|
+
{
|
|
314
|
+
style: {
|
|
315
|
+
marginLeft: '-8px',
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
children,
|
|
319
|
+
),
|
|
320
|
+
],
|
|
296
321
|
),
|
|
297
|
-
])
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
// TODO: move it to a proper place
|
|
325
|
+
for (const debugPoint of this.debugPoints) {
|
|
326
|
+
const debugRender = await htmlToImage(
|
|
327
|
+
buildDocument(flatten(results.slice(0, debugPoint.index))),
|
|
328
|
+
this.backgroundSize,
|
|
329
|
+
);
|
|
330
|
+
const debugImage: ImageType = await this.background
|
|
331
|
+
.clone()
|
|
332
|
+
.composite(debugRender);
|
|
333
|
+
await debugImage.write('assets/debug-1.png');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const render = await htmlToImage(
|
|
337
|
+
buildDocument(flatten(results)),
|
|
338
|
+
this.backgroundSize,
|
|
339
|
+
);
|
|
340
|
+
return this.background.clone().composite(render);
|
|
305
341
|
}
|
|
306
342
|
|
|
307
343
|
async renderAll(
|
|
@@ -363,11 +399,31 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
363
399
|
private imageFromImageRef = async (
|
|
364
400
|
entry: EntryType,
|
|
365
401
|
ref: ImageRef<EntryType>,
|
|
366
|
-
|
|
402
|
+
assetsPath: string,
|
|
367
403
|
): Promise<ImageType> => {
|
|
368
404
|
const pathSegments = [];
|
|
369
|
-
if (
|
|
370
|
-
pathSegments.push(
|
|
405
|
+
if (assetsPath.length) {
|
|
406
|
+
pathSegments.push(assetsPath);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if ('key' in ref) {
|
|
410
|
+
const fileName = entry[ref.key];
|
|
411
|
+
return this.loadImage(path.join(...pathSegments, fileName));
|
|
412
|
+
} else if ('pathFn' in ref) {
|
|
413
|
+
const fileName = ref.pathFn(entry);
|
|
414
|
+
return this.loadImage(path.join(...pathSegments, fileName));
|
|
415
|
+
} else {
|
|
416
|
+
return this.imageFromStaticImageRef(ref, assetsPath);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
private imageFromStaticImageRef = async (
|
|
421
|
+
ref: StaticImageRef,
|
|
422
|
+
assetsPath: string,
|
|
423
|
+
): Promise<ImageType> => {
|
|
424
|
+
const pathSegments = [];
|
|
425
|
+
if (assetsPath.length) {
|
|
426
|
+
pathSegments.push(assetsPath);
|
|
371
427
|
}
|
|
372
428
|
|
|
373
429
|
if ('buffer' in ref) {
|
|
@@ -376,12 +432,6 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
376
432
|
return this.loadImage(path.join(...pathSegments, ref.path));
|
|
377
433
|
} else if ('absolutePath' in ref) {
|
|
378
434
|
return this.loadImage(ref.absolutePath);
|
|
379
|
-
} else if ('key' in ref) {
|
|
380
|
-
const fileName = entry[ref.key];
|
|
381
|
-
return this.loadImage(path.join(...pathSegments, fileName));
|
|
382
|
-
} else if ('pathFn' in ref) {
|
|
383
|
-
const fileName = ref.pathFn(entry);
|
|
384
|
-
return this.loadImage(path.join(...pathSegments, fileName));
|
|
385
435
|
} else {
|
|
386
436
|
throw new Error('Unknown ImageRef variant');
|
|
387
437
|
}
|
|
@@ -408,18 +458,22 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
408
458
|
): Promise<HyperNode> => {
|
|
409
459
|
if ('text' in ref) {
|
|
410
460
|
const options = merge({}, DEFAULT_TEXT_LAYER_OPTIONS, ref.options);
|
|
411
|
-
return prepareText({
|
|
461
|
+
return this.prepareText({
|
|
412
462
|
text: this.textFromTextRef(entry, ref.text),
|
|
413
463
|
options,
|
|
414
464
|
});
|
|
415
465
|
} else if ('image' in ref) {
|
|
416
466
|
const options = merge({}, DEFAULT_IMAGE_LAYER_OPTIONS, ref.options);
|
|
417
|
-
return prepareImage({
|
|
418
|
-
image: await this.imageFromImageRef(
|
|
467
|
+
return this.prepareImage({
|
|
468
|
+
image: await this.imageFromImageRef(
|
|
469
|
+
entry,
|
|
470
|
+
ref.image,
|
|
471
|
+
options.assetsPath,
|
|
472
|
+
),
|
|
419
473
|
options,
|
|
420
474
|
});
|
|
421
475
|
} else if ('node' in ref) {
|
|
422
|
-
return ref.node
|
|
476
|
+
return ref.node;
|
|
423
477
|
} else {
|
|
424
478
|
throw new Error('Unknown TextRef variant');
|
|
425
479
|
}
|
|
@@ -434,4 +488,193 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
434
488
|
}
|
|
435
489
|
return options.skip ?? false;
|
|
436
490
|
};
|
|
491
|
+
|
|
492
|
+
private placeText = async ({
|
|
493
|
+
text,
|
|
494
|
+
box,
|
|
495
|
+
options,
|
|
496
|
+
}: {
|
|
497
|
+
text: string;
|
|
498
|
+
box: BoundingBox;
|
|
499
|
+
options: RequiredDeep<TextLayerOptions<EntryType>>;
|
|
500
|
+
}): Promise<HyperNode> => {
|
|
501
|
+
return h(
|
|
502
|
+
'div',
|
|
503
|
+
{
|
|
504
|
+
style: {
|
|
505
|
+
display: 'flex',
|
|
506
|
+
overflow: 'visible',
|
|
507
|
+
position: 'absolute',
|
|
508
|
+
|
|
509
|
+
justifyContent: options.justifyContent,
|
|
510
|
+
alignItems: options.alignItems,
|
|
511
|
+
|
|
512
|
+
...boundingBoxToPx(box),
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
[
|
|
516
|
+
await this.prepareText({
|
|
517
|
+
text,
|
|
518
|
+
options,
|
|
519
|
+
}),
|
|
520
|
+
],
|
|
521
|
+
);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
private prepareText = async ({
|
|
525
|
+
text,
|
|
526
|
+
options,
|
|
527
|
+
}: {
|
|
528
|
+
text: string;
|
|
529
|
+
options: RequiredDeep<TextLayerSpecificOptions<EntryType>>;
|
|
530
|
+
}): Promise<HyperNode> => {
|
|
531
|
+
let textChildren: Array<string | HyperNode> = [text];
|
|
532
|
+
const replacementBuilder = new ReplacementBuilder();
|
|
533
|
+
options.replacementFn(replacementBuilder);
|
|
534
|
+
const replacementMap = replacementBuilder.build();
|
|
535
|
+
for (const [word, imageRef] of Object.entries(replacementMap)) {
|
|
536
|
+
const regex = new RegExp(word, 'gi');
|
|
537
|
+
const textOptions: RequiredDeep<ImageLayerOptions<EntryType>> =
|
|
538
|
+
merge(
|
|
539
|
+
{},
|
|
540
|
+
DEFAULT_IMAGE_LAYER_OPTIONS,
|
|
541
|
+
{ assetsPath: this.defaultAssetsPath },
|
|
542
|
+
options,
|
|
543
|
+
);
|
|
544
|
+
const image = await this.imageFromStaticImageRef(
|
|
545
|
+
imageRef,
|
|
546
|
+
textOptions.assetsPath,
|
|
547
|
+
);
|
|
548
|
+
const imageBase64 = await image.getBase64('image/png');
|
|
549
|
+
|
|
550
|
+
const tmpChildren: Array<string | HyperNode> = [];
|
|
551
|
+
for (const textSegment of textChildren) {
|
|
552
|
+
if (typeof textSegment !== 'string') {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const parts = (textSegment as string).split(regex);
|
|
557
|
+
for (let index = 0; index < parts.length; index++) {
|
|
558
|
+
if (index > 0) {
|
|
559
|
+
tmpChildren.push(
|
|
560
|
+
h(
|
|
561
|
+
'img',
|
|
562
|
+
{
|
|
563
|
+
style: {
|
|
564
|
+
display: 'inline',
|
|
565
|
+
verticalAlign: 'middle',
|
|
566
|
+
height: toPx(options.font.size),
|
|
567
|
+
width: 'auto',
|
|
568
|
+
},
|
|
569
|
+
src: imageBase64,
|
|
570
|
+
},
|
|
571
|
+
[],
|
|
572
|
+
),
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
tmpChildren.push(parts[index]);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
textChildren = tmpChildren;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return h(
|
|
583
|
+
'div',
|
|
584
|
+
{
|
|
585
|
+
style: {
|
|
586
|
+
overflow: 'visible',
|
|
587
|
+
overflowWrap: 'word-wrap',
|
|
588
|
+
whiteSpace: 'normal',
|
|
589
|
+
|
|
590
|
+
color: options.color,
|
|
591
|
+
fontFamily: options.font.family,
|
|
592
|
+
fontSize: options.font.size,
|
|
593
|
+
fontStyle: options.font.italic ? 'italic' : undefined,
|
|
594
|
+
fontWeight: options.font.bold ? 'bold' : undefined,
|
|
595
|
+
|
|
596
|
+
'-webkit-text-stroke': `${options.border.width}px ${options.border.color}`,
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
textChildren,
|
|
600
|
+
);
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
private placeImage = async ({
|
|
604
|
+
image,
|
|
605
|
+
box,
|
|
606
|
+
options,
|
|
607
|
+
}: {
|
|
608
|
+
image: ImageType;
|
|
609
|
+
box: BoundingBox;
|
|
610
|
+
options: RequiredDeep<ImageLayerOptions<EntryType>>;
|
|
611
|
+
}): Promise<HyperNode> => {
|
|
612
|
+
return h(
|
|
613
|
+
'div',
|
|
614
|
+
{
|
|
615
|
+
style: {
|
|
616
|
+
display: 'flex',
|
|
617
|
+
position: 'absolute',
|
|
618
|
+
scale: 1,
|
|
619
|
+
|
|
620
|
+
justifyContent: options.justifyContent,
|
|
621
|
+
alignItems: options.alignItems,
|
|
622
|
+
|
|
623
|
+
...boundingBoxToPx(box),
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
[await this.prepareImage({ image, options })],
|
|
627
|
+
);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
private prepareImage = async ({
|
|
631
|
+
image,
|
|
632
|
+
options,
|
|
633
|
+
}: {
|
|
634
|
+
image: ImageType;
|
|
635
|
+
options: RequiredDeep<ImageLayerSpecificOptions<EntryType>>;
|
|
636
|
+
}): Promise<HyperNode> => {
|
|
637
|
+
const imageBase64 = await image.getBase64('image/png');
|
|
638
|
+
const objectFit = SCALE_MODE_TO_OBJECT_FIT[options.scale];
|
|
639
|
+
|
|
640
|
+
return h(
|
|
641
|
+
'img',
|
|
642
|
+
{
|
|
643
|
+
style: {
|
|
644
|
+
objectFit,
|
|
645
|
+
flex: '1 1 auto',
|
|
646
|
+
minWidth: 0,
|
|
647
|
+
minHeight: 0,
|
|
648
|
+
maxWidth: '100%',
|
|
649
|
+
maxHeight: '100%',
|
|
650
|
+
},
|
|
651
|
+
src: imageBase64,
|
|
652
|
+
},
|
|
653
|
+
[],
|
|
654
|
+
);
|
|
655
|
+
};
|
|
437
656
|
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Represents a replacement map between sets of words and symbols
|
|
660
|
+
*/
|
|
661
|
+
export class Replacement {
|
|
662
|
+
protected readonly replacementMap: ReplacementMap = {};
|
|
663
|
+
|
|
664
|
+
replace(words: Array<string>, symbol: StaticImageRef): this {
|
|
665
|
+
words.forEach(word => {
|
|
666
|
+
this.replacementMap[word] = symbol;
|
|
667
|
+
});
|
|
668
|
+
return this;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
class ReplacementBuilder extends Replacement {
|
|
673
|
+
build(): ReplacementMap {
|
|
674
|
+
return this.replacementMap;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
type DebugPoint = {
|
|
679
|
+
index: number;
|
|
680
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { RequiredDeep } from 'type-fest';
|
|
2
2
|
import { BoundingBox } from './boundingBox';
|
|
3
|
-
import { ImageLayerSpecificOptions, ImageRef
|
|
3
|
+
import { ImageLayerSpecificOptions, ImageRef } from './image';
|
|
4
4
|
import { ScaleMode } from './scale';
|
|
5
|
-
import { LayerOptions } from './layer';
|
|
5
|
+
import { DEFAULT_LAYER_OPTIONS, LayerOptions } from './layer';
|
|
6
6
|
import { HyperNode } from './hypernode';
|
|
7
7
|
import { TextLayerSpecificOptions, TextRef } from './text';
|
|
8
8
|
|
|
@@ -25,11 +25,9 @@ export type ContainerOptions<EntryType extends Record<string, string>> =
|
|
|
25
25
|
export const DEFAULT_CONTAINER_OPTIONS: RequiredDeep<
|
|
26
26
|
ContainerOptions<Record<string, string>>
|
|
27
27
|
> = {
|
|
28
|
+
...DEFAULT_LAYER_OPTIONS,
|
|
28
29
|
gap: 0,
|
|
29
|
-
justifyContent: 'normal',
|
|
30
|
-
alignItems: 'center',
|
|
31
30
|
scale: 'none',
|
|
32
|
-
skip: false,
|
|
33
31
|
};
|
|
34
32
|
|
|
35
33
|
export type DirectionContainerOptions<
|
package/src/lib/types/image.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JimpInstance } from 'jimp';
|
|
2
2
|
import { ScaleMode } from './scale';
|
|
3
|
-
import { LayerOptions } from './layer';
|
|
3
|
+
import { DEFAULT_LAYER_OPTIONS, LayerOptions } from './layer';
|
|
4
4
|
import { RequiredDeep } from 'type-fest';
|
|
5
5
|
|
|
6
6
|
export type ImageType = JimpInstance;
|
|
@@ -10,12 +10,15 @@ export type ImageType = JimpInstance;
|
|
|
10
10
|
* Dynamic images -> `key`, `pathFn`
|
|
11
11
|
*/
|
|
12
12
|
export type ImageRef<EntryType extends Record<string, string>> =
|
|
13
|
-
|
|
|
14
|
-
| { path: string }
|
|
15
|
-
| { absolutePath: string }
|
|
13
|
+
| StaticImageRef
|
|
16
14
|
| { key: keyof EntryType }
|
|
17
15
|
| { pathFn: (entry: EntryType) => string };
|
|
18
16
|
|
|
17
|
+
export type StaticImageRef =
|
|
18
|
+
| { buffer: Buffer }
|
|
19
|
+
| { path: string }
|
|
20
|
+
| { absolutePath: string };
|
|
21
|
+
|
|
19
22
|
export type ImageLayerOptions<EntryType extends Record<string, string>> =
|
|
20
23
|
LayerOptions<EntryType> & {
|
|
21
24
|
/**
|
|
@@ -32,15 +35,17 @@ export type ImageLayerOptions<EntryType extends Record<string, string>> =
|
|
|
32
35
|
// processorFn?: (entry: EntryType, image: ImageType) => Promise<ImageType>;
|
|
33
36
|
};
|
|
34
37
|
|
|
35
|
-
export const DEFAULT_IMAGE_LAYER_OPTIONS: RequiredDeep<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
export const DEFAULT_IMAGE_LAYER_OPTIONS: RequiredDeep<
|
|
39
|
+
ImageLayerOptions<Record<string, string>>
|
|
40
|
+
> = {
|
|
41
|
+
...DEFAULT_LAYER_OPTIONS,
|
|
38
42
|
scale: 'none',
|
|
39
|
-
|
|
40
|
-
assetsPath: ''
|
|
43
|
+
assetsPath: '',
|
|
41
44
|
};
|
|
42
45
|
|
|
43
|
-
export type ImageLayerSpecificOptions<
|
|
46
|
+
export type ImageLayerSpecificOptions<
|
|
47
|
+
EntryType extends Record<string, string>,
|
|
48
|
+
> = Omit<ImageLayerOptions<EntryType>, keyof LayerOptions<EntryType>>;
|
|
44
49
|
|
|
45
50
|
export type Alignment = 'start' | 'center' | 'end';
|
|
46
51
|
|
package/src/lib/types/index.ts
CHANGED
package/src/lib/types/layer.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RequiredDeep } from 'type-fest';
|
|
2
|
+
import { JustifyContent, AlignItems } from './css';
|
|
3
|
+
import { HyperNode } from './hypernode';
|
|
2
4
|
|
|
3
5
|
export type LayerOptions<EntryType extends Record<string, string>> = {
|
|
4
6
|
/**
|
|
@@ -15,4 +17,25 @@ export type LayerOptions<EntryType extends Record<string, string>> = {
|
|
|
15
17
|
* Decides if a layer should be rendered or not for a certain entry
|
|
16
18
|
*/
|
|
17
19
|
skip?: boolean | ((entry: EntryType) => boolean);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Control if should render bounding box for this layer
|
|
23
|
+
*/
|
|
24
|
+
renderBoundingBox?: boolean;
|
|
18
25
|
};
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_LAYER_OPTIONS: RequiredDeep<
|
|
28
|
+
LayerOptions<Record<string, string>>
|
|
29
|
+
> = {
|
|
30
|
+
justifyContent: 'center',
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
skip: false,
|
|
33
|
+
renderBoundingBox: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type LayerFnContext = {};
|
|
37
|
+
|
|
38
|
+
export type LayerFn<EntryType> = (
|
|
39
|
+
entry: EntryType,
|
|
40
|
+
context: LayerFnContext,
|
|
41
|
+
) => Promise<Array<HyperNode>>;
|