tikzify 0.0.8 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tikzify",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "module": "dist/tikzsvg.es.js",
6
6
  "types": "dist/tikzsvg.d.ts",
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: z.array(z.string()).length(2),
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 colorMap(colors: Set<string | undefined>) {
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
- \fancyhf{}
214
- \renewcommand{\headrulewidth}{0pt}
215
- \fancyfoot[C]{\Huge\thepage}
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
- Script=Hebrew,
222
- Path=./,
223
- BoldFont={Fredoka-Bold.ttf}
64
+ Script=Hebrew,
65
+ Path=./,
66
+ BoldFont={Fredoka-Bold.ttf}
224
67
  ]{Fredoka-Bold.ttf}
225
68
 
226
69
 
227
- ${Object.entries(colors).map(([color, i]) => `\\definecolor{c${i}}{HTML}{${color.replace('#', '')}}`).join('\n')}
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
- return String.raw`
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
- xshift=-190,
268
- yshift=190,
269
- scale=380,
270
- 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};
271
- \node[opacity=0.8] {\includegraphics[width=13.1cm]{${i}.jpg}};
272
- \end{scope}
273
- ${esImage}
274
- \end{tikzpicture}
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
- \shade[shading=axis, bottom color=c${colors[c1]}, top color=c${colors[c2]}, shading angle=45]
282
- (current page.south west) rectangle ([xshift=148.5mm]current page.north east);
283
- \fill[
284
- opacity=0.5,
285
- color=c${colors[page.textBg]},
286
- yshift=-20,
287
- xscale=380,
288
- yscale=-500,
289
- ] 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};
290
-
291
- ${esText}
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
- \begin{minipage}{10cm}
297
- \Huge
298
- \raggedleft
299
- ${page.text.map(line => line.trim()).join('\\\\')}
300
- \end{minipage}
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, toTex } from "./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
- async fetch(req) {
10
- const body = await req.json()
11
- const book = Book.parse(body)
12
-
13
- const id = crypto.randomUUID()
14
- const tmp = `/tmp/doks/${id}`
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)