@hellkite/pipkin 0.6.2 → 0.8.0
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/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 +18 -26
- package/build/src/lib/template.js +197 -70
- package/build/src/lib/template.js.map +1 -1
- package/build/src/lib/types/containers.d.ts +14 -2
- 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 +2 -0
- package/build/src/lib/types/hypernode.js +3 -0
- package/build/src/lib/types/hypernode.js.map +1 -0
- package/build/src/lib/types/image.d.ts +11 -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 +6 -0
- package/build/src/lib/types/layer.js +7 -0
- package/build/src/lib/types/layer.js.map +1 -1
- 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 +9 -4
- 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 +10 -45
- package/build/src/lib/utils/container.js.map +1 -1
- package/build/src/lib/utils/htmlToImage.d.ts +2 -3
- package/build/src/lib/utils/htmlToImage.js.map +1 -1
- package/build/src/lib/utils/index.d.ts +1 -3
- package/build/src/lib/utils/index.js +1 -3
- package/build/src/lib/utils/index.js.map +1 -1
- package/build/src/lib/utils/placeBoundingBox.d.ts +2 -0
- package/build/src/lib/utils/placeBoundingBox.js +21 -0
- package/build/src/lib/utils/placeBoundingBox.js.map +1 -0
- package/build/src/lib/utils/placeImage.d.ts +7 -6
- package/build/src/lib/utils/placeImage.js +20 -20
- package/build/src/lib/utils/placeImage.js.map +1 -1
- package/build/src/lib/utils/placeText.d.ts +14 -0
- package/build/src/lib/utils/placeText.js +69 -0
- package/build/src/lib/utils/placeText.js.map +1 -0
- package/build/src/test.js +87 -15
- package/build/src/test.js.map +1 -1
- package/package.json +3 -1
- package/roadmap.md +3 -1
- package/src/lib/replacement.ts +6 -5
- package/src/lib/template.ts +336 -114
- package/src/lib/types/containers.ts +24 -8
- package/src/lib/types/hypernode.ts +4 -0
- package/src/lib/types/image.ts +20 -9
- package/src/lib/types/index.ts +2 -0
- package/src/lib/types/layer.ts +15 -0
- package/src/lib/types/replacement.ts +2 -2
- package/src/lib/types/template.ts +22 -0
- package/src/lib/types/text.ts +16 -12
- package/src/lib/utils/container.ts +13 -70
- package/src/lib/utils/htmlToImage.ts +3 -3
- package/src/lib/utils/index.ts +1 -3
- package/src/lib/utils/{drawBoundingBox.ts → placeBoundingBox.ts} +3 -9
- package/src/lib/utils/placeImage.ts +0 -62
- package/src/lib/utils/renderText.ts +0 -107
package/src/lib/template.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
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
|
-
import { registerFont } from 'canvas';
|
|
7
6
|
import path from 'path';
|
|
8
7
|
import {
|
|
9
|
-
|
|
8
|
+
placeBoundingBox,
|
|
10
9
|
gridPackingFn,
|
|
11
10
|
hboxPackingFn,
|
|
12
|
-
|
|
13
|
-
renderText,
|
|
11
|
+
htmlToImage,
|
|
14
12
|
vboxPackingFn,
|
|
13
|
+
boundingBoxToPx,
|
|
14
|
+
toPx,
|
|
15
15
|
} from './utils';
|
|
16
16
|
import {
|
|
17
17
|
DirectionContainerOptions,
|
|
@@ -31,37 +31,28 @@ import {
|
|
|
31
31
|
LayerOptions,
|
|
32
32
|
ContainerOptions,
|
|
33
33
|
DEFAULT_CONTAINER_OPTIONS,
|
|
34
|
+
HyperNode,
|
|
35
|
+
ElementsFn,
|
|
36
|
+
ElementRef,
|
|
37
|
+
ImageLayerSpecificOptions,
|
|
38
|
+
TextLayerSpecificOptions,
|
|
39
|
+
SCALE_MODE_TO_OBJECT_FIT,
|
|
40
|
+
StaticImageRef,
|
|
41
|
+
TemplateOptions,
|
|
42
|
+
DEFAULT_TEMPLATE_OPTIONS,
|
|
34
43
|
} from './types';
|
|
35
44
|
import merge from 'lodash.merge';
|
|
36
45
|
import { RequiredDeep } from 'type-fest';
|
|
46
|
+
import { h } from 'virtual-dom';
|
|
47
|
+
import flatten from 'lodash.flatten';
|
|
48
|
+
import { ReplacementBuilder } from './replacement';
|
|
37
49
|
|
|
38
|
-
type
|
|
39
|
-
height: number;
|
|
40
|
-
width: number;
|
|
41
|
-
color: number;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
type OptionalTemplateOptions = Partial<{
|
|
45
|
-
defaultFontFamily: string;
|
|
46
|
-
defaultAssetsPath: string;
|
|
47
|
-
}>;
|
|
48
|
-
|
|
49
|
-
export type TemplateOptions = RequiredTemplateOptions & OptionalTemplateOptions;
|
|
50
|
-
|
|
51
|
-
const DEFAULT_TEMPLATE_OPTIONS: RequiredTemplateOptions = {
|
|
52
|
-
height: 1050,
|
|
53
|
-
width: 750,
|
|
54
|
-
color: rgbaToInt(255, 255, 255, 255),
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export type LayerFnContext = {
|
|
58
|
-
debugMode: boolean;
|
|
59
|
-
};
|
|
50
|
+
export type LayerFnContext = {};
|
|
60
51
|
|
|
61
52
|
export type LayerFn<EntryType> = (
|
|
62
53
|
entry: EntryType,
|
|
63
54
|
context: LayerFnContext,
|
|
64
|
-
) => Promise<
|
|
55
|
+
) => Promise<Array<HyperNode>>;
|
|
65
56
|
|
|
66
57
|
export type TemplateLayerFn<EntryType extends Record<string, string>> = (
|
|
67
58
|
template: Template<EntryType>,
|
|
@@ -70,8 +61,9 @@ export type TemplateLayerFn<EntryType extends Record<string, string>> = (
|
|
|
70
61
|
export class Template<EntryType extends Record<string, string>> {
|
|
71
62
|
private readonly fonts: Record<string, string> = {};
|
|
72
63
|
private readonly layers: LayerFn<EntryType>[] = [];
|
|
64
|
+
private readonly debugPoints: Array<number> = [];
|
|
73
65
|
private readonly background: ImageType;
|
|
74
|
-
private
|
|
66
|
+
private renderBoundingBox?: boolean;
|
|
75
67
|
private defaultFontFamily?: string;
|
|
76
68
|
private defaultAssetsPath?: string;
|
|
77
69
|
|
|
@@ -102,13 +94,6 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
102
94
|
});
|
|
103
95
|
}
|
|
104
96
|
|
|
105
|
-
private shadowBackground(): ImageType {
|
|
106
|
-
return new Jimp({
|
|
107
|
-
width: this.background.width,
|
|
108
|
-
height: this.background.height,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
97
|
private get backgroundSize(): Size {
|
|
113
98
|
return {
|
|
114
99
|
width: this.background.width,
|
|
@@ -122,100 +107,113 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
122
107
|
}
|
|
123
108
|
|
|
124
109
|
template = (fn: TemplateLayerFn<EntryType>): this =>
|
|
125
|
-
this.layer(entry => {
|
|
110
|
+
this.layer(async entry => {
|
|
126
111
|
const template = fn(this.shadowTemplate());
|
|
127
|
-
|
|
112
|
+
const image = await template.render(entry);
|
|
113
|
+
return [
|
|
114
|
+
await this.placeImage({
|
|
115
|
+
image,
|
|
116
|
+
box: {
|
|
117
|
+
left: 0,
|
|
118
|
+
top: 0,
|
|
119
|
+
...this.backgroundSize,
|
|
120
|
+
},
|
|
121
|
+
options: DEFAULT_IMAGE_LAYER_OPTIONS,
|
|
122
|
+
}),
|
|
123
|
+
];
|
|
128
124
|
});
|
|
129
125
|
|
|
130
126
|
container = (
|
|
131
|
-
|
|
127
|
+
elementsFn: ElementsFn,
|
|
132
128
|
box: BoundingBox,
|
|
133
129
|
packingFn: PackingFn,
|
|
134
130
|
options?: ContainerOptions<EntryType>,
|
|
135
131
|
): this =>
|
|
136
|
-
this.layer(async (entry: EntryType
|
|
132
|
+
this.layer(async (entry: EntryType) => {
|
|
137
133
|
const mergedOptions: RequiredDeep<ContainerOptions<EntryType>> =
|
|
138
|
-
merge(
|
|
134
|
+
merge(
|
|
135
|
+
{},
|
|
136
|
+
DEFAULT_CONTAINER_OPTIONS,
|
|
137
|
+
{ renderBoundingBox: this.renderBoundingBox },
|
|
138
|
+
options,
|
|
139
|
+
);
|
|
139
140
|
if (this.shouldSkipLayerForEntry(entry, mergedOptions)) {
|
|
140
|
-
return
|
|
141
|
+
return [];
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
const elementRefs = await elementsFn(entry);
|
|
145
|
+
const elements = await Promise.all(
|
|
146
|
+
elementRefs.map(elementRef =>
|
|
147
|
+
this.elementFromElementRef(entry, elementRef),
|
|
148
|
+
),
|
|
148
149
|
);
|
|
150
|
+
const result = await packingFn(box, elements);
|
|
149
151
|
|
|
150
152
|
// debug mode
|
|
151
|
-
if (
|
|
152
|
-
const debugImage = await
|
|
153
|
-
|
|
154
|
-
this.backgroundSize,
|
|
155
|
-
);
|
|
156
|
-
return debugImage.composite(result);
|
|
153
|
+
if (mergedOptions.renderBoundingBox) {
|
|
154
|
+
const debugImage = await placeBoundingBox(box);
|
|
155
|
+
return [result, debugImage];
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
return result;
|
|
158
|
+
return [result];
|
|
160
159
|
});
|
|
161
160
|
|
|
162
161
|
hbox = (
|
|
163
|
-
|
|
162
|
+
elementsFn: ElementsFn,
|
|
164
163
|
box: BoundingBox,
|
|
165
164
|
options?: DirectionContainerOptions<EntryType>,
|
|
166
|
-
): this => this.container(
|
|
165
|
+
): this => this.container(elementsFn, box, hboxPackingFn(options), options);
|
|
167
166
|
|
|
168
167
|
vbox = (
|
|
169
|
-
|
|
168
|
+
elementsFn: ElementsFn,
|
|
170
169
|
box: BoundingBox,
|
|
171
170
|
options?: DirectionContainerOptions<EntryType>,
|
|
172
|
-
): this => this.container(
|
|
171
|
+
): this => this.container(elementsFn, box, vboxPackingFn(options), options);
|
|
173
172
|
|
|
174
173
|
grid = (
|
|
175
|
-
|
|
174
|
+
elementsFn: ElementsFn,
|
|
176
175
|
box: BoundingBox,
|
|
177
176
|
options?: GridContainerOptions<EntryType>,
|
|
178
|
-
): this => this.container(
|
|
177
|
+
): this => this.container(elementsFn, box, gridPackingFn(options), options);
|
|
179
178
|
|
|
180
179
|
image = (
|
|
181
180
|
ref: ImageRef<EntryType>,
|
|
182
181
|
box: BoundingBox,
|
|
183
182
|
options: ImageLayerOptions<EntryType>,
|
|
184
183
|
): this =>
|
|
185
|
-
this.layer(async
|
|
184
|
+
this.layer(async entry => {
|
|
186
185
|
const mergedOptions: RequiredDeep<ImageLayerOptions<EntryType>> =
|
|
187
186
|
merge(
|
|
188
187
|
{},
|
|
189
188
|
DEFAULT_IMAGE_LAYER_OPTIONS,
|
|
190
|
-
{
|
|
189
|
+
{
|
|
190
|
+
assetsPath: this.defaultAssetsPath,
|
|
191
|
+
renderBoundingBox: this.renderBoundingBox,
|
|
192
|
+
},
|
|
191
193
|
options,
|
|
192
194
|
);
|
|
193
195
|
if (this.shouldSkipLayerForEntry(entry, mergedOptions)) {
|
|
194
|
-
return
|
|
196
|
+
return [];
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
const image = await this.
|
|
199
|
+
const image = await this.imageFromImageRef(
|
|
198
200
|
entry,
|
|
199
201
|
ref,
|
|
200
|
-
mergedOptions,
|
|
202
|
+
mergedOptions.assetsPath,
|
|
201
203
|
);
|
|
202
|
-
const result = await placeImage({
|
|
204
|
+
const result = await this.placeImage({
|
|
203
205
|
image,
|
|
204
206
|
box,
|
|
205
|
-
backgroundSize: this.backgroundSize,
|
|
206
207
|
options: mergedOptions,
|
|
207
208
|
});
|
|
208
209
|
|
|
209
210
|
// debug mode
|
|
210
|
-
if (
|
|
211
|
-
const debugImage = await
|
|
212
|
-
|
|
213
|
-
this.backgroundSize,
|
|
214
|
-
);
|
|
215
|
-
return debugImage.composite(result);
|
|
211
|
+
if (mergedOptions.renderBoundingBox) {
|
|
212
|
+
const debugImage = await placeBoundingBox(box);
|
|
213
|
+
return [result, debugImage];
|
|
216
214
|
}
|
|
217
215
|
|
|
218
|
-
return result;
|
|
216
|
+
return [result];
|
|
219
217
|
});
|
|
220
218
|
|
|
221
219
|
loadImage = async (imagePath: string | Buffer): Promise<ImageType> => {
|
|
@@ -228,7 +226,7 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
228
226
|
box: BoundingBox,
|
|
229
227
|
options?: TextLayerOptions<EntryType>,
|
|
230
228
|
): this =>
|
|
231
|
-
this.layer(async
|
|
229
|
+
this.layer(async entry => {
|
|
232
230
|
const mergedOptions = merge(
|
|
233
231
|
{},
|
|
234
232
|
DEFAULT_TEXT_LAYER_OPTIONS,
|
|
@@ -236,32 +234,28 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
236
234
|
font: {
|
|
237
235
|
family: this.defaultFontFamily,
|
|
238
236
|
},
|
|
237
|
+
renderBoundingBox: this.renderBoundingBox,
|
|
239
238
|
} as FontOptions,
|
|
240
239
|
options,
|
|
241
240
|
);
|
|
242
241
|
if (this.shouldSkipLayerForEntry(entry, mergedOptions)) {
|
|
243
|
-
return
|
|
242
|
+
return [];
|
|
244
243
|
}
|
|
245
244
|
|
|
246
245
|
const text = this.textFromTextRef(entry, ref);
|
|
247
|
-
const result = await
|
|
246
|
+
const result = await this.placeText({
|
|
248
247
|
text,
|
|
249
248
|
box,
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this.fonts,
|
|
253
|
-
);
|
|
249
|
+
options: mergedOptions,
|
|
250
|
+
});
|
|
254
251
|
|
|
255
252
|
// debug mode
|
|
256
|
-
if (
|
|
257
|
-
const debugImage = await
|
|
258
|
-
|
|
259
|
-
this.backgroundSize,
|
|
260
|
-
);
|
|
261
|
-
return debugImage.composite(result);
|
|
253
|
+
if (mergedOptions.renderBoundingBox) {
|
|
254
|
+
const debugImage = await placeBoundingBox(box);
|
|
255
|
+
return [result, debugImage];
|
|
262
256
|
}
|
|
263
257
|
|
|
264
|
-
return result;
|
|
258
|
+
return [result];
|
|
265
259
|
});
|
|
266
260
|
|
|
267
261
|
font(path: fs.PathLike, name: string): this {
|
|
@@ -270,31 +264,53 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
270
264
|
return this;
|
|
271
265
|
}
|
|
272
266
|
|
|
273
|
-
debug(): this {
|
|
274
|
-
this.
|
|
267
|
+
debug = (): this => {
|
|
268
|
+
this.debugPoints.push(this.layers.length);
|
|
275
269
|
return this;
|
|
276
|
-
}
|
|
270
|
+
};
|
|
277
271
|
|
|
278
|
-
|
|
272
|
+
async render(entry: EntryType): Promise<ImageType> {
|
|
279
273
|
const results = await Promise.all(
|
|
280
274
|
this.layers.map(layerFn =>
|
|
281
275
|
layerFn(entry, {
|
|
282
|
-
|
|
276
|
+
renderBoundingBox: this.renderBoundingBox,
|
|
283
277
|
}),
|
|
284
278
|
),
|
|
285
279
|
);
|
|
286
|
-
|
|
287
|
-
|
|
280
|
+
const buildDocument = (children: Array<HyperNode>) =>
|
|
281
|
+
h('html', [
|
|
282
|
+
h('head', [
|
|
283
|
+
h(
|
|
284
|
+
'style',
|
|
285
|
+
Object.entries(this.fonts)
|
|
286
|
+
.map(
|
|
287
|
+
([name, data]) =>
|
|
288
|
+
`@font-face {
|
|
289
|
+
font-family: '${name}';
|
|
290
|
+
src: url(data:font/ttf;base64,${data}) format('truetype');
|
|
291
|
+
}`,
|
|
292
|
+
)
|
|
293
|
+
.join('\n'),
|
|
294
|
+
),
|
|
295
|
+
]),
|
|
296
|
+
h('body', children),
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
// TODO: move it to a proper place
|
|
300
|
+
for (const debugPoint of this.debugPoints) {
|
|
301
|
+
const debugRender = await htmlToImage(
|
|
302
|
+
buildDocument(flatten(results.slice(0, debugPoint))),
|
|
303
|
+
this.backgroundSize,
|
|
304
|
+
);
|
|
305
|
+
const debugImage: ImageType = await this.background.clone().composite(debugRender);
|
|
306
|
+
await debugImage.write('assets/debug-1.png');
|
|
307
|
+
}
|
|
288
308
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
): Promise<ImageType> {
|
|
293
|
-
const renderedLayers = await this.renderLayers(entry);
|
|
294
|
-
return renderedLayers.reduce(
|
|
295
|
-
(acc, layerRender) => acc.composite(layerRender),
|
|
296
|
-
this.background.clone(),
|
|
309
|
+
const render = await htmlToImage(
|
|
310
|
+
buildDocument(flatten(results)),
|
|
311
|
+
this.backgroundSize,
|
|
297
312
|
);
|
|
313
|
+
return this.background.clone().composite(render);
|
|
298
314
|
}
|
|
299
315
|
|
|
300
316
|
async renderAll(
|
|
@@ -305,7 +321,7 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
305
321
|
entries.map(
|
|
306
322
|
entry =>
|
|
307
323
|
new Promise<Array<ImageType>>(resolve =>
|
|
308
|
-
this.render(entry
|
|
324
|
+
this.render(entry).then(image => {
|
|
309
325
|
if (!options?.duplication) {
|
|
310
326
|
return resolve([image]);
|
|
311
327
|
}
|
|
@@ -353,14 +369,34 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
353
369
|
});
|
|
354
370
|
}
|
|
355
371
|
|
|
356
|
-
private
|
|
372
|
+
private imageFromImageRef = async (
|
|
357
373
|
entry: EntryType,
|
|
358
374
|
ref: ImageRef<EntryType>,
|
|
359
|
-
|
|
375
|
+
assetsPath: string,
|
|
376
|
+
): Promise<ImageType> => {
|
|
377
|
+
const pathSegments = [];
|
|
378
|
+
if (assetsPath.length) {
|
|
379
|
+
pathSegments.push(assetsPath);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if ('key' in ref) {
|
|
383
|
+
const fileName = entry[ref.key];
|
|
384
|
+
return this.loadImage(path.join(...pathSegments, fileName));
|
|
385
|
+
} else if ('pathFn' in ref) {
|
|
386
|
+
const fileName = ref.pathFn(entry);
|
|
387
|
+
return this.loadImage(path.join(...pathSegments, fileName));
|
|
388
|
+
} else {
|
|
389
|
+
return this.imageFromStaticImageRef(ref, assetsPath);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
private imageFromStaticImageRef = async (
|
|
394
|
+
ref: StaticImageRef,
|
|
395
|
+
assetsPath: string,
|
|
360
396
|
): Promise<ImageType> => {
|
|
361
397
|
const pathSegments = [];
|
|
362
|
-
if (
|
|
363
|
-
pathSegments.push(
|
|
398
|
+
if (assetsPath.length) {
|
|
399
|
+
pathSegments.push(assetsPath);
|
|
364
400
|
}
|
|
365
401
|
|
|
366
402
|
if ('buffer' in ref) {
|
|
@@ -369,12 +405,6 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
369
405
|
return this.loadImage(path.join(...pathSegments, ref.path));
|
|
370
406
|
} else if ('absolutePath' in ref) {
|
|
371
407
|
return this.loadImage(ref.absolutePath);
|
|
372
|
-
} else if ('key' in ref) {
|
|
373
|
-
const fileName = entry[ref.key];
|
|
374
|
-
return this.loadImage(path.join(...pathSegments, fileName));
|
|
375
|
-
} else if ('pathFn' in ref) {
|
|
376
|
-
const fileName = ref.pathFn(entry);
|
|
377
|
-
return this.loadImage(path.join(...pathSegments, fileName));
|
|
378
408
|
} else {
|
|
379
409
|
throw new Error('Unknown ImageRef variant');
|
|
380
410
|
}
|
|
@@ -395,6 +425,33 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
395
425
|
}
|
|
396
426
|
};
|
|
397
427
|
|
|
428
|
+
private elementFromElementRef = async (
|
|
429
|
+
entry: EntryType,
|
|
430
|
+
ref: ElementRef<EntryType>,
|
|
431
|
+
): Promise<HyperNode> => {
|
|
432
|
+
if ('text' in ref) {
|
|
433
|
+
const options = merge({}, DEFAULT_TEXT_LAYER_OPTIONS, ref.options);
|
|
434
|
+
return this.prepareText({
|
|
435
|
+
text: this.textFromTextRef(entry, ref.text),
|
|
436
|
+
options,
|
|
437
|
+
});
|
|
438
|
+
} else if ('image' in ref) {
|
|
439
|
+
const options = merge({}, DEFAULT_IMAGE_LAYER_OPTIONS, ref.options);
|
|
440
|
+
return this.prepareImage({
|
|
441
|
+
image: await this.imageFromImageRef(
|
|
442
|
+
entry,
|
|
443
|
+
ref.image,
|
|
444
|
+
options.assetsPath,
|
|
445
|
+
),
|
|
446
|
+
options,
|
|
447
|
+
});
|
|
448
|
+
} else if ('node' in ref) {
|
|
449
|
+
return ref.node;
|
|
450
|
+
} else {
|
|
451
|
+
throw new Error('Unknown TextRef variant');
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
398
455
|
private shouldSkipLayerForEntry = (
|
|
399
456
|
entry: EntryType,
|
|
400
457
|
options: LayerOptions<EntryType>,
|
|
@@ -404,4 +461,169 @@ export class Template<EntryType extends Record<string, string>> {
|
|
|
404
461
|
}
|
|
405
462
|
return options.skip ?? false;
|
|
406
463
|
};
|
|
464
|
+
|
|
465
|
+
private placeText = async ({
|
|
466
|
+
text,
|
|
467
|
+
box,
|
|
468
|
+
options,
|
|
469
|
+
}: {
|
|
470
|
+
text: string;
|
|
471
|
+
box: BoundingBox;
|
|
472
|
+
options: RequiredDeep<TextLayerOptions<EntryType>>;
|
|
473
|
+
}): Promise<HyperNode> => {
|
|
474
|
+
return h(
|
|
475
|
+
'div',
|
|
476
|
+
{
|
|
477
|
+
style: {
|
|
478
|
+
display: 'flex',
|
|
479
|
+
overflow: 'visible',
|
|
480
|
+
position: 'absolute',
|
|
481
|
+
|
|
482
|
+
justifyContent: options.justifyContent,
|
|
483
|
+
alignItems: options.alignItems,
|
|
484
|
+
|
|
485
|
+
...boundingBoxToPx(box),
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
[
|
|
489
|
+
await this.prepareText({
|
|
490
|
+
text,
|
|
491
|
+
options,
|
|
492
|
+
}),
|
|
493
|
+
],
|
|
494
|
+
);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
private prepareText = async ({
|
|
498
|
+
text,
|
|
499
|
+
options,
|
|
500
|
+
}: {
|
|
501
|
+
text: string;
|
|
502
|
+
options: RequiredDeep<TextLayerSpecificOptions<EntryType>>;
|
|
503
|
+
}): Promise<HyperNode> => {
|
|
504
|
+
let textChildren: Array<string | HyperNode> = [text];
|
|
505
|
+
const replacementBuilder = new ReplacementBuilder();
|
|
506
|
+
options.replacementFn(replacementBuilder);
|
|
507
|
+
const replacementMap = replacementBuilder.build();
|
|
508
|
+
for (const [word, imageRef] of Object.entries(replacementMap)) {
|
|
509
|
+
const regex = new RegExp(word, 'gi');
|
|
510
|
+
const textOptions: RequiredDeep<ImageLayerOptions<EntryType>> =
|
|
511
|
+
merge(
|
|
512
|
+
{},
|
|
513
|
+
DEFAULT_IMAGE_LAYER_OPTIONS,
|
|
514
|
+
{ assetsPath: this.defaultAssetsPath },
|
|
515
|
+
options,
|
|
516
|
+
);
|
|
517
|
+
const image = await this.imageFromStaticImageRef(
|
|
518
|
+
imageRef,
|
|
519
|
+
textOptions.assetsPath,
|
|
520
|
+
);
|
|
521
|
+
const imageBase64 = await image.getBase64('image/png');
|
|
522
|
+
|
|
523
|
+
let tmpChildren: Array<string | HyperNode> = [];
|
|
524
|
+
for (const textSegment of textChildren) {
|
|
525
|
+
if (typeof textSegment !== 'string') {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const parts = (textSegment as string).split(regex);
|
|
530
|
+
for (let index = 0; index < parts.length; index++) {
|
|
531
|
+
if (index > 0) {
|
|
532
|
+
tmpChildren.push(
|
|
533
|
+
h(
|
|
534
|
+
'img',
|
|
535
|
+
{
|
|
536
|
+
style: {
|
|
537
|
+
display: 'inline',
|
|
538
|
+
verticalAlign: 'middle',
|
|
539
|
+
height: toPx(options.font.size),
|
|
540
|
+
width: 'auto',
|
|
541
|
+
},
|
|
542
|
+
src: imageBase64,
|
|
543
|
+
},
|
|
544
|
+
[],
|
|
545
|
+
),
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
tmpChildren.push(parts[index]);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
textChildren = tmpChildren;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return h(
|
|
556
|
+
'div',
|
|
557
|
+
{
|
|
558
|
+
style: {
|
|
559
|
+
overflow: 'visible',
|
|
560
|
+
overflowWrap: 'word-wrap',
|
|
561
|
+
whiteSpace: 'normal',
|
|
562
|
+
|
|
563
|
+
color: options.color,
|
|
564
|
+
fontFamily: options.font.family,
|
|
565
|
+
fontSize: options.font.size,
|
|
566
|
+
fontStyle: options.font.italic ? 'italic' : undefined,
|
|
567
|
+
fontWeight: options.font.bold ? 'bold' : undefined,
|
|
568
|
+
|
|
569
|
+
'-webkit-text-stroke': `${options.border.width}px ${options.border.color}`,
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
textChildren,
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
private placeImage = async ({
|
|
577
|
+
image,
|
|
578
|
+
box,
|
|
579
|
+
options,
|
|
580
|
+
}: {
|
|
581
|
+
image: ImageType;
|
|
582
|
+
box: BoundingBox;
|
|
583
|
+
options: RequiredDeep<ImageLayerOptions<EntryType>>;
|
|
584
|
+
}): Promise<HyperNode> => {
|
|
585
|
+
return h(
|
|
586
|
+
'div',
|
|
587
|
+
{
|
|
588
|
+
style: {
|
|
589
|
+
display: 'flex',
|
|
590
|
+
position: 'absolute',
|
|
591
|
+
scale: 1,
|
|
592
|
+
|
|
593
|
+
justifyContent: options.justifyContent,
|
|
594
|
+
alignItems: options.alignItems,
|
|
595
|
+
|
|
596
|
+
...boundingBoxToPx(box),
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
[await this.prepareImage({ image, options })],
|
|
600
|
+
);
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
private prepareImage = async ({
|
|
604
|
+
image,
|
|
605
|
+
options,
|
|
606
|
+
}: {
|
|
607
|
+
image: ImageType;
|
|
608
|
+
options: RequiredDeep<ImageLayerSpecificOptions<EntryType>>;
|
|
609
|
+
}): Promise<HyperNode> => {
|
|
610
|
+
const imageBase64 = await image.getBase64('image/png');
|
|
611
|
+
const objectFit = SCALE_MODE_TO_OBJECT_FIT[options.scale];
|
|
612
|
+
|
|
613
|
+
return h(
|
|
614
|
+
'img',
|
|
615
|
+
{
|
|
616
|
+
style: {
|
|
617
|
+
objectFit,
|
|
618
|
+
flex: '1 1 auto',
|
|
619
|
+
minWidth: 0,
|
|
620
|
+
minHeight: 0,
|
|
621
|
+
maxWidth: '100%',
|
|
622
|
+
maxHeight: '100%',
|
|
623
|
+
},
|
|
624
|
+
src: imageBase64,
|
|
625
|
+
},
|
|
626
|
+
[],
|
|
627
|
+
);
|
|
628
|
+
};
|
|
407
629
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { RequiredDeep } from 'type-fest';
|
|
2
2
|
import { BoundingBox } from './boundingBox';
|
|
3
|
-
import { ImageType } from './image';
|
|
3
|
+
import { ImageLayerSpecificOptions, ImageRef, ImageType } from './image';
|
|
4
4
|
import { ScaleMode } from './scale';
|
|
5
|
-
import { LayerOptions } from './layer';
|
|
5
|
+
import { DEFAULT_LAYER_OPTIONS, LayerOptions } from './layer';
|
|
6
|
+
import { HyperNode } from './hypernode';
|
|
7
|
+
import { TextLayerSpecificOptions, TextRef } from './text';
|
|
6
8
|
|
|
7
9
|
export type ContainerOptions<EntryType extends Record<string, string>> =
|
|
8
10
|
LayerOptions<EntryType> & {
|
|
@@ -23,11 +25,9 @@ export type ContainerOptions<EntryType extends Record<string, string>> =
|
|
|
23
25
|
export const DEFAULT_CONTAINER_OPTIONS: RequiredDeep<
|
|
24
26
|
ContainerOptions<Record<string, string>>
|
|
25
27
|
> = {
|
|
28
|
+
...DEFAULT_LAYER_OPTIONS,
|
|
26
29
|
gap: 0,
|
|
27
|
-
justifyContent: 'normal',
|
|
28
|
-
alignItems: 'center',
|
|
29
30
|
scale: 'none',
|
|
30
|
-
skip: false,
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export type DirectionContainerOptions<
|
|
@@ -65,8 +65,24 @@ export const DEFAULT_GRID_CONTAINER_OPTIONS: RequiredDeep<
|
|
|
65
65
|
direction: 'rows',
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
export type ElementRef<EntryType extends Record<string, string>> =
|
|
69
|
+
| {
|
|
70
|
+
image: ImageRef<EntryType>;
|
|
71
|
+
options?: ImageLayerSpecificOptions<EntryType>;
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
text: TextRef<EntryType>;
|
|
75
|
+
options?: TextLayerSpecificOptions<EntryType>;
|
|
76
|
+
}
|
|
77
|
+
| {
|
|
78
|
+
node: HyperNode;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type ElementsFn = <EntryType extends Record<string, string>>(
|
|
82
|
+
entry: EntryType,
|
|
83
|
+
) => Promise<Array<ElementRef<EntryType>>>;
|
|
84
|
+
|
|
68
85
|
export type PackingFn = (
|
|
69
86
|
box: BoundingBox,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
) => Promise<ImageType>;
|
|
87
|
+
elements: Array<HyperNode>,
|
|
88
|
+
) => Promise<HyperNode>;
|