tikzify 0.0.7 → 0.0.9
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/package.json +1 -1
- package/src/book.ts +36 -198
- package/src/common.ts +172 -0
- package/src/cover.ts +164 -0
- package/src/server.ts +68 -30
package/package.json
CHANGED
package/src/book.ts
CHANGED
|
@@ -1,131 +1,12 @@
|
|
|
1
1
|
import assert from 'assert'
|
|
2
|
-
import sax from 'sax'
|
|
3
2
|
import z from 'zod'
|
|
3
|
+
import { colorMap, defineColors, Emoji, fromSvg, getColors, gradient, svgTex } from './common'
|
|
4
4
|
import { emojiMap } from './emojis'
|
|
5
5
|
|
|
6
|
-
type Path = z.infer<typeof Path>
|
|
7
|
-
const Path = z.object({
|
|
8
|
-
type: z.literal("path"),
|
|
9
|
-
d: z.string(),
|
|
10
|
-
fill: z.string().optional(),
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
type Circle = z.infer<typeof Circle>
|
|
14
|
-
const Circle = z.object({
|
|
15
|
-
type: z.literal("circle"),
|
|
16
|
-
cx: z.number(),
|
|
17
|
-
cy: z.number(),
|
|
18
|
-
r: z.number(),
|
|
19
|
-
fill: z.string().optional(),
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
type Group = z.infer<typeof Group>
|
|
23
|
-
const Group = z.object({
|
|
24
|
-
type: z.literal("g"),
|
|
25
|
-
fill: z.string().optional(),
|
|
26
|
-
get kids() {
|
|
27
|
-
return z.array(Element)
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
type types = Element["type"]
|
|
34
|
-
type Element = z.infer<typeof Element>
|
|
35
|
-
const Element = z.union([Group, Path, Circle])
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
type Emoji = z.infer<typeof Emoji>
|
|
39
|
-
const Emoji = z.object({
|
|
40
|
-
x: z.number(),
|
|
41
|
-
y: z.number(),
|
|
42
|
-
scale: z.number(),
|
|
43
|
-
rotate: z.number(),
|
|
44
|
-
emoji: z.string()
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
type Open = {
|
|
48
|
-
[t in types]: (args: Omit<Extract<Element, { type: t }>, "type">) => void
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
type Close = {
|
|
52
|
-
[t in types]: () => void
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function hex(s: string | undefined): string | undefined {
|
|
56
|
-
if (!s) return
|
|
57
|
-
const res = s.replace('#', '')
|
|
58
|
-
assert([3, 6].includes(res.length), `Color ${s} is not valid hex`)
|
|
59
|
-
return res.length === 3 ? res.split('').map(c => c + c).join('') : res
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function fromSvg(svg: string): Group[] {
|
|
63
|
-
|
|
64
|
-
const res: Group[] = [{
|
|
65
|
-
type: "g",
|
|
66
|
-
kids: [],
|
|
67
|
-
}]
|
|
68
|
-
|
|
69
|
-
function push(e: Element) {
|
|
70
|
-
res[res.length - 1]?.kids.push({ ...e, fill: hex(e.fill) })
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const open: Open = {
|
|
74
|
-
circle(attrs) {
|
|
75
|
-
push({ type: "circle", ...attrs })
|
|
76
|
-
},
|
|
77
|
-
path(attrs) {
|
|
78
|
-
push({ type: "path", ...attrs })
|
|
79
|
-
},
|
|
80
|
-
g({ fill }) {
|
|
81
|
-
res.push({
|
|
82
|
-
type: "g",
|
|
83
|
-
fill: hex(fill),
|
|
84
|
-
kids: [],
|
|
85
|
-
})
|
|
86
|
-
},
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const close: Close = {
|
|
90
|
-
circle() { },
|
|
91
|
-
path() { },
|
|
92
|
-
g() {
|
|
93
|
-
res.push({
|
|
94
|
-
type: "g",
|
|
95
|
-
kids: [],
|
|
96
|
-
})
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const p = sax.parser(true)
|
|
101
|
-
|
|
102
|
-
p.onopentag = ({ name, attributes }) => {
|
|
103
|
-
if (!(name in open)) {
|
|
104
|
-
console.log('unknown tag', name)
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
open[name as types](attributes as any)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
p.onclosetag = (name) => {
|
|
111
|
-
if (!(name in close)) {
|
|
112
|
-
console.log('unknown tag', name)
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
close[name as types]()
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
p.write(svg).close()
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return res
|
|
123
|
-
}
|
|
124
|
-
|
|
125
6
|
|
|
126
7
|
type Page = z.infer<typeof Page>
|
|
127
8
|
const Page = z.object({
|
|
128
|
-
gradient:
|
|
9
|
+
gradient: gradient,
|
|
129
10
|
textBg: z.string(),
|
|
130
11
|
text: z.array(z.string()),
|
|
131
12
|
emojis: z.object({
|
|
@@ -141,17 +22,7 @@ export const Book = z.object({
|
|
|
141
22
|
pages: z.array(Page),
|
|
142
23
|
})
|
|
143
24
|
|
|
144
|
-
function
|
|
145
|
-
colors.delete(undefined)
|
|
146
|
-
return Object.fromEntries(Array.from(colors).filter((c): c is string => c !== undefined).map((c, i) => [c, i]))
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function getColors(e: Element): (string | undefined)[] {
|
|
150
|
-
if (e.type === 'g') return [e.fill, ...e.kids.flatMap(getColors)]
|
|
151
|
-
return [e.fill]
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function toTex(book: Book) {
|
|
25
|
+
export function bookTex(book: Book) {
|
|
155
26
|
const pages = book.pages
|
|
156
27
|
|
|
157
28
|
const emojis = Object.fromEntries(
|
|
@@ -173,34 +44,6 @@ export function toTex(book: Book) {
|
|
|
173
44
|
...emojiColors
|
|
174
45
|
]))
|
|
175
46
|
|
|
176
|
-
type To = {
|
|
177
|
-
[t in types]: (args: Extract<Element, { type: t }>) => string
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function fillStr(fill?: string) {
|
|
181
|
-
if (!fill) return ''
|
|
182
|
-
assert(fill in colors, `Color ${fill} not in ${Object.keys(colors)}`)
|
|
183
|
-
return `fill=c${colors[fill]}`
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const ToTikz: To = {
|
|
187
|
-
circle({ cx, cy, r, fill }) {
|
|
188
|
-
return `\\fill[${fillStr(fill)}] (${cx}, ${cy}) circle (${r});`
|
|
189
|
-
},
|
|
190
|
-
|
|
191
|
-
path({ d, fill }) {
|
|
192
|
-
return `\\fill[${fillStr(fill)}] svg {${d}};`
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
g({ fill, kids }) {
|
|
196
|
-
return [
|
|
197
|
-
`\\begin{scope}[${fillStr(fill)}]`,
|
|
198
|
-
...kids.map(e => ToTikz[e.type](e as any)),
|
|
199
|
-
`\\end{scope}`
|
|
200
|
-
].join('\n')
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
47
|
return String.raw`
|
|
205
48
|
\documentclass[a5paper, oneside]{article}
|
|
206
49
|
\usepackage[margin=0cm,bottom=2cm]{geometry}
|
|
@@ -210,21 +53,21 @@ export function toTex(book: Book) {
|
|
|
210
53
|
\usepackage{fancyhdr}
|
|
211
54
|
|
|
212
55
|
\fancypagestyle{bigpagenumbers}{
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
56
|
+
\fancyhf{}
|
|
57
|
+
\renewcommand{\headrulewidth}{0pt}
|
|
58
|
+
\fancyfoot[C]{\Huge\thepage}
|
|
216
59
|
}
|
|
217
60
|
|
|
218
61
|
\usepackage{polyglossia}
|
|
219
62
|
\setmainlanguage{hebrew}
|
|
220
63
|
\newfontfamily\hebrewfont[
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
64
|
+
Script=Hebrew,
|
|
65
|
+
Path=./,
|
|
66
|
+
BoldFont={Fredoka-Bold.ttf}
|
|
224
67
|
]{Fredoka-Bold.ttf}
|
|
225
68
|
|
|
226
69
|
|
|
227
|
-
${
|
|
70
|
+
${defineColors(colors)}
|
|
228
71
|
|
|
229
72
|
\begin{document}
|
|
230
73
|
|
|
@@ -241,13 +84,8 @@ ${book.pages.map((page, i) => {
|
|
|
241
84
|
function es(es: Emoji[]) {
|
|
242
85
|
return es.map(({ emoji, x, y, scale, rotate }) => {
|
|
243
86
|
assert(emoji && emoji in emojis, `Emoji ${emoji} not found`)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
\begin{scope}[x=1pt, y=1pt, xshift=${x}, scale=${scale}, yscale=-1, yshift=${y}, rotate=${rotate}]
|
|
247
|
-
${emojis[emoji]?.emoji.map(e => {
|
|
248
|
-
return ToTikz[e.type](e as any)
|
|
249
|
-
}).join('\n')}
|
|
250
|
-
\end{scope}`}).join('\n')
|
|
87
|
+
return svgTex({ x, y, scale, rotate }, emojis[emoji]!.emoji, colors)
|
|
88
|
+
}).join('\n')
|
|
251
89
|
}
|
|
252
90
|
|
|
253
91
|
const esText = es(page.emojis.text)
|
|
@@ -264,40 +102,40 @@ ${book.pages.map((page, i) => {
|
|
|
264
102
|
\begin{tikzpicture}
|
|
265
103
|
\begin{scope}
|
|
266
104
|
\clip[
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
105
|
+
xshift=-190,
|
|
106
|
+
yshift=190,
|
|
107
|
+
scale=380,
|
|
108
|
+
yscale=-1] svg {M 0.97 0.37 C 0.95 0.26 0.94 0.11 0.85 0.05 C 0.76 0.00 0.54 0.02 0.41 0.05 C 0.28 0.08 0.12 0.10 0.06 0.24 C 0.00 0.37 0.00 0.73 0.05 0.85 C 0.11 0.97 0.26 0.94 0.39 0.96 C 0.51 0.98 0.71 1.00 0.80 0.96 C 0.90 0.92 0.94 0.82 0.97 0.72 C 1.00 0.62 0.99 0.48 0.97 0.37 C 0.95 0.26 0.94 0.11 0.85 0.05};
|
|
109
|
+
\node[opacity=0.8] {\includegraphics[width=13.1cm]{${i}.jpg}};
|
|
110
|
+
\end{scope}
|
|
111
|
+
${esImage}
|
|
112
|
+
\end{tikzpicture}
|
|
275
113
|
\vspace*{\fill}
|
|
276
114
|
\newpage
|
|
277
115
|
`
|
|
278
116
|
|
|
279
117
|
const text = String.raw`
|
|
280
118
|
\begin{tikzpicture}[remember picture, overlay]
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
119
|
+
\shade[shading=axis, bottom color=c${colors[c1]}, top color=c${colors[c2]}, shading angle=45]
|
|
120
|
+
(current page.south west) rectangle ([xshift=148.5mm]current page.north east);
|
|
121
|
+
\fill[
|
|
122
|
+
opacity=0.5,
|
|
123
|
+
color=c${colors[page.textBg]},
|
|
124
|
+
yshift=-20,
|
|
125
|
+
xscale=380,
|
|
126
|
+
yscale=-500,
|
|
127
|
+
] svg {M 0.97 0.37 C 0.95 0.26 0.94 0.11 0.85 0.05 C 0.76 0.00 0.54 0.02 0.41 0.05 C 0.28 0.08 0.12 0.10 0.06 0.24 C 0.00 0.37 0.00 0.73 0.05 0.85 C 0.11 0.97 0.26 0.94 0.39 0.96 C 0.51 0.98 0.71 1.00 0.80 0.96 C 0.90 0.92 0.94 0.82 0.97 0.72 C 1.00 0.62 0.99 0.48 0.97 0.37 C 0.95 0.26 0.94 0.11 0.85 0.05};
|
|
128
|
+
|
|
129
|
+
${esText}
|
|
292
130
|
\end{tikzpicture}
|
|
293
131
|
|
|
294
132
|
\vspace*{\fill}
|
|
295
133
|
\begin{center}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
134
|
+
\begin{minipage}{10cm}
|
|
135
|
+
\Huge
|
|
136
|
+
\raggedleft
|
|
137
|
+
${page.text.map(line => line.trim()).join('\\\\')}
|
|
138
|
+
\end{minipage}
|
|
301
139
|
\end{center}
|
|
302
140
|
\vspace*{\fill}
|
|
303
141
|
\newpage
|
package/src/common.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import sax from 'sax'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
export const gradient = z.array(z.string()).length(2)
|
|
6
|
+
|
|
7
|
+
export type Transform = z.infer<typeof Transform>
|
|
8
|
+
export const Transform = z.object({
|
|
9
|
+
x: z.number(),
|
|
10
|
+
y: z.number(),
|
|
11
|
+
scale: z.number(),
|
|
12
|
+
rotate: z.number(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export type Emoji = z.infer<typeof Emoji>
|
|
16
|
+
export const Emoji = Transform.extend({
|
|
17
|
+
emoji: z.string().max(8)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
type Path = z.infer<typeof Path>
|
|
21
|
+
const Path = z.object({
|
|
22
|
+
type: z.literal("path"),
|
|
23
|
+
d: z.string(),
|
|
24
|
+
fill: z.string().optional(),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
type Circle = z.infer<typeof Circle>
|
|
28
|
+
const Circle = z.object({
|
|
29
|
+
type: z.literal("circle"),
|
|
30
|
+
cx: z.number(),
|
|
31
|
+
cy: z.number(),
|
|
32
|
+
r: z.number(),
|
|
33
|
+
fill: z.string().optional(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
type Group = z.infer<typeof Group>
|
|
37
|
+
const Group = z.object({
|
|
38
|
+
type: z.literal("g"),
|
|
39
|
+
fill: z.string().optional(),
|
|
40
|
+
get kids() {
|
|
41
|
+
return z.array(Element)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
type types = Element["type"]
|
|
46
|
+
type Element = z.infer<typeof Element>
|
|
47
|
+
const Element = z.union([Group, Path, Circle])
|
|
48
|
+
|
|
49
|
+
type Open = {
|
|
50
|
+
[t in types]: (args: Omit<Extract<Element, { type: t }>, "type">) => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Close = {
|
|
54
|
+
[t in types]: () => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hex(s: string | undefined): string | undefined {
|
|
58
|
+
if (!s) return
|
|
59
|
+
const res = s.replace('#', '')
|
|
60
|
+
assert([3, 6].includes(res.length), `Color ${s} is not valid hex`)
|
|
61
|
+
return res.length === 3 ? res.split('').map(c => c + c).join('') : res
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function fromSvg(svg: string): Group[] {
|
|
65
|
+
|
|
66
|
+
const res: Group[] = [{
|
|
67
|
+
type: "g",
|
|
68
|
+
kids: [],
|
|
69
|
+
}]
|
|
70
|
+
|
|
71
|
+
function push(e: Element) {
|
|
72
|
+
res[res.length - 1]?.kids.push({ ...e, fill: hex(e.fill) })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const open: Open = {
|
|
76
|
+
circle(attrs) {
|
|
77
|
+
push({ type: "circle", ...attrs })
|
|
78
|
+
},
|
|
79
|
+
path(attrs) {
|
|
80
|
+
push({ type: "path", ...attrs })
|
|
81
|
+
},
|
|
82
|
+
g({ fill }) {
|
|
83
|
+
res.push({
|
|
84
|
+
type: "g",
|
|
85
|
+
fill: hex(fill),
|
|
86
|
+
kids: [],
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const close: Close = {
|
|
92
|
+
circle() { },
|
|
93
|
+
path() { },
|
|
94
|
+
g() {
|
|
95
|
+
res.push({
|
|
96
|
+
type: "g",
|
|
97
|
+
kids: [],
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const p = sax.parser(true)
|
|
103
|
+
|
|
104
|
+
p.onopentag = ({ name, attributes }) => {
|
|
105
|
+
if (!(name in open)) {
|
|
106
|
+
console.log('unknown tag', name)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
open[name as types](attributes as any)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
p.onclosetag = (name) => {
|
|
113
|
+
if (!(name in close)) {
|
|
114
|
+
console.log('unknown tag', name)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
close[name as types]()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
p.write(svg).close()
|
|
121
|
+
|
|
122
|
+
return res
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function colorMap(colors: Set<string | undefined>) {
|
|
126
|
+
colors.delete(undefined)
|
|
127
|
+
return Object.fromEntries(Array.from(colors).filter((c): c is string => c !== undefined).map((c, i) => [c, i]))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getColors(e: Element): (string | undefined)[] {
|
|
131
|
+
if (e.type === 'g') return [e.fill, ...e.kids.flatMap(getColors)]
|
|
132
|
+
return [e.fill]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function fillStr(fill: string | undefined, colors: Record<string, number>) {
|
|
136
|
+
if (!fill) return ''
|
|
137
|
+
assert(fill in colors, `Color ${fill} not in ${Object.keys(colors)}`)
|
|
138
|
+
return `fill=c${colors[fill]}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type To = {
|
|
142
|
+
[t in types]: (args: Extract<Element, { type: t }>, colors: Record<string, number>) => string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const toTikz: To = {
|
|
146
|
+
circle({ cx, cy, r, fill }, colors) {
|
|
147
|
+
return `\\fill[${fillStr(fill, colors)}] (${cx}, ${cy}) circle (${r});`
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
path({ d, fill }, colors) {
|
|
151
|
+
return `\\fill[${fillStr(fill, colors)}] svg {${d}};`
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
g({ fill, kids }, colors) {
|
|
155
|
+
return [
|
|
156
|
+
`\\begin{scope}[${fillStr(fill, colors)}]`,
|
|
157
|
+
...kids.map(e => toTikz[e.type](e as any, colors)),
|
|
158
|
+
`\\end{scope}`
|
|
159
|
+
].join('\n')
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function defineColors(colors: Record<string, number>) {
|
|
164
|
+
return Object.entries(colors).map(([color, i]) => `\\definecolor{c${i}}{HTML}{${color.replace('#', '')}}`).join('\n')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function svgTex({ x, y, scale, rotate }: Transform, es: Element[], colors: Record<string, number>) {
|
|
168
|
+
return String.raw`
|
|
169
|
+
\begin{scope}[x=1pt, y=1pt, xshift=${x}, scale=${scale}, yscale=-1, yshift=${y}, rotate=${rotate}]
|
|
170
|
+
${es.map(e => toTikz[e.type](e as any, colors)).join('\n')}
|
|
171
|
+
\end{scope}`
|
|
172
|
+
}
|
package/src/cover.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
|
|
2
|
+
import assert from "assert"
|
|
3
|
+
import { z } from "zod"
|
|
4
|
+
import { colorMap, defineColors, Emoji, fromSvg, getColors, svgTex } from "./common"
|
|
5
|
+
import { emojiMap } from "./emojis"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export type Cover = z.infer<typeof Cover>
|
|
9
|
+
export const Cover = z.object({
|
|
10
|
+
gradient: z.array(z.string()).length(2),
|
|
11
|
+
emoji: Emoji,
|
|
12
|
+
title: z.string().max(64),
|
|
13
|
+
author: z.string().max(32),
|
|
14
|
+
tagline: z.string().max(128),
|
|
15
|
+
blurb: z.array(z.string().max(512)).max(3),
|
|
16
|
+
testimonial_quote: z.string().max(256),
|
|
17
|
+
testimonial_name: z.string().max(64),
|
|
18
|
+
slogan: z.string().max(64),
|
|
19
|
+
jpgBase64: z.string().max(256_000),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export function coverTex({
|
|
23
|
+
gradient,
|
|
24
|
+
emoji,
|
|
25
|
+
title,
|
|
26
|
+
author,
|
|
27
|
+
tagline,
|
|
28
|
+
blurb,
|
|
29
|
+
testimonial_quote,
|
|
30
|
+
testimonial_name,
|
|
31
|
+
slogan
|
|
32
|
+
}: Cover) {
|
|
33
|
+
|
|
34
|
+
const [c1, c2] = gradient
|
|
35
|
+
assert(c1 && c2, "Gradient must have two colors")
|
|
36
|
+
const el = fromSvg(emojiMap[emoji.emoji]!)
|
|
37
|
+
const colors = colorMap(new Set([...gradient, ...el.flatMap(getColors)]))
|
|
38
|
+
|
|
39
|
+
return String.raw`
|
|
40
|
+
\documentclass[17pt]{extarticle}
|
|
41
|
+
\usepackage[a4paper, landscape, margin=0cm]{geometry}
|
|
42
|
+
\usepackage{setspace}
|
|
43
|
+
\usepackage{tikz}
|
|
44
|
+
\usetikzlibrary{svg.path}
|
|
45
|
+
\usetikzlibrary{calc}
|
|
46
|
+
|
|
47
|
+
\usepackage{polyglossia}
|
|
48
|
+
\setmainlanguage{hebrew}
|
|
49
|
+
% Ensure this file exists in your project folder!
|
|
50
|
+
\newfontfamily\hebrewfont[
|
|
51
|
+
Script=Hebrew,
|
|
52
|
+
Path=./,
|
|
53
|
+
Extension=.ttf,
|
|
54
|
+
UprightFont=*-Regular,
|
|
55
|
+
BoldFont=*-Bold
|
|
56
|
+
]{Fredoka}
|
|
57
|
+
|
|
58
|
+
\pagestyle{empty}
|
|
59
|
+
|
|
60
|
+
\begin{document}
|
|
61
|
+
${defineColors(colors)}
|
|
62
|
+
|
|
63
|
+
\begin{tikzpicture}[remember picture, overlay]
|
|
64
|
+
\shade [shading=axis, shading angle=45, left color=c${colors[c1]}, right color=c${colors[c2]}]
|
|
65
|
+
(current page.south west) rectangle (current page.north east);
|
|
66
|
+
|
|
67
|
+
\fill[
|
|
68
|
+
yshift=-130,
|
|
69
|
+
xshift=-20,
|
|
70
|
+
xscale=410,
|
|
71
|
+
yscale=120,
|
|
72
|
+
blue!10,
|
|
73
|
+
opacity=0.4] svg "M 0.97 0.37 C 0.95 0.26 0.94 0.11 0.85 0.05 C 0.76 0.00 0.54 0.02 0.41 0.05 C 0.28 0.08 0.12 0.10 0.06 0.24 C 0.00 0.37 0.00 0.73 0.05 0.85 C 0.11 0.97 0.26 0.94 0.39 0.96 C 0.51 0.98 0.71 1.00 0.80 0.96 C 0.90 0.92 0.94 0.82 0.97 0.72 C 1.00 0.62 0.99 0.48 0.97 0.37 C 0.95 0.26 0.94 0.11 0.85 0.05";
|
|
74
|
+
|
|
75
|
+
\fill[
|
|
76
|
+
white,
|
|
77
|
+
opacity=0.2]
|
|
78
|
+
($(current page.north east) + (-0.025\paperwidth, -1cm)$)
|
|
79
|
+
rectangle ++(-0.45\paperwidth, -0.6\paperheight);
|
|
80
|
+
\end{tikzpicture}
|
|
81
|
+
|
|
82
|
+
\noindent
|
|
83
|
+
\begin{minipage}[c][0.95\textheight]{0.5\textwidth}
|
|
84
|
+
\centering
|
|
85
|
+
\begin{minipage}[t][0.6\textheight]{.8\textwidth}
|
|
86
|
+
\vspace{1cm}
|
|
87
|
+
\small
|
|
88
|
+
\textbf{${tagline}}
|
|
89
|
+
|
|
90
|
+
\vspace{.5cm}
|
|
91
|
+
\scriptsize
|
|
92
|
+
\begin{spacing}{1.5}
|
|
93
|
+
\setlength{\parskip}{.8em}
|
|
94
|
+
${blurb.join('\n\n')}
|
|
95
|
+
\end{spacing}
|
|
96
|
+
\vspace{\fill}
|
|
97
|
+
|
|
98
|
+
\vspace{0.5cm}
|
|
99
|
+
\footnotesize
|
|
100
|
+
\textbf{"${testimonial_quote}"}
|
|
101
|
+
|
|
102
|
+
\vspace{0.3cm}
|
|
103
|
+
\tiny
|
|
104
|
+
\hfill
|
|
105
|
+
- ${testimonial_name}
|
|
106
|
+
\end{minipage}
|
|
107
|
+
\vspace{\fill}
|
|
108
|
+
\begin{minipage}[c][0.25\textheight]{.8\textwidth}
|
|
109
|
+
\centering
|
|
110
|
+
\vspace{\fill}
|
|
111
|
+
\begin{tikzpicture}
|
|
112
|
+
% 1. Draw the white background rectangle with a thin border
|
|
113
|
+
\fill[white, opacity=0.8, rounded corners=2pt] (0,0) rectangle (4,2);
|
|
114
|
+
|
|
115
|
+
% 2. Generate "random" vertical lines for the barcode effect
|
|
116
|
+
% {position / thickness}
|
|
117
|
+
\foreach \x / \w in {
|
|
118
|
+
0.3/1.5, 0.5/0.5, 0.7/2.0, 0.9/0.8,
|
|
119
|
+
1.2/1.2, 1.4/0.4, 1.6/2.5, 1.9/1.0,
|
|
120
|
+
2.2/0.6, 2.4/1.8, 2.7/0.5, 3.0/2.2,
|
|
121
|
+
3.3/0.9, 3.5/1.4, 3.7/0.7%
|
|
122
|
+
} {
|
|
123
|
+
\draw[line width=\w pt, black] (\x, 0.2) -- (\x, 1.8);
|
|
124
|
+
}
|
|
125
|
+
\end{tikzpicture}
|
|
126
|
+
|
|
127
|
+
\vspace{\fill}
|
|
128
|
+
\small
|
|
129
|
+
https://booky.kids
|
|
130
|
+
|
|
131
|
+
\vspace{0.2cm}
|
|
132
|
+
${slogan}
|
|
133
|
+
|
|
134
|
+
\end{minipage}
|
|
135
|
+
\end{minipage}
|
|
136
|
+
%
|
|
137
|
+
%
|
|
138
|
+
\begin{minipage}[c][0.95\textheight]{0.5\textwidth}
|
|
139
|
+
\begin{minipage}[c][0.25\textheight]{\textwidth}
|
|
140
|
+
\centering
|
|
141
|
+
\large
|
|
142
|
+
\textbf{${title}} \\[.1cm]
|
|
143
|
+
\small
|
|
144
|
+
${author}
|
|
145
|
+
\end{minipage}
|
|
146
|
+
\begin{minipage}[c][0.7\textheight]{\textwidth}
|
|
147
|
+
\centering
|
|
148
|
+
\begin{tikzpicture}
|
|
149
|
+
\begin{scope}
|
|
150
|
+
\clip[
|
|
151
|
+
yshift=-170,
|
|
152
|
+
xshift=-170,
|
|
153
|
+
scale=340] svg "M 0.97 0.37 C 0.95 0.26 0.94 0.11 0.85 0.05 C 0.76 0.00 0.54 0.02 0.41 0.05 C 0.28 0.08 0.12 0.10 0.06 0.24 C 0.00 0.37 0.00 0.73 0.05 0.85 C 0.11 0.97 0.26 0.94 0.39 0.96 C 0.51 0.98 0.71 1.00 0.80 0.96 C 0.90 0.92 0.94 0.82 0.97 0.72 C 1.00 0.62 0.99 0.48 0.97 0.37 C 0.95 0.26 0.94 0.11 0.85 0.05";
|
|
154
|
+
\node[opacity=.75] at (0,0) {\includegraphics[width=12cm]{cover.jpg}};
|
|
155
|
+
\end{scope}
|
|
156
|
+
${svgTex(emoji, el, colors)}
|
|
157
|
+
\end{tikzpicture}
|
|
158
|
+
\end{minipage}
|
|
159
|
+
\end{minipage}
|
|
160
|
+
|
|
161
|
+
\end{document}
|
|
162
|
+
`
|
|
163
|
+
}
|
|
164
|
+
|
package/src/server.ts
CHANGED
|
@@ -1,40 +1,78 @@
|
|
|
1
1
|
import { $ } from "bun"
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises"
|
|
3
3
|
import PQueue from "p-queue"
|
|
4
|
-
import { Book,
|
|
4
|
+
import { Book, bookTex } from "./book"
|
|
5
|
+
import { Cover, coverTex } from "./cover"
|
|
5
6
|
|
|
6
7
|
const q = new PQueue({ concurrency: 1 })
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async function tmpDir() {
|
|
11
|
+
const id = crypto.randomUUID()
|
|
12
|
+
const tmp = `/tmp/doks/${id}`
|
|
13
|
+
await mkdir(tmp, { recursive: true })
|
|
14
|
+
await $`ln -s ${process.cwd()}/Fredoka-Bold.ttf ${tmp}/Fredoka-Bold.ttf`
|
|
15
|
+
await $`ln -s ${process.cwd()}/Fredoka-Regular.ttf ${tmp}/Fredoka-Regular.ttf`
|
|
16
|
+
|
|
17
|
+
return tmp
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Build {
|
|
21
|
+
tmp: string,
|
|
22
|
+
tex: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function pdf({ tmp, tex }: Build) {
|
|
26
|
+
|
|
27
|
+
await writeFile(`${tmp}/main.tex`, tex)
|
|
28
|
+
|
|
29
|
+
// Run twice to resolve "current page" coordinates
|
|
30
|
+
const runTex = () => $`xelatex -interaction=nonstopmode main.tex`.cwd(tmp).quiet()
|
|
31
|
+
await q.add(async () => {
|
|
32
|
+
await runTex()
|
|
33
|
+
await runTex()
|
|
34
|
+
})
|
|
35
|
+
const pdf = await Bun.file(`${tmp}/main.pdf`).arrayBuffer()
|
|
36
|
+
|
|
37
|
+
return new Response(pdf, {
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/pdf"
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function book(req: Request) {
|
|
45
|
+
const body = await req.json()
|
|
46
|
+
const book = Book.parse(body)
|
|
47
|
+
|
|
48
|
+
const tmp = await tmpDir()
|
|
49
|
+
await Promise.all(book.pages.map(async (page, i) => {
|
|
50
|
+
console.log(`Writing page ${i} jpg`)
|
|
51
|
+
writeFile(`${tmp}/${i}.jpg`, Buffer.from(page.jpgBase64, 'base64'))
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
const tex = bookTex(book)
|
|
55
|
+
return pdf({ tmp, tex })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function cover(req: Request) {
|
|
59
|
+
const body = await req.json()
|
|
60
|
+
const cover: Cover = Cover.parse(body)
|
|
61
|
+
|
|
62
|
+
const tex = coverTex(cover)
|
|
63
|
+
const tmp = await tmpDir()
|
|
64
|
+
await writeFile(`${tmp}/cover.jpg`, Buffer.from(cover.jpgBase64, 'base64'))
|
|
65
|
+
return pdf({ tmp, tex })
|
|
66
|
+
}
|
|
67
|
+
|
|
7
68
|
const server = Bun.serve({
|
|
8
69
|
port: 3000,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
await mkdir(tmp, { recursive: true })
|
|
16
|
-
await $`ln -s ${process.cwd()}/Fredoka-Bold.ttf ${tmp}/Fredoka-Bold.ttf`
|
|
17
|
-
|
|
18
|
-
const tex = toTex(book)
|
|
19
|
-
await writeFile(`${tmp}/book.tex`, tex)
|
|
20
|
-
await Promise.all(book.pages.map(async (page, i) => {
|
|
21
|
-
console.log(`Writing page ${i} jpg`)
|
|
22
|
-
writeFile(`${tmp}/${i}.jpg`, Buffer.from(page.jpgBase64, 'base64'))
|
|
23
|
-
}))
|
|
24
|
-
|
|
25
|
-
// Run twice to resolve "current page" coordinates
|
|
26
|
-
const runTex = () => $`xelatex -interaction=nonstopmode book.tex`.cwd(tmp).quiet()
|
|
27
|
-
await q.add(async () => {
|
|
28
|
-
await runTex()
|
|
29
|
-
await runTex()
|
|
30
|
-
})
|
|
31
|
-
const pdf = await Bun.file(`${tmp}/book.pdf`).arrayBuffer()
|
|
32
|
-
|
|
33
|
-
return new Response(pdf, {
|
|
34
|
-
headers: {
|
|
35
|
-
"Content-Type": "application/pdf"
|
|
36
|
-
}
|
|
37
|
-
})
|
|
70
|
+
routes: {
|
|
71
|
+
"/book": book,
|
|
72
|
+
"/cover": cover
|
|
73
|
+
},
|
|
74
|
+
fetch(req) {
|
|
75
|
+
return new Response('Hello from booky.kids!\n', { status: 200 })
|
|
38
76
|
},
|
|
39
77
|
error(err) {
|
|
40
78
|
console.error(err)
|