pptx-custom 0.3.0 → 0.4.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/README.md +94 -16
- package/README.zh-CN.md +131 -0
- package/dist/index.js +181 -167
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +181 -167
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# pptx-custom
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[中文文档](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
5
|
+
Utilities for customizing `json2pptx` JSON decks in two stages:
|
|
6
|
+
|
|
7
|
+
- Content stage: map backend slide content into a template deck.
|
|
8
|
+
- Theme stage: replace theme colors/font/background and apply scoped media.
|
|
8
9
|
|
|
9
10
|
## Install
|
|
10
11
|
|
|
@@ -12,31 +13,85 @@ Utilities to customize PPTX JSON templates in two stages:
|
|
|
12
13
|
npm i pptx-custom
|
|
13
14
|
```
|
|
14
15
|
|
|
15
|
-
##
|
|
16
|
+
## Exports
|
|
17
|
+
|
|
18
|
+
- `applyCustomContent(template, input)`
|
|
19
|
+
- `parseCustomContent(raw)`
|
|
20
|
+
- `applyCustomContentToTemplate(template, slides)`
|
|
21
|
+
- `applyCustomTheme(deck, themeInput)`
|
|
22
|
+
|
|
23
|
+
Types are also exported, including:
|
|
24
|
+
`CustomSlide`, `Deck`, `PptxCustomContentInput`, `PptxCustomThemeInput`,
|
|
25
|
+
`PptxCustomOptions`, `TemplateJson`, `TemplateJsonSlide`, `TemplateJsonElement`,
|
|
26
|
+
`TemplateJsonTheme`.
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
16
29
|
|
|
17
30
|
```ts
|
|
18
31
|
import {
|
|
19
32
|
applyCustomContent,
|
|
20
33
|
applyCustomTheme,
|
|
21
34
|
parseCustomContent,
|
|
35
|
+
applyCustomContentToTemplate
|
|
22
36
|
} from 'pptx-custom'
|
|
23
37
|
|
|
24
|
-
const
|
|
38
|
+
const withContent = applyCustomContent(templateDeck, backendText)
|
|
25
39
|
|
|
26
|
-
const
|
|
40
|
+
const withTheme = applyCustomTheme(withContent, {
|
|
27
41
|
themeColors: ['#111111', '#333333', '#555555', '#777777', '#999999', '#BBBBBB'],
|
|
28
42
|
fontColor: '#222222',
|
|
29
|
-
backgroundColor: '#FFFFFF'
|
|
43
|
+
backgroundColor: '#FFFFFF',
|
|
44
|
+
backgroundImage: {
|
|
45
|
+
src: 'https://example.com/background.png',
|
|
46
|
+
scope: {
|
|
47
|
+
cover: false,
|
|
48
|
+
contents: true,
|
|
49
|
+
transition: true,
|
|
50
|
+
content: true,
|
|
51
|
+
end: false
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
logoImage: {
|
|
55
|
+
src: 'https://example.com/logo.png',
|
|
56
|
+
position: 'right',
|
|
57
|
+
scope: {
|
|
58
|
+
cover: true,
|
|
59
|
+
contents: true,
|
|
60
|
+
transition: true,
|
|
61
|
+
content: true,
|
|
62
|
+
end: true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
30
65
|
})
|
|
31
66
|
|
|
32
67
|
const slides = parseCustomContent(backendText)
|
|
33
|
-
const
|
|
34
|
-
|
|
68
|
+
const withContentDirect = applyCustomContentToTemplate(templateDeck, slides)
|
|
35
69
|
```
|
|
36
70
|
|
|
37
|
-
## Custom Content
|
|
71
|
+
## Custom Content Input
|
|
72
|
+
|
|
73
|
+
`parseCustomContent` and `applyCustomContent` support:
|
|
74
|
+
|
|
75
|
+
1. NDJSON (one slide per line)
|
|
76
|
+
2. JSON array of slides
|
|
77
|
+
3. JSON object with a `slides` array
|
|
78
|
+
4. JSON object containing a single slide (with `type`)
|
|
79
|
+
|
|
80
|
+
Supported slide types:
|
|
38
81
|
|
|
39
|
-
|
|
82
|
+
- `cover`
|
|
83
|
+
- `contents`
|
|
84
|
+
- `transition`
|
|
85
|
+
- `content`
|
|
86
|
+
- `end`
|
|
87
|
+
|
|
88
|
+
Legacy aliases accepted in input:
|
|
89
|
+
|
|
90
|
+
- `agenda` -> `contents`
|
|
91
|
+
- `section` -> `transition`
|
|
92
|
+
- `ending` -> `end`
|
|
93
|
+
|
|
94
|
+
Example NDJSON:
|
|
40
95
|
|
|
41
96
|
```json
|
|
42
97
|
{"type":"cover","data":{"title":"Title","text":"Subtitle"}}
|
|
@@ -46,8 +101,31 @@ const deckFromSlides = applyCustomContentToTemplate(templateDeck, slides)
|
|
|
46
101
|
{"type":"end"}
|
|
47
102
|
```
|
|
48
103
|
|
|
49
|
-
##
|
|
104
|
+
## Theme Input
|
|
50
105
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- `
|
|
106
|
+
`applyCustomTheme` accepts `PptxCustomThemeInput`:
|
|
107
|
+
|
|
108
|
+
- `themeColors: string[]` (uses first 6)
|
|
109
|
+
- `fontColor: string`
|
|
110
|
+
- `backgroundColor?: string`
|
|
111
|
+
- `backgroundImage?: { src, scope, width?, height? }`
|
|
112
|
+
- `logoImage?: { src, scope, position, width?, height? }`
|
|
113
|
+
- `clearBackgroundImage?: boolean`
|
|
114
|
+
- `clearLogoImage?: boolean`
|
|
115
|
+
|
|
116
|
+
`scope` keys:
|
|
117
|
+
`cover | contents | transition | content | end`
|
|
118
|
+
|
|
119
|
+
## Behavior Notes
|
|
120
|
+
|
|
121
|
+
- Both `applyCustomContent` and `applyCustomTheme` run through `json2pptx-schema`
|
|
122
|
+
parsing/normalization before returning.
|
|
123
|
+
- `applyCustomContent` selects template slides by `type`, and for `contents/content`
|
|
124
|
+
prefers layouts with the closest capacity to the requested item count.
|
|
125
|
+
- `applyCustomContent` normalizes logo elements (`imageType: "logo"`) to stay within
|
|
126
|
+
top margins and removes logo clipping.
|
|
127
|
+
- `applyCustomTheme` inserts scoped background images as slide elements with
|
|
128
|
+
`imageType: "background"` and logo images with `imageType: "logo"`.
|
|
129
|
+
- When both `backgroundImage` and `backgroundColor` are provided, background color is
|
|
130
|
+
applied as a 50% alpha overlay color on targeted slides.
|
|
131
|
+
- `clearBackgroundImage` also clears logo images in current behavior.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# pptx-custom
|
|
2
|
+
|
|
3
|
+
[English](./README.md)
|
|
4
|
+
|
|
5
|
+
用于对 `json2pptx` JSON deck 进行两阶段定制:
|
|
6
|
+
|
|
7
|
+
- 内容阶段:将后端内容映射到模板 deck。
|
|
8
|
+
- 主题阶段:替换主题色/字体/背景,并按范围应用媒体资源。
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i pptx-custom
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 导出 API
|
|
17
|
+
|
|
18
|
+
- `applyCustomContent(template, input)`
|
|
19
|
+
- `parseCustomContent(raw)`
|
|
20
|
+
- `applyCustomContentToTemplate(template, slides)`
|
|
21
|
+
- `applyCustomTheme(deck, themeInput)`
|
|
22
|
+
|
|
23
|
+
同时导出类型,包括:
|
|
24
|
+
`CustomSlide`、`Deck`、`PptxCustomContentInput`、`PptxCustomThemeInput`、
|
|
25
|
+
`PptxCustomOptions`、`TemplateJson`、`TemplateJsonSlide`、`TemplateJsonElement`、
|
|
26
|
+
`TemplateJsonTheme`。
|
|
27
|
+
|
|
28
|
+
## 快速开始
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import {
|
|
32
|
+
applyCustomContent,
|
|
33
|
+
applyCustomTheme,
|
|
34
|
+
parseCustomContent,
|
|
35
|
+
applyCustomContentToTemplate
|
|
36
|
+
} from 'pptx-custom'
|
|
37
|
+
|
|
38
|
+
const withContent = applyCustomContent(templateDeck, backendText)
|
|
39
|
+
|
|
40
|
+
const withTheme = applyCustomTheme(withContent, {
|
|
41
|
+
themeColors: ['#111111', '#333333', '#555555', '#777777', '#999999', '#BBBBBB'],
|
|
42
|
+
fontColor: '#222222',
|
|
43
|
+
backgroundColor: '#FFFFFF',
|
|
44
|
+
backgroundImage: {
|
|
45
|
+
src: 'https://example.com/background.png',
|
|
46
|
+
scope: {
|
|
47
|
+
cover: false,
|
|
48
|
+
contents: true,
|
|
49
|
+
transition: true,
|
|
50
|
+
content: true,
|
|
51
|
+
end: false
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
logoImage: {
|
|
55
|
+
src: 'https://example.com/logo.png',
|
|
56
|
+
position: 'right',
|
|
57
|
+
scope: {
|
|
58
|
+
cover: true,
|
|
59
|
+
contents: true,
|
|
60
|
+
transition: true,
|
|
61
|
+
content: true,
|
|
62
|
+
end: true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const slides = parseCustomContent(backendText)
|
|
68
|
+
const withContentDirect = applyCustomContentToTemplate(templateDeck, slides)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 自定义内容输入格式
|
|
72
|
+
|
|
73
|
+
`parseCustomContent` 和 `applyCustomContent` 支持:
|
|
74
|
+
|
|
75
|
+
1. NDJSON(每行一个 slide)
|
|
76
|
+
2. JSON slide 数组
|
|
77
|
+
3. 带 `slides` 字段的 JSON 对象
|
|
78
|
+
4. 单个 slide JSON 对象(包含 `type`)
|
|
79
|
+
|
|
80
|
+
支持的 slide 类型:
|
|
81
|
+
|
|
82
|
+
- `cover`
|
|
83
|
+
- `contents`
|
|
84
|
+
- `transition`
|
|
85
|
+
- `content`
|
|
86
|
+
- `end`
|
|
87
|
+
|
|
88
|
+
兼容的历史别名:
|
|
89
|
+
|
|
90
|
+
- `agenda` -> `contents`
|
|
91
|
+
- `section` -> `transition`
|
|
92
|
+
- `ending` -> `end`
|
|
93
|
+
|
|
94
|
+
NDJSON 示例:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{"type":"cover","data":{"title":"Title","text":"Subtitle"}}
|
|
98
|
+
{"type":"contents","data":{"items":["Part A","Part B"]}}
|
|
99
|
+
{"type":"transition","data":{"title":"Part A","text":"Section intro"}}
|
|
100
|
+
{"type":"content","data":{"title":"Topic","items":[{"title":"Point","text":"Detail"}]}}
|
|
101
|
+
{"type":"end"}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 主题输入
|
|
105
|
+
|
|
106
|
+
`applyCustomTheme` 接收 `PptxCustomThemeInput`:
|
|
107
|
+
|
|
108
|
+
- `themeColors: string[]`(最多使用前 6 个)
|
|
109
|
+
- `fontColor: string`
|
|
110
|
+
- `backgroundColor?: string`
|
|
111
|
+
- `backgroundImage?: { src, scope, width?, height? }`
|
|
112
|
+
- `logoImage?: { src, scope, position, width?, height? }`
|
|
113
|
+
- `clearBackgroundImage?: boolean`
|
|
114
|
+
- `clearLogoImage?: boolean`
|
|
115
|
+
|
|
116
|
+
`scope` 可选键:
|
|
117
|
+
`cover | contents | transition | content | end`
|
|
118
|
+
|
|
119
|
+
## 行为说明
|
|
120
|
+
|
|
121
|
+
- `applyCustomContent` 与 `applyCustomTheme` 都会在返回前经过
|
|
122
|
+
`json2pptx-schema` 的解析与规范化。
|
|
123
|
+
- `applyCustomContent` 按 `type` 选模板页;对 `contents/content` 会优先选择
|
|
124
|
+
与输入条目数容量最接近的布局。
|
|
125
|
+
- `applyCustomContent` 会规范化 logo 元素(`imageType: "logo"`):
|
|
126
|
+
保持在顶部边距内,并移除 logo 裁剪配置。
|
|
127
|
+
- `applyCustomTheme` 会将背景图以元素形式写入(`imageType: "background"`),
|
|
128
|
+
并将 logo 以 `imageType: "logo"` 写入。
|
|
129
|
+
- 当同时提供 `backgroundImage` 与 `backgroundColor` 时,会在目标页应用
|
|
130
|
+
50% 透明度的背景色叠加效果。
|
|
131
|
+
- 当前行为下,`clearBackgroundImage` 也会同时清除 logo 图片。
|
package/dist/index.js
CHANGED
|
@@ -453,6 +453,187 @@ function findMappingForColor(value, mappings) {
|
|
|
453
453
|
);
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
// src/custom-theme/media.ts
|
|
457
|
+
var FALLBACK_SLIDE_WIDTH2 = 1e3;
|
|
458
|
+
var FALLBACK_SLIDE_HEIGHT2 = 562.5;
|
|
459
|
+
var LOGO_MARGIN_X2 = 24;
|
|
460
|
+
var LOGO_MARGIN_Y2 = 18;
|
|
461
|
+
var LOGO_MAX_WIDTH_RATIO2 = 0.34;
|
|
462
|
+
var LOGO_MAX_HEIGHT_RATIO2 = 0.16;
|
|
463
|
+
var DEFAULT_LOGO_ASPECT_RATIO = 3.2;
|
|
464
|
+
function mapSlideTypeToScopeKey(type) {
|
|
465
|
+
const normalized = type?.trim().toLowerCase();
|
|
466
|
+
if (!normalized) return null;
|
|
467
|
+
if (normalized === "cover") return "cover";
|
|
468
|
+
if (normalized === "contents" || normalized === "agenda") return "contents";
|
|
469
|
+
if (normalized === "transition" || normalized === "section") return "transition";
|
|
470
|
+
if (normalized === "content") return "content";
|
|
471
|
+
if (normalized === "end" || normalized === "ending") return "end";
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
function isSlideInScope(slide, scope) {
|
|
475
|
+
const key = mapSlideTypeToScopeKey(slide.type);
|
|
476
|
+
if (!key) return false;
|
|
477
|
+
return Boolean(scope[key]);
|
|
478
|
+
}
|
|
479
|
+
function createElementId(prefix, slideIndex) {
|
|
480
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
481
|
+
return `${prefix}-${slideIndex}-${random}`;
|
|
482
|
+
}
|
|
483
|
+
function toHalfOpacityColor(value) {
|
|
484
|
+
const rgb = parseColorToRgb(value);
|
|
485
|
+
if (!rgb) return value;
|
|
486
|
+
return `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)`;
|
|
487
|
+
}
|
|
488
|
+
function buildBackgroundElement(input) {
|
|
489
|
+
const { src, slideWidth, slideHeight, slideIndex } = input;
|
|
490
|
+
return {
|
|
491
|
+
type: "image",
|
|
492
|
+
id: createElementId("background", slideIndex),
|
|
493
|
+
src,
|
|
494
|
+
width: slideWidth,
|
|
495
|
+
height: slideHeight,
|
|
496
|
+
left: 0,
|
|
497
|
+
top: 0,
|
|
498
|
+
fixedRatio: true,
|
|
499
|
+
rotate: 0,
|
|
500
|
+
imageType: "background",
|
|
501
|
+
filters: {
|
|
502
|
+
opacity: "100%"
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function resolveLogoAspectRatio(width, height) {
|
|
507
|
+
if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) {
|
|
508
|
+
return width / height;
|
|
509
|
+
}
|
|
510
|
+
return DEFAULT_LOGO_ASPECT_RATIO;
|
|
511
|
+
}
|
|
512
|
+
function resolveLogoSize(input) {
|
|
513
|
+
const {
|
|
514
|
+
slideWidth,
|
|
515
|
+
slideHeight,
|
|
516
|
+
logoWidth,
|
|
517
|
+
logoHeight
|
|
518
|
+
} = input;
|
|
519
|
+
const aspectRatio = resolveLogoAspectRatio(logoWidth, logoHeight);
|
|
520
|
+
const maxWidth = slideWidth * LOGO_MAX_WIDTH_RATIO2;
|
|
521
|
+
const maxHeight = slideHeight * LOGO_MAX_HEIGHT_RATIO2;
|
|
522
|
+
let width = maxWidth;
|
|
523
|
+
let height = width / aspectRatio;
|
|
524
|
+
if (height > maxHeight) {
|
|
525
|
+
height = maxHeight;
|
|
526
|
+
width = height * aspectRatio;
|
|
527
|
+
}
|
|
528
|
+
return { width, height };
|
|
529
|
+
}
|
|
530
|
+
function buildLogoElement(input) {
|
|
531
|
+
const { src, slideWidth, slideIndex, position } = input;
|
|
532
|
+
const { width, height } = resolveLogoSize(input);
|
|
533
|
+
const left = position === "left" ? LOGO_MARGIN_X2 : Math.max(LOGO_MARGIN_X2, slideWidth - width - LOGO_MARGIN_X2);
|
|
534
|
+
return {
|
|
535
|
+
type: "image",
|
|
536
|
+
id: createElementId("logo", slideIndex),
|
|
537
|
+
src,
|
|
538
|
+
width,
|
|
539
|
+
height,
|
|
540
|
+
left,
|
|
541
|
+
top: LOGO_MARGIN_Y2,
|
|
542
|
+
fixedRatio: true,
|
|
543
|
+
rotate: 0,
|
|
544
|
+
imageType: "logo"
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function replaceScopedMedia(deck, input) {
|
|
548
|
+
if (!deck.slides?.length) return deck;
|
|
549
|
+
const slideWidth = deck.width ?? FALLBACK_SLIDE_WIDTH2;
|
|
550
|
+
const slideHeight = deck.height ?? FALLBACK_SLIDE_HEIGHT2;
|
|
551
|
+
const backgroundInput = input.backgroundImage;
|
|
552
|
+
const logoInput = input.logoImage;
|
|
553
|
+
const shouldClearBackground = Boolean(input.clearBackgroundImage);
|
|
554
|
+
const shouldClearLogo = Boolean(input.clearLogoImage || shouldClearBackground);
|
|
555
|
+
if (!backgroundInput?.src && !logoInput?.src && !shouldClearBackground && !shouldClearLogo) {
|
|
556
|
+
return deck;
|
|
557
|
+
}
|
|
558
|
+
const slides = deck.slides.map((slide, slideIndex) => {
|
|
559
|
+
let nextElements = slide.elements ? [...slide.elements] : [];
|
|
560
|
+
let nextBackground = slide.background;
|
|
561
|
+
let changed = false;
|
|
562
|
+
if (shouldClearBackground) {
|
|
563
|
+
const before = nextElements.length;
|
|
564
|
+
nextElements = nextElements.filter(
|
|
565
|
+
(element) => !(element.type === "image" && element.imageType === "background")
|
|
566
|
+
);
|
|
567
|
+
if (nextElements.length !== before) {
|
|
568
|
+
changed = true;
|
|
569
|
+
if (input.backgroundColor) {
|
|
570
|
+
nextBackground = {
|
|
571
|
+
...nextBackground ?? {},
|
|
572
|
+
type: "solid",
|
|
573
|
+
color: input.backgroundColor
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (shouldClearLogo) {
|
|
579
|
+
const before = nextElements.length;
|
|
580
|
+
nextElements = nextElements.filter(
|
|
581
|
+
(element) => !(element.type === "image" && element.imageType === "logo")
|
|
582
|
+
);
|
|
583
|
+
if (nextElements.length !== before) {
|
|
584
|
+
changed = true;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (backgroundInput?.src && isSlideInScope(slide, backgroundInput.scope)) {
|
|
588
|
+
const withoutBackground = nextElements.filter(
|
|
589
|
+
(element) => !(element.type === "image" && element.imageType === "background")
|
|
590
|
+
);
|
|
591
|
+
nextElements = [
|
|
592
|
+
buildBackgroundElement({
|
|
593
|
+
src: backgroundInput.src,
|
|
594
|
+
slideWidth,
|
|
595
|
+
slideHeight,
|
|
596
|
+
slideIndex
|
|
597
|
+
}),
|
|
598
|
+
...withoutBackground
|
|
599
|
+
];
|
|
600
|
+
changed = true;
|
|
601
|
+
if (input.backgroundColor) {
|
|
602
|
+
nextBackground = {
|
|
603
|
+
...nextBackground ?? {},
|
|
604
|
+
type: "solid",
|
|
605
|
+
color: toHalfOpacityColor(input.backgroundColor)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (logoInput?.src && isSlideInScope(slide, logoInput.scope)) {
|
|
610
|
+
const withoutLogo = nextElements.filter(
|
|
611
|
+
(element) => !(element.type === "image" && element.imageType === "logo")
|
|
612
|
+
);
|
|
613
|
+
nextElements = [
|
|
614
|
+
...withoutLogo,
|
|
615
|
+
buildLogoElement({
|
|
616
|
+
src: logoInput.src,
|
|
617
|
+
slideWidth,
|
|
618
|
+
slideHeight,
|
|
619
|
+
slideIndex,
|
|
620
|
+
position: logoInput.position,
|
|
621
|
+
logoWidth: logoInput.width,
|
|
622
|
+
logoHeight: logoInput.height
|
|
623
|
+
})
|
|
624
|
+
];
|
|
625
|
+
changed = true;
|
|
626
|
+
}
|
|
627
|
+
if (!changed && nextBackground === slide.background) return slide;
|
|
628
|
+
return {
|
|
629
|
+
...slide,
|
|
630
|
+
...changed ? { elements: nextElements } : {},
|
|
631
|
+
...nextBackground ? { background: nextBackground } : {}
|
|
632
|
+
};
|
|
633
|
+
});
|
|
634
|
+
return { ...deck, slides };
|
|
635
|
+
}
|
|
636
|
+
|
|
456
637
|
// src/custom-theme/mappings.ts
|
|
457
638
|
function buildColorMappings(previous, next) {
|
|
458
639
|
const mappings = [];
|
|
@@ -631,173 +812,6 @@ function applyTheme2Json(deck, update) {
|
|
|
631
812
|
}
|
|
632
813
|
|
|
633
814
|
// src/custom-theme/index.ts
|
|
634
|
-
var FALLBACK_SLIDE_WIDTH2 = 1e3;
|
|
635
|
-
var FALLBACK_SLIDE_HEIGHT2 = 562.5;
|
|
636
|
-
var LOGO_MARGIN_X2 = 24;
|
|
637
|
-
var LOGO_MARGIN_Y2 = 18;
|
|
638
|
-
var LOGO_MAX_WIDTH_RATIO2 = 0.34;
|
|
639
|
-
var LOGO_MAX_HEIGHT_RATIO2 = 0.16;
|
|
640
|
-
var DEFAULT_LOGO_ASPECT_RATIO = 3.2;
|
|
641
|
-
function mapSlideTypeToScopeKey(type) {
|
|
642
|
-
const normalized = type?.trim().toLowerCase();
|
|
643
|
-
if (!normalized) return null;
|
|
644
|
-
if (normalized === "cover") return "cover";
|
|
645
|
-
if (normalized === "contents" || normalized === "agenda") return "contents";
|
|
646
|
-
if (normalized === "transition" || normalized === "section") return "transition";
|
|
647
|
-
if (normalized === "content") return "content";
|
|
648
|
-
if (normalized === "end" || normalized === "ending") return "end";
|
|
649
|
-
return null;
|
|
650
|
-
}
|
|
651
|
-
function isSlideInScope(slide, scope) {
|
|
652
|
-
const key = mapSlideTypeToScopeKey(slide.type);
|
|
653
|
-
if (!key) return false;
|
|
654
|
-
return Boolean(scope[key]);
|
|
655
|
-
}
|
|
656
|
-
function createElementId(prefix, slideIndex) {
|
|
657
|
-
const random = Math.random().toString(36).slice(2, 10);
|
|
658
|
-
return `${prefix}-${slideIndex}-${random}`;
|
|
659
|
-
}
|
|
660
|
-
function toHalfOpacityColor(value) {
|
|
661
|
-
const rgb = parseColorToRgb(value);
|
|
662
|
-
if (!rgb) return value;
|
|
663
|
-
return `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)`;
|
|
664
|
-
}
|
|
665
|
-
function buildBackgroundElement(src, slideWidth, slideHeight, slideIndex) {
|
|
666
|
-
return {
|
|
667
|
-
type: "image",
|
|
668
|
-
id: createElementId("background", slideIndex),
|
|
669
|
-
src,
|
|
670
|
-
width: slideWidth,
|
|
671
|
-
height: slideHeight,
|
|
672
|
-
left: 0,
|
|
673
|
-
top: 0,
|
|
674
|
-
fixedRatio: true,
|
|
675
|
-
rotate: 0,
|
|
676
|
-
imageType: "background",
|
|
677
|
-
filters: {
|
|
678
|
-
opacity: "100%"
|
|
679
|
-
}
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
function buildLogoElement(src, slideWidth, slideHeight, position, slideIndex, logoWidth, logoHeight) {
|
|
683
|
-
const aspectRatio = resolveLogoAspectRatio(logoWidth, logoHeight);
|
|
684
|
-
const maxWidth = slideWidth * LOGO_MAX_WIDTH_RATIO2;
|
|
685
|
-
const maxHeight = slideHeight * LOGO_MAX_HEIGHT_RATIO2;
|
|
686
|
-
let width = maxWidth;
|
|
687
|
-
let height = width / aspectRatio;
|
|
688
|
-
if (height > maxHeight) {
|
|
689
|
-
height = maxHeight;
|
|
690
|
-
width = height * aspectRatio;
|
|
691
|
-
}
|
|
692
|
-
const left = position === "left" ? LOGO_MARGIN_X2 : Math.max(LOGO_MARGIN_X2, slideWidth - width - LOGO_MARGIN_X2);
|
|
693
|
-
return {
|
|
694
|
-
type: "image",
|
|
695
|
-
id: createElementId("logo", slideIndex),
|
|
696
|
-
src,
|
|
697
|
-
width,
|
|
698
|
-
height,
|
|
699
|
-
left,
|
|
700
|
-
top: LOGO_MARGIN_Y2,
|
|
701
|
-
fixedRatio: true,
|
|
702
|
-
rotate: 0,
|
|
703
|
-
imageType: "logo"
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
function resolveLogoAspectRatio(width, height) {
|
|
707
|
-
if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) {
|
|
708
|
-
return width / height;
|
|
709
|
-
}
|
|
710
|
-
return DEFAULT_LOGO_ASPECT_RATIO;
|
|
711
|
-
}
|
|
712
|
-
function replaceScopedMedia(deck, input) {
|
|
713
|
-
if (!deck.slides?.length) return deck;
|
|
714
|
-
const slideWidth = deck.width ?? FALLBACK_SLIDE_WIDTH2;
|
|
715
|
-
const slideHeight = deck.height ?? FALLBACK_SLIDE_HEIGHT2;
|
|
716
|
-
const backgroundInput = input.backgroundImage;
|
|
717
|
-
const logoInput = input.logoImage;
|
|
718
|
-
const shouldClearBackground = Boolean(input.clearBackgroundImage);
|
|
719
|
-
const shouldClearLogo = Boolean(input.clearLogoImage || shouldClearBackground);
|
|
720
|
-
if (!backgroundInput?.src && !logoInput?.src && !shouldClearBackground && !shouldClearLogo) {
|
|
721
|
-
return deck;
|
|
722
|
-
}
|
|
723
|
-
const slides = deck.slides.map((slide, slideIndex) => {
|
|
724
|
-
let nextElements = slide.elements ? [...slide.elements] : [];
|
|
725
|
-
let nextBackground = slide.background;
|
|
726
|
-
let changed = false;
|
|
727
|
-
if (shouldClearBackground) {
|
|
728
|
-
const before = nextElements.length;
|
|
729
|
-
nextElements = nextElements.filter(
|
|
730
|
-
(element) => !(element.type === "image" && element.imageType === "background")
|
|
731
|
-
);
|
|
732
|
-
if (nextElements.length !== before) {
|
|
733
|
-
changed = true;
|
|
734
|
-
if (input.backgroundColor) {
|
|
735
|
-
nextBackground = {
|
|
736
|
-
...nextBackground ?? {},
|
|
737
|
-
type: "solid",
|
|
738
|
-
color: input.backgroundColor
|
|
739
|
-
};
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
if (shouldClearLogo) {
|
|
744
|
-
const before = nextElements.length;
|
|
745
|
-
nextElements = nextElements.filter(
|
|
746
|
-
(element) => !(element.type === "image" && element.imageType === "logo")
|
|
747
|
-
);
|
|
748
|
-
if (nextElements.length !== before) {
|
|
749
|
-
changed = true;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
if (backgroundInput?.src && isSlideInScope(slide, backgroundInput.scope)) {
|
|
753
|
-
const withoutBackground = nextElements.filter(
|
|
754
|
-
(element) => !(element.type === "image" && element.imageType === "background")
|
|
755
|
-
);
|
|
756
|
-
nextElements = [
|
|
757
|
-
buildBackgroundElement(
|
|
758
|
-
backgroundInput.src,
|
|
759
|
-
slideWidth,
|
|
760
|
-
slideHeight,
|
|
761
|
-
slideIndex
|
|
762
|
-
),
|
|
763
|
-
...withoutBackground
|
|
764
|
-
];
|
|
765
|
-
changed = true;
|
|
766
|
-
if (input.backgroundColor) {
|
|
767
|
-
nextBackground = {
|
|
768
|
-
...nextBackground ?? {},
|
|
769
|
-
type: "solid",
|
|
770
|
-
color: toHalfOpacityColor(input.backgroundColor)
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
if (logoInput?.src && isSlideInScope(slide, logoInput.scope)) {
|
|
775
|
-
const withoutLogo = nextElements.filter(
|
|
776
|
-
(element) => !(element.type === "image" && element.imageType === "logo")
|
|
777
|
-
);
|
|
778
|
-
nextElements = [
|
|
779
|
-
...withoutLogo,
|
|
780
|
-
buildLogoElement(
|
|
781
|
-
logoInput.src,
|
|
782
|
-
slideWidth,
|
|
783
|
-
slideHeight,
|
|
784
|
-
logoInput.position,
|
|
785
|
-
slideIndex,
|
|
786
|
-
logoInput.width,
|
|
787
|
-
logoInput.height
|
|
788
|
-
)
|
|
789
|
-
];
|
|
790
|
-
changed = true;
|
|
791
|
-
}
|
|
792
|
-
if (!changed && nextBackground === slide.background) return slide;
|
|
793
|
-
return {
|
|
794
|
-
...slide,
|
|
795
|
-
...changed ? { elements: nextElements } : {},
|
|
796
|
-
...nextBackground ? { background: nextBackground } : {}
|
|
797
|
-
};
|
|
798
|
-
});
|
|
799
|
-
return { ...deck, slides };
|
|
800
|
-
}
|
|
801
815
|
function applyCustomTheme(deck, input) {
|
|
802
816
|
const normalizedDeck = (0, import_json2pptx_schema2.parseDocument)(deck);
|
|
803
817
|
const withColors = applyTheme2Json(normalizedDeck, input);
|