@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.
Files changed (64) hide show
  1. package/build/src/lib/replacement.d.ts +5 -3
  2. package/build/src/lib/replacement.js +6 -3
  3. package/build/src/lib/replacement.js.map +1 -1
  4. package/build/src/lib/template.d.ts +18 -26
  5. package/build/src/lib/template.js +197 -70
  6. package/build/src/lib/template.js.map +1 -1
  7. package/build/src/lib/types/containers.d.ts +14 -2
  8. package/build/src/lib/types/containers.js +2 -7
  9. package/build/src/lib/types/containers.js.map +1 -1
  10. package/build/src/lib/types/hypernode.d.ts +2 -0
  11. package/build/src/lib/types/hypernode.js +3 -0
  12. package/build/src/lib/types/hypernode.js.map +1 -0
  13. package/build/src/lib/types/image.d.ts +11 -5
  14. package/build/src/lib/types/image.js +2 -7
  15. package/build/src/lib/types/image.js.map +1 -1
  16. package/build/src/lib/types/index.d.ts +2 -0
  17. package/build/src/lib/types/index.js +2 -0
  18. package/build/src/lib/types/index.js.map +1 -1
  19. package/build/src/lib/types/layer.d.ts +6 -0
  20. package/build/src/lib/types/layer.js +7 -0
  21. package/build/src/lib/types/layer.js.map +1 -1
  22. package/build/src/lib/types/replacement.d.ts +2 -2
  23. package/build/src/lib/types/template.d.ts +13 -0
  24. package/build/src/lib/types/template.js +10 -0
  25. package/build/src/lib/types/template.js.map +1 -0
  26. package/build/src/lib/types/text.d.ts +9 -4
  27. package/build/src/lib/types/text.js +3 -10
  28. package/build/src/lib/types/text.js.map +1 -1
  29. package/build/src/lib/utils/container.js +10 -45
  30. package/build/src/lib/utils/container.js.map +1 -1
  31. package/build/src/lib/utils/htmlToImage.d.ts +2 -3
  32. package/build/src/lib/utils/htmlToImage.js.map +1 -1
  33. package/build/src/lib/utils/index.d.ts +1 -3
  34. package/build/src/lib/utils/index.js +1 -3
  35. package/build/src/lib/utils/index.js.map +1 -1
  36. package/build/src/lib/utils/placeBoundingBox.d.ts +2 -0
  37. package/build/src/lib/utils/placeBoundingBox.js +21 -0
  38. package/build/src/lib/utils/placeBoundingBox.js.map +1 -0
  39. package/build/src/lib/utils/placeImage.d.ts +7 -6
  40. package/build/src/lib/utils/placeImage.js +20 -20
  41. package/build/src/lib/utils/placeImage.js.map +1 -1
  42. package/build/src/lib/utils/placeText.d.ts +14 -0
  43. package/build/src/lib/utils/placeText.js +69 -0
  44. package/build/src/lib/utils/placeText.js.map +1 -0
  45. package/build/src/test.js +87 -15
  46. package/build/src/test.js.map +1 -1
  47. package/package.json +3 -1
  48. package/roadmap.md +3 -1
  49. package/src/lib/replacement.ts +6 -5
  50. package/src/lib/template.ts +336 -114
  51. package/src/lib/types/containers.ts +24 -8
  52. package/src/lib/types/hypernode.ts +4 -0
  53. package/src/lib/types/image.ts +20 -9
  54. package/src/lib/types/index.ts +2 -0
  55. package/src/lib/types/layer.ts +15 -0
  56. package/src/lib/types/replacement.ts +2 -2
  57. package/src/lib/types/template.ts +22 -0
  58. package/src/lib/types/text.ts +16 -12
  59. package/src/lib/utils/container.ts +13 -70
  60. package/src/lib/utils/htmlToImage.ts +3 -3
  61. package/src/lib/utils/index.ts +1 -3
  62. package/src/lib/utils/{drawBoundingBox.ts → placeBoundingBox.ts} +3 -9
  63. package/src/lib/utils/placeImage.ts +0 -62
  64. package/src/lib/utils/renderText.ts +0 -107
@@ -1,17 +1,17 @@
1
1
  import * as fs from 'fs';
2
2
  import { parse as parseCsv } from 'papaparse';
3
- import { Jimp, rgbaToInt } from '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
- drawBoundingBox,
8
+ placeBoundingBox,
10
9
  gridPackingFn,
11
10
  hboxPackingFn,
12
- placeImage,
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 RequiredTemplateOptions = {
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<ImageType | undefined>;
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 debugMode: boolean = false;
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
- return template.render(entry);
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
- imagesFn: (entry: EntryType) => Promise<Array<ImageType>>,
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, { debugMode }) => {
132
+ this.layer(async (entry: EntryType) => {
137
133
  const mergedOptions: RequiredDeep<ContainerOptions<EntryType>> =
138
- merge({}, DEFAULT_CONTAINER_OPTIONS, options);
134
+ merge(
135
+ {},
136
+ DEFAULT_CONTAINER_OPTIONS,
137
+ { renderBoundingBox: this.renderBoundingBox },
138
+ options,
139
+ );
139
140
  if (this.shouldSkipLayerForEntry(entry, mergedOptions)) {
140
- return undefined;
141
+ return [];
141
142
  }
142
143
 
143
- const images = await imagesFn(entry);
144
- const result = await packingFn(
145
- box,
146
- this.shadowBackground(),
147
- images,
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 (debugMode) {
152
- const debugImage = await drawBoundingBox(
153
- box,
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
- imagesFn: (entry: EntryType) => Promise<Array<ImageType>>,
162
+ elementsFn: ElementsFn,
164
163
  box: BoundingBox,
165
164
  options?: DirectionContainerOptions<EntryType>,
166
- ): this => this.container(imagesFn, box, hboxPackingFn(options), options);
165
+ ): this => this.container(elementsFn, box, hboxPackingFn(options), options);
167
166
 
168
167
  vbox = (
169
- imagesFn: (entry: EntryType) => Promise<Array<ImageType>>,
168
+ elementsFn: ElementsFn,
170
169
  box: BoundingBox,
171
170
  options?: DirectionContainerOptions<EntryType>,
172
- ): this => this.container(imagesFn, box, vboxPackingFn(options), options);
171
+ ): this => this.container(elementsFn, box, vboxPackingFn(options), options);
173
172
 
174
173
  grid = (
175
- imagesFn: (entry: EntryType) => Promise<Array<ImageType>>,
174
+ elementsFn: ElementsFn,
176
175
  box: BoundingBox,
177
176
  options?: GridContainerOptions<EntryType>,
178
- ): this => this.container(imagesFn, box, gridPackingFn(options), options);
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 (entry, { debugMode }) => {
184
+ this.layer(async entry => {
186
185
  const mergedOptions: RequiredDeep<ImageLayerOptions<EntryType>> =
187
186
  merge(
188
187
  {},
189
188
  DEFAULT_IMAGE_LAYER_OPTIONS,
190
- { assetsPath: this.defaultAssetsPath },
189
+ {
190
+ assetsPath: this.defaultAssetsPath,
191
+ renderBoundingBox: this.renderBoundingBox,
192
+ },
191
193
  options,
192
194
  );
193
195
  if (this.shouldSkipLayerForEntry(entry, mergedOptions)) {
194
- return undefined;
196
+ return [];
195
197
  }
196
198
 
197
- const image = await this.pathFromImageRef(
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 (debugMode) {
211
- const debugImage = await drawBoundingBox(
212
- box,
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 (entry, { debugMode }) => {
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 undefined;
242
+ return [];
244
243
  }
245
244
 
246
245
  const text = this.textFromTextRef(entry, ref);
247
- const result = await renderText(
246
+ const result = await this.placeText({
248
247
  text,
249
248
  box,
250
- this.backgroundSize,
251
- mergedOptions,
252
- this.fonts,
253
- );
249
+ options: mergedOptions,
250
+ });
254
251
 
255
252
  // debug mode
256
- if (debugMode) {
257
- const debugImage = await drawBoundingBox(
258
- box,
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.debugMode = true;
267
+ debug = (): this => {
268
+ this.debugPoints.push(this.layers.length);
275
269
  return this;
276
- }
270
+ };
277
271
 
278
- private async renderLayers(entry: EntryType): Promise<Array<ImageType>> {
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
- debugMode: this.debugMode,
276
+ renderBoundingBox: this.renderBoundingBox,
283
277
  }),
284
278
  ),
285
279
  );
286
- return results.filter(result => !!result);
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
- async render(
290
- entry: EntryType,
291
- options?: RenderOptions<EntryType>,
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, options).then(image => {
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 pathFromImageRef = async (
372
+ private imageFromImageRef = async (
357
373
  entry: EntryType,
358
374
  ref: ImageRef<EntryType>,
359
- options: RequiredDeep<ImageLayerOptions<EntryType>>,
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 (options.assetsPath) {
363
- pathSegments.push(options.assetsPath);
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
- background: ImageType,
71
- images: Array<ImageType>,
72
- ) => Promise<ImageType>;
87
+ elements: Array<HyperNode>,
88
+ ) => Promise<HyperNode>;
@@ -0,0 +1,4 @@
1
+ import { VNode } from "virtual-dom";
2
+
3
+ export type HyperNode = VNode;
4
+