tikzify 0.0.36 → 0.0.38
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 +2 -1
- package/src/book.ts +26 -7
- package/src/common.ts +230 -5
- package/src/cover.ts +150 -112
- package/src/server.ts +11 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tikzify",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": "./src/index.ts",
|
|
6
6
|
"files": [
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"@types/sax": "^1.2.7",
|
|
20
20
|
"p-queue": "^9.1.0",
|
|
21
21
|
"sax": "^1.4.4",
|
|
22
|
+
"sharp": "^0.34.5",
|
|
22
23
|
"zod": "^4.3.5"
|
|
23
24
|
}
|
|
24
25
|
}
|
package/src/book.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'assert'
|
|
2
2
|
import z from 'zod'
|
|
3
|
-
import { color, colorMap, defineColors, Emoji, fromSvg, getColors, gradient, Lang,
|
|
3
|
+
import { color, colorMap, defineColors, Emoji, fromSvg, getColors, gradient, Lang, svg, } from './common'
|
|
4
4
|
import { emojiMap } from './emojis'
|
|
5
5
|
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ export const Page = z.object({
|
|
|
10
10
|
textBackground: z.string(),
|
|
11
11
|
text: z.string().max(1024),
|
|
12
12
|
emojis: z.array(Emoji).length(6),
|
|
13
|
-
|
|
13
|
+
avifBase64: z.string().max(500_000)
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
export type Book = z.infer<typeof Book>
|
|
@@ -21,12 +21,19 @@ export const Book = z.object({
|
|
|
21
21
|
color: color,
|
|
22
22
|
title: z.string().max(256),
|
|
23
23
|
author: z.string().max(256),
|
|
24
|
-
|
|
24
|
+
heroAvifBase64: z.string().max(300_000),
|
|
25
25
|
|
|
26
26
|
// OTHER PAGES
|
|
27
27
|
pages: z.array(Page),
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
+
interface Transform {
|
|
31
|
+
x: number
|
|
32
|
+
y: number
|
|
33
|
+
scale: number
|
|
34
|
+
rotate: number
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
const TRANSFORMS_TEXT: Transform[] = [
|
|
31
38
|
{ x: 10, y: 15, scale: 2.3, rotate: -15, },
|
|
32
39
|
{ x: 300, y: 220, scale: 2.1, rotate: 15, },
|
|
@@ -39,6 +46,8 @@ const TRANSFORMS_IMAGE: Transform[] = [
|
|
|
39
46
|
{ x: 100, y: 80, scale: 2.3, rotate: 10 }
|
|
40
47
|
]
|
|
41
48
|
|
|
49
|
+
const LINE_SPACING = 1.15
|
|
50
|
+
|
|
42
51
|
export function bookTex(book: Book) {
|
|
43
52
|
const pages = book.pages
|
|
44
53
|
|
|
@@ -73,12 +82,16 @@ export function bookTex(book: Book) {
|
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
\usepackage{polyglossia}
|
|
76
|
-
|
|
85
|
+
${book.lang === 'he' ? String.raw`\setmainlanguage{hebrew}
|
|
77
86
|
\newfontfamily\hebrewfont[
|
|
78
87
|
Script=Hebrew,
|
|
79
88
|
Path=./,
|
|
80
89
|
BoldFont={Fredoka-Bold.ttf}
|
|
81
|
-
]{Fredoka-Bold.ttf}
|
|
90
|
+
]{Fredoka-Bold.ttf}` : String.raw`\setmainlanguage{english}
|
|
91
|
+
\newfontfamily\englishfont[
|
|
92
|
+
Path=./,
|
|
93
|
+
BoldFont={Fredoka-Bold.ttf}
|
|
94
|
+
]{Fredoka-Bold.ttf}`}
|
|
82
95
|
|
|
83
96
|
|
|
84
97
|
${defineColors(colors)}
|
|
@@ -116,7 +129,11 @@ ${book.pages.map((page, i) => {
|
|
|
116
129
|
assert(emojis.length <= transforms.length)
|
|
117
130
|
return emojis.map((emoji, i) => {
|
|
118
131
|
const els = emojiElements[emoji]!
|
|
119
|
-
return
|
|
132
|
+
return svg({
|
|
133
|
+
...transforms[i]!,
|
|
134
|
+
elements: els,
|
|
135
|
+
colors
|
|
136
|
+
})
|
|
120
137
|
}).join('\n')
|
|
121
138
|
}
|
|
122
139
|
|
|
@@ -165,7 +182,9 @@ ${esText}
|
|
|
165
182
|
\begin{center}
|
|
166
183
|
\begin{minipage}{10cm}
|
|
167
184
|
\Huge
|
|
168
|
-
\
|
|
185
|
+
\linespread{${LINE_SPACING}}\selectfont
|
|
186
|
+
\setlength{\parskip}{0.8em}
|
|
187
|
+
${rtl ? '\\raggedleft' : '\\raggedright'}
|
|
169
188
|
${page.text}
|
|
170
189
|
\end{minipage}
|
|
171
190
|
\end{center}
|
package/src/common.ts
CHANGED
|
@@ -10,8 +10,29 @@ export type Emoji = z.infer<typeof Emoji>
|
|
|
10
10
|
export const Emoji = z.object(emojiMap).keyof()
|
|
11
11
|
|
|
12
12
|
const langs = ['he', 'en'] as const
|
|
13
|
+
export type Lang = typeof langs[number]
|
|
13
14
|
export const Lang = z.enum(langs)
|
|
14
15
|
|
|
16
|
+
type LangConfig = {
|
|
17
|
+
name: string
|
|
18
|
+
script: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const langConfig: { [key in Lang]: LangConfig } = {
|
|
22
|
+
he: { name: 'hebrew', script: 'Hebrew' },
|
|
23
|
+
en: { name: 'english', script: 'Latin' },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const langName: { [key in Lang]: string } = {
|
|
27
|
+
he: langConfig.he.name,
|
|
28
|
+
en: langConfig.en.name,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const script: { [key in Lang]: string } = {
|
|
32
|
+
he: langConfig.he.script,
|
|
33
|
+
en: langConfig.en.script,
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
type Path = z.infer<typeof Path>
|
|
16
37
|
const Path = z.object({
|
|
17
38
|
type: z.literal("path"),
|
|
@@ -178,16 +199,220 @@ export function defineColors(colors: Record<string, number>) {
|
|
|
178
199
|
return Object.entries(colors).map(([color, i]) => `\\definecolor{c${i}}{HTML}{${color.replace('#', '')}}`).join('\n')
|
|
179
200
|
}
|
|
180
201
|
|
|
181
|
-
|
|
202
|
+
interface Svg {
|
|
182
203
|
x: number
|
|
183
204
|
y: number
|
|
184
205
|
scale: number
|
|
185
206
|
rotate: number
|
|
207
|
+
elements: Element[]
|
|
208
|
+
colors: Record<string, number>
|
|
186
209
|
}
|
|
187
|
-
|
|
188
|
-
export function svgTex({ x, y, scale, rotate }: Transform, es: Element[], colors: Record<string, number>) {
|
|
210
|
+
export function svg({ x, y, scale, rotate, elements, colors }: Svg) {
|
|
189
211
|
return String.raw`
|
|
190
212
|
\begin{scope}[x=1pt, y=1pt, xshift=${x}, scale=${scale}, yscale=-1, yshift=${y}, rotate=${rotate}]
|
|
191
|
-
${
|
|
213
|
+
${elements.map(e => toTikz[e.type](e as any, colors)).join('\n')}
|
|
214
|
+
\end{scope}`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function poly(lang: Lang) {
|
|
218
|
+
const { name, script } = langConfig[lang]
|
|
219
|
+
return String.raw`
|
|
220
|
+
\usepackage{polyglossia}
|
|
221
|
+
\setmainlanguage{${name}}
|
|
222
|
+
\setmainfont{Fredoka}[
|
|
223
|
+
Script=${script},
|
|
224
|
+
Path=./,
|
|
225
|
+
Extension=.ttf,
|
|
226
|
+
UprightFont=*-Regular,
|
|
227
|
+
BoldFont=*-Bold]`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
type MiniPage = z.infer<typeof MiniPage>
|
|
231
|
+
const MiniPage = z.object({
|
|
232
|
+
vAlign: z.enum(['t', 'c', 'b']),
|
|
233
|
+
width: z.number().min(0).max(1),
|
|
234
|
+
height: z.number().min(0).max(1),
|
|
235
|
+
content: z.string(),
|
|
236
|
+
})
|
|
237
|
+
export function minipage(raw: MiniPage) {
|
|
238
|
+
const { vAlign, width, height, content } = MiniPage.parse(raw)
|
|
239
|
+
return String.raw`\fbox{%
|
|
240
|
+
\begin{minipage}[c][${height}\textheight][${vAlign}]{${width}\textwidth}
|
|
241
|
+
${content}
|
|
242
|
+
\end{minipage}}%`
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
type Background = z.infer<typeof Background>
|
|
246
|
+
const Background = z.object({
|
|
247
|
+
gradient: gradient,
|
|
248
|
+
colors: z.record(z.string(), z.number()),
|
|
249
|
+
content: z.string(),
|
|
250
|
+
rtl: z.boolean()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
export function background(raw: Background) {
|
|
254
|
+
const { gradient: [c1, c2], content, colors, rtl } = Background.parse(raw)
|
|
255
|
+
assert(c1 && c2, "Gradient must have two colors")
|
|
256
|
+
const g1 = colors[c1]
|
|
257
|
+
const g2 = colors[c2]
|
|
258
|
+
return String.raw`\begin{tikzpicture}[remember picture, overlay, shift={(current page.north${rtl ? ' west' : ''})}]
|
|
259
|
+
\shade [left color=c${g1}, right color=c${g2}, shading angle=45]
|
|
260
|
+
(current page.south west) rectangle (current page.north east);
|
|
261
|
+
${content}
|
|
262
|
+
\end{tikzpicture}%`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
interface Absolute {
|
|
266
|
+
rtl: boolean
|
|
267
|
+
content: string
|
|
268
|
+
}
|
|
269
|
+
export function absolute({ rtl, content }: Absolute) {
|
|
270
|
+
return String.raw`\begin{tikzpicture}[remember picture, overlay, shift={(current page.north${rtl ? ' west' : ''})}]
|
|
271
|
+
${content}
|
|
272
|
+
\end{tikzpicture}%`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
type TextBackground = z.infer<typeof TextBackground>
|
|
276
|
+
const TextBackground = z.object({
|
|
277
|
+
yshift: z.number(),
|
|
278
|
+
xshift: z.number(),
|
|
279
|
+
xscale: z.number(),
|
|
280
|
+
yscale: z.number(),
|
|
281
|
+
color: color,
|
|
282
|
+
opacity: z.number().min(.1).max(1),
|
|
283
|
+
|
|
284
|
+
})
|
|
285
|
+
export function txtBackground(raw: TextBackground) {
|
|
286
|
+
const { yshift, xshift, xscale, yscale, color, opacity } = TextBackground.parse(raw)
|
|
287
|
+
return String.raw`% TEXT BACKGROUND
|
|
288
|
+
\fill[
|
|
289
|
+
yshift=${yshift},
|
|
290
|
+
xshift=${xshift},
|
|
291
|
+
xscale=${xscale},
|
|
292
|
+
yscale=${yscale},
|
|
293
|
+
fill=${color},
|
|
294
|
+
opacity=${opacity}] 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";`
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
type Img = z.infer<typeof Img>
|
|
299
|
+
const Img = z.object({
|
|
300
|
+
src: z.string(),
|
|
301
|
+
})
|
|
302
|
+
export function img(raw: Img) {
|
|
303
|
+
const { src } = Img.parse(raw)
|
|
304
|
+
return String.raw`\begin{scope}
|
|
305
|
+
\clip[
|
|
306
|
+
scale=349,
|
|
307
|
+
xshift=-.5,
|
|
308
|
+
yshift=-.5,
|
|
309
|
+
] 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";
|
|
310
|
+
\node[opacity=.75] at (0,0) {\includegraphics[width=12cm]{${src}}};
|
|
192
311
|
\end{scope}`
|
|
193
|
-
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function tikzpicture(content: string) {
|
|
315
|
+
return String.raw`\begin{tikzpicture}
|
|
316
|
+
${content}
|
|
317
|
+
\end{tikzpicture}%`
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
interface Rect {
|
|
321
|
+
x: number
|
|
322
|
+
y: number
|
|
323
|
+
width: number
|
|
324
|
+
height: number
|
|
325
|
+
fill: string
|
|
326
|
+
opacity: number
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
type Tcolorbox = z.infer<typeof Tcolorbox>
|
|
330
|
+
const Tcolorbox = z.object({
|
|
331
|
+
width: z.number().min(0).max(1),
|
|
332
|
+
color: z.string(),
|
|
333
|
+
opacity: z.number().min(0).max(1),
|
|
334
|
+
arc: z.number(),
|
|
335
|
+
boxsep: z.number(),
|
|
336
|
+
halign: z.enum(['left', 'center', 'right']),
|
|
337
|
+
content: z.string(),
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
export function tcolorbox(raw: Tcolorbox) {
|
|
341
|
+
const { width, color, opacity, arc, boxsep, halign, content } = Tcolorbox.parse(raw)
|
|
342
|
+
return String.raw`\begin{tcolorbox}[
|
|
343
|
+
width=${width}\textwidth,
|
|
344
|
+
enhanced,
|
|
345
|
+
colback=${color},
|
|
346
|
+
colframe=${color},
|
|
347
|
+
opacityback=${opacity},
|
|
348
|
+
opacityframe=0,
|
|
349
|
+
arc=${arc}pt,
|
|
350
|
+
boxsep=${boxsep}cm,
|
|
351
|
+
halign=${halign},
|
|
352
|
+
valign=top]
|
|
353
|
+
${content}
|
|
354
|
+
\end{tcolorbox}`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function ragged(rtl: boolean) {
|
|
358
|
+
return rtl
|
|
359
|
+
? `\\RaggedLeft`
|
|
360
|
+
: `\\RaggedRight`
|
|
361
|
+
}
|
|
362
|
+
export function vspace(cm: number) {
|
|
363
|
+
return `\\vspace{${cm}cm}`
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function Huge(text: string) {
|
|
367
|
+
return `\\Huge ${text}`
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function Large(text: string) {
|
|
371
|
+
return `\\Large ${text}`
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function large(text: string) {
|
|
375
|
+
return `\\large ${text}`
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function normalsize(text: string) {
|
|
379
|
+
return `\\normalsize ${text}`
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function small(text: string) {
|
|
383
|
+
return `\\small ${text}`
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function footnotesize(text: string) {
|
|
387
|
+
return `\\footnotesize ${text}`
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function scriptsize(text: string) {
|
|
391
|
+
return `\\scriptsize ${text}`
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function tiny(text: string) {
|
|
395
|
+
return `\\tiny ${text}`
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function bf(text: string) {
|
|
399
|
+
return `\\textbf{${text}}`
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function parskip(cm: number) {
|
|
403
|
+
return `\\setlength{\\parskip}{${cm}cm}`
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
interface Quote {
|
|
407
|
+
text: string
|
|
408
|
+
name: string
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function quote({ text, name }: Quote) {
|
|
412
|
+
return String.raw`
|
|
413
|
+
${footnotesize(bf(`${text}`))} \\[.3cm]
|
|
414
|
+
\hfill ${tiny(`-${name}`)}`
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export const vfill = `\\vspace*{\\fill}`
|
|
418
|
+
export const centering = '\\centering'
|
package/src/cover.ts
CHANGED
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
|
|
2
2
|
import assert from "assert"
|
|
3
3
|
import { z } from "zod"
|
|
4
|
-
import { colorMap, defineColors, Emoji, fromSvg, getColors,
|
|
4
|
+
import { absolute, background, bf, centering, colorMap, defineColors, Emoji, footnotesize, fromSvg, getColors, gradient, img, Lang, Large, minipage, normalsize, parskip, poly, quote, ragged, svg, tcolorbox, tikzpicture, txtBackground, vfill, vspace } from "./common"
|
|
5
5
|
import { emojiMap } from "./emojis"
|
|
6
6
|
|
|
7
|
+
const backBackground = String.raw`\fill[
|
|
8
|
+
white,
|
|
9
|
+
opacity=0.2]
|
|
10
|
+
($(current page.north east) + (-0.025\paperwidth, -1cm)$)
|
|
11
|
+
rectangle ++(-0.45\paperwidth, -0.6\paperheight);
|
|
12
|
+
\end{tikzpicture}
|
|
13
|
+
`
|
|
14
|
+
|
|
15
|
+
const barcode = String.raw`\begin{tikzpicture}
|
|
16
|
+
% 1. Draw the white background rectangle with a thin border
|
|
17
|
+
\fill[white, opacity=0.8, rounded corners=2pt] (0,0) rectangle (4,2);
|
|
18
|
+
|
|
19
|
+
% 2. Generate "random" vertical lines for the barcode effect
|
|
20
|
+
% {position / thickness}
|
|
21
|
+
\foreach \x / \w in {
|
|
22
|
+
0.3/1.5, 0.5/0.5, 0.7/2.0, 0.9/0.8,
|
|
23
|
+
1.2/1.2, 1.4/0.4, 1.6/2.5, 1.9/1.0,
|
|
24
|
+
2.2/0.6, 2.4/1.8, 2.7/0.5, 3.0/2.2,
|
|
25
|
+
3.3/0.9, 3.5/1.4, 3.7/0.7%
|
|
26
|
+
} {
|
|
27
|
+
\draw[line width=\w pt, black] (\x, 0.2) -- (\x, 1.8);
|
|
28
|
+
}
|
|
29
|
+
\end{tikzpicture}
|
|
30
|
+
`
|
|
7
31
|
|
|
8
32
|
export type Cover = z.infer<typeof Cover>
|
|
9
33
|
export const Cover = z.object({
|
|
10
|
-
|
|
34
|
+
lang: Lang,
|
|
35
|
+
gradient: gradient,
|
|
11
36
|
emoji: Emoji,
|
|
12
37
|
title: z.string().max(64),
|
|
13
38
|
author: z.string().max(32),
|
|
@@ -16,11 +41,11 @@ export const Cover = z.object({
|
|
|
16
41
|
testimonial_quote: z.string().max(256),
|
|
17
42
|
testimonial_name: z.string().max(64),
|
|
18
43
|
slogan: z.string().max(64),
|
|
19
|
-
|
|
44
|
+
avifBase64: z.string().max(500_000),
|
|
20
45
|
})
|
|
21
46
|
|
|
22
|
-
const TRANSFORM: Transform = { x: -220, y: 60, scale: 2.3, rotate: -15 }
|
|
23
47
|
export function coverTex({
|
|
48
|
+
lang,
|
|
24
49
|
gradient,
|
|
25
50
|
emoji,
|
|
26
51
|
title,
|
|
@@ -37,127 +62,140 @@ export function coverTex({
|
|
|
37
62
|
const el = fromSvg(emojiMap[emoji]!)
|
|
38
63
|
const colors = colorMap(new Set([...gradient, ...el.flatMap(getColors)]))
|
|
39
64
|
|
|
65
|
+
const rtl = lang === 'he'
|
|
66
|
+
|
|
67
|
+
const titlePage = minipage({
|
|
68
|
+
vAlign: 't',
|
|
69
|
+
width: .5,
|
|
70
|
+
height: 1,
|
|
71
|
+
content: [
|
|
72
|
+
minipage({
|
|
73
|
+
vAlign: 'c',
|
|
74
|
+
width: 1,
|
|
75
|
+
height: .28,
|
|
76
|
+
content: [
|
|
77
|
+
centering,
|
|
78
|
+
Large(bf(title)),
|
|
79
|
+
'',
|
|
80
|
+
vspace(0.5),
|
|
81
|
+
normalsize(author)
|
|
82
|
+
].join('\n')
|
|
83
|
+
}),
|
|
84
|
+
'',
|
|
85
|
+
vfill,
|
|
86
|
+
centering,
|
|
87
|
+
tikzpicture([
|
|
88
|
+
img({ src: 'cover.jpg' }),
|
|
89
|
+
].join('\n')),
|
|
90
|
+
vfill,
|
|
91
|
+
].join('\n')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const backPage = minipage({
|
|
95
|
+
vAlign: 't',
|
|
96
|
+
width: .5,
|
|
97
|
+
height: 1,
|
|
98
|
+
content: [
|
|
99
|
+
centering,
|
|
100
|
+
vspace(1),
|
|
101
|
+
tcolorbox({
|
|
102
|
+
width: 0.9,
|
|
103
|
+
color: 'white',
|
|
104
|
+
opacity: 0.4,
|
|
105
|
+
arc: 5,
|
|
106
|
+
boxsep: .4,
|
|
107
|
+
halign: rtl ? 'right' : 'left',
|
|
108
|
+
content: minipage({
|
|
109
|
+
vAlign: 't',
|
|
110
|
+
width: 1,
|
|
111
|
+
height: .6,
|
|
112
|
+
content: [
|
|
113
|
+
ragged(rtl),
|
|
114
|
+
parskip(0.3),
|
|
115
|
+
normalsize(bf(tagline)),
|
|
116
|
+
'',
|
|
117
|
+
vspace(0.3),
|
|
118
|
+
footnotesize(blurb),
|
|
119
|
+
'',
|
|
120
|
+
vfill,
|
|
121
|
+
quote({
|
|
122
|
+
text: testimonial_quote,
|
|
123
|
+
name: testimonial_name,
|
|
124
|
+
})
|
|
125
|
+
].join('\n')
|
|
126
|
+
})
|
|
127
|
+
}),
|
|
128
|
+
'',
|
|
129
|
+
vfill,
|
|
130
|
+
barcode,
|
|
131
|
+
'',
|
|
132
|
+
'https://booky.kids',
|
|
133
|
+
'',
|
|
134
|
+
vspace(0.2),
|
|
135
|
+
footnotesize(slogan),
|
|
136
|
+
'',
|
|
137
|
+
vfill,
|
|
138
|
+
].join('\n')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const pages = [backPage, titlePage]
|
|
142
|
+
const doc = [
|
|
143
|
+
// BACKGROUND
|
|
144
|
+
background({
|
|
145
|
+
rtl,
|
|
146
|
+
gradient,
|
|
147
|
+
colors,
|
|
148
|
+
content: txtBackground({
|
|
149
|
+
yshift: -145,
|
|
150
|
+
xshift: 0,
|
|
151
|
+
xscale: 410,
|
|
152
|
+
yscale: 130,
|
|
153
|
+
color: 'blue!10',
|
|
154
|
+
opacity: 0.4
|
|
155
|
+
})
|
|
156
|
+
}),
|
|
157
|
+
|
|
158
|
+
...(rtl ? pages.reverse() : pages),
|
|
159
|
+
// EMOJI
|
|
160
|
+
absolute({
|
|
161
|
+
rtl,
|
|
162
|
+
content: [
|
|
163
|
+
svg({
|
|
164
|
+
x: 10,
|
|
165
|
+
y: 220,
|
|
166
|
+
scale: 2.3,
|
|
167
|
+
rotate: -15,
|
|
168
|
+
colors,
|
|
169
|
+
elements: el,
|
|
170
|
+
})
|
|
171
|
+
].join('\n')
|
|
172
|
+
})
|
|
173
|
+
]
|
|
174
|
+
|
|
40
175
|
return String.raw`
|
|
41
176
|
\documentclass[17pt]{extarticle}
|
|
42
177
|
\usepackage[a4paper, landscape, margin=0cm]{geometry}
|
|
43
178
|
\usepackage{setspace}
|
|
179
|
+
\usepackage[most]{tcolorbox}
|
|
180
|
+
\usepackage[none]{hyphenat}
|
|
181
|
+
\usepackage{ragged2e}
|
|
182
|
+
|
|
183
|
+
|
|
44
184
|
\usepackage{tikz}
|
|
45
185
|
\usetikzlibrary{svg.path}
|
|
46
186
|
\usetikzlibrary{calc}
|
|
47
187
|
|
|
48
|
-
\
|
|
49
|
-
\
|
|
50
|
-
|
|
51
|
-
\newfontfamily\hebrewfont[
|
|
52
|
-
Script=Hebrew,
|
|
53
|
-
Path=./,
|
|
54
|
-
Extension=.ttf,
|
|
55
|
-
UprightFont=*-Regular,
|
|
56
|
-
BoldFont=*-Bold
|
|
57
|
-
]{Fredoka}
|
|
58
|
-
|
|
188
|
+
\setlength{\fboxsep}{0pt}
|
|
189
|
+
\setlength{\fboxrule}{0pt}
|
|
190
|
+
${poly(lang)}
|
|
59
191
|
\pagestyle{empty}
|
|
60
192
|
|
|
61
193
|
\begin{document}
|
|
194
|
+
${ragged(rtl)}
|
|
62
195
|
${defineColors(colors)}
|
|
63
|
-
|
|
64
|
-
\begin{tikzpicture}[remember picture, overlay]
|
|
65
|
-
\shade [shading=axis, shading angle=45, left color=c${colors[c1]}, right color=c${colors[c2]}]
|
|
66
|
-
(current page.south west) rectangle (current page.north east);
|
|
67
|
-
|
|
68
|
-
\fill[
|
|
69
|
-
yshift=-130,
|
|
70
|
-
xshift=-20,
|
|
71
|
-
xscale=410,
|
|
72
|
-
yscale=120,
|
|
73
|
-
blue!10,
|
|
74
|
-
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";
|
|
75
|
-
|
|
76
|
-
\fill[
|
|
77
|
-
white,
|
|
78
|
-
opacity=0.2]
|
|
79
|
-
($(current page.north east) + (-0.025\paperwidth, -1cm)$)
|
|
80
|
-
rectangle ++(-0.45\paperwidth, -0.6\paperheight);
|
|
81
|
-
\end{tikzpicture}
|
|
82
|
-
|
|
196
|
+
\setlength{\parindent}{0pt}
|
|
83
197
|
\noindent
|
|
84
|
-
|
|
85
|
-
\centering
|
|
86
|
-
\begin{minipage}[t][0.6\textheight]{.8\textwidth}
|
|
87
|
-
\vspace{1cm}
|
|
88
|
-
\small
|
|
89
|
-
\textbf{${tagline}}
|
|
90
|
-
|
|
91
|
-
\vspace{.5cm}
|
|
92
|
-
\scriptsize
|
|
93
|
-
\begin{spacing}{1.5}
|
|
94
|
-
\setlength{\parskip}{.8em}
|
|
95
|
-
${blurb}
|
|
96
|
-
\end{spacing}
|
|
97
|
-
\vspace{\fill}
|
|
98
|
-
|
|
99
|
-
\vspace{0.5cm}
|
|
100
|
-
\footnotesize
|
|
101
|
-
\textbf{"${testimonial_quote}"}
|
|
102
|
-
|
|
103
|
-
\vspace{0.3cm}
|
|
104
|
-
\tiny
|
|
105
|
-
\hfill
|
|
106
|
-
- ${testimonial_name}
|
|
107
|
-
\end{minipage}
|
|
108
|
-
\vspace{\fill}
|
|
109
|
-
\begin{minipage}[c][0.25\textheight]{.8\textwidth}
|
|
110
|
-
\centering
|
|
111
|
-
\vspace{\fill}
|
|
112
|
-
\begin{tikzpicture}
|
|
113
|
-
% 1. Draw the white background rectangle with a thin border
|
|
114
|
-
\fill[white, opacity=0.8, rounded corners=2pt] (0,0) rectangle (4,2);
|
|
115
|
-
|
|
116
|
-
% 2. Generate "random" vertical lines for the barcode effect
|
|
117
|
-
% {position / thickness}
|
|
118
|
-
\foreach \x / \w in {
|
|
119
|
-
0.3/1.5, 0.5/0.5, 0.7/2.0, 0.9/0.8,
|
|
120
|
-
1.2/1.2, 1.4/0.4, 1.6/2.5, 1.9/1.0,
|
|
121
|
-
2.2/0.6, 2.4/1.8, 2.7/0.5, 3.0/2.2,
|
|
122
|
-
3.3/0.9, 3.5/1.4, 3.7/0.7%
|
|
123
|
-
} {
|
|
124
|
-
\draw[line width=\w pt, black] (\x, 0.2) -- (\x, 1.8);
|
|
125
|
-
}
|
|
126
|
-
\end{tikzpicture}
|
|
127
|
-
|
|
128
|
-
\vspace{\fill}
|
|
129
|
-
\small
|
|
130
|
-
https://booky.kids
|
|
131
|
-
|
|
132
|
-
\vspace{0.2cm}
|
|
133
|
-
${slogan}
|
|
134
|
-
|
|
135
|
-
\end{minipage}
|
|
136
|
-
\end{minipage}
|
|
137
|
-
%
|
|
138
|
-
%
|
|
139
|
-
\begin{minipage}[c][0.95\textheight]{0.5\textwidth}
|
|
140
|
-
\begin{minipage}[c][0.25\textheight]{\textwidth}
|
|
141
|
-
\centering
|
|
142
|
-
\large
|
|
143
|
-
\textbf{${title}} \\[.1cm]
|
|
144
|
-
\small
|
|
145
|
-
${author}
|
|
146
|
-
\end{minipage}
|
|
147
|
-
\begin{minipage}[c][0.7\textheight]{\textwidth}
|
|
148
|
-
\centering
|
|
149
|
-
\begin{tikzpicture}
|
|
150
|
-
\begin{scope}
|
|
151
|
-
\clip[
|
|
152
|
-
yshift=-170,
|
|
153
|
-
xshift=-170,
|
|
154
|
-
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";
|
|
155
|
-
\node[opacity=.75] at (0,0) {\includegraphics[width=12cm]{cover.jpg}};
|
|
156
|
-
\end{scope}
|
|
157
|
-
${svgTex(TRANSFORM, el, colors)}
|
|
158
|
-
\end{tikzpicture}
|
|
159
|
-
\end{minipage}
|
|
160
|
-
\end{minipage}
|
|
198
|
+
${(rtl ? doc.reverse() : doc).join('\n')}
|
|
161
199
|
|
|
162
200
|
\end{document}
|
|
163
201
|
`
|
package/src/server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { $ } from "bun"
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises"
|
|
3
3
|
import PQueue from "p-queue"
|
|
4
|
+
import sharp from "sharp"
|
|
4
5
|
import { Book, bookTex } from "./book"
|
|
5
6
|
import { Cover, coverTex } from "./cover"
|
|
6
7
|
|
|
@@ -22,6 +23,11 @@ interface Build {
|
|
|
22
23
|
tex: string
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
async function jpg(base64: string) {
|
|
27
|
+
const b = Buffer.from(base64, 'base64')
|
|
28
|
+
return await sharp(b).jpeg({ quality: 90 }).toBuffer()
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
async function pdf({ tmp, tex }: Build) {
|
|
26
32
|
|
|
27
33
|
await writeFile(`${tmp}/main.tex`, tex)
|
|
@@ -46,11 +52,11 @@ async function book(req: Request) {
|
|
|
46
52
|
const book = Book.parse(body)
|
|
47
53
|
|
|
48
54
|
const tmp = await tmpDir()
|
|
49
|
-
await writeFile(`${tmp}/hero.jpg`,
|
|
50
|
-
|
|
55
|
+
await writeFile(`${tmp}/hero.jpg`, await jpg(book.heroAvifBase64))
|
|
56
|
+
for (const [i, page] of book.pages.entries()) {
|
|
51
57
|
console.log(`Writing page ${i} jpg`)
|
|
52
|
-
writeFile(`${tmp}/${i}.jpg`,
|
|
53
|
-
}
|
|
58
|
+
await writeFile(`${tmp}/${i}.jpg`, await jpg(page.avifBase64))
|
|
59
|
+
}
|
|
54
60
|
|
|
55
61
|
const tex = bookTex(book)
|
|
56
62
|
return pdf({ tmp, tex })
|
|
@@ -62,7 +68,7 @@ async function cover(req: Request) {
|
|
|
62
68
|
|
|
63
69
|
const tex = coverTex(cover)
|
|
64
70
|
const tmp = await tmpDir()
|
|
65
|
-
await writeFile(`${tmp}/cover.jpg`,
|
|
71
|
+
await writeFile(`${tmp}/cover.jpg`, await jpg(cover.avifBase64))
|
|
66
72
|
return pdf({ tmp, tex })
|
|
67
73
|
}
|
|
68
74
|
|