tangled
alpha
login
or
join now
timtinkers.online
/
onomatopoeia
0
fork
atom
bluesky quote bot
0
fork
atom
overview
issues
pulls
pipelines
Replaced skia canvas with ImageScript
timtinkers.online
1 year ago
3abb3b22
de77a632
+325
-294
4 changed files
expand all
collapse all
unified
split
deno.lock
main.ts
utils
canvasUtils.ts
imageUtils.ts
+106
-72
deno.lock
···
1
1
{
2
2
"version": "4",
3
3
"specifiers": {
4
4
-
"jsr:@denosaurs/plug@1.0.5": "1.0.5",
5
5
-
"jsr:@gfx/canvas@0.5.7": "0.5.7",
6
6
-
"jsr:@std/assert@0.214": "0.214.0",
7
7
-
"jsr:@std/assert@0.217": "0.217.0",
8
4
"jsr:@std/dotenv@*": "0.225.2",
9
9
-
"jsr:@std/encoding@0.214": "0.214.0",
10
10
-
"jsr:@std/encoding@0.217.0": "0.217.0",
11
11
-
"jsr:@std/fmt@0.214": "0.214.0",
12
12
-
"jsr:@std/fs@0.214": "0.214.0",
13
13
-
"jsr:@std/fs@0.217.0": "0.217.0",
14
14
-
"jsr:@std/path@0.214": "0.214.0",
15
15
-
"jsr:@std/path@0.217": "0.217.0",
16
16
-
"jsr:@std/path@0.217.0": "0.217.0",
17
17
-
"npm:@atcute/bluesky-richtext-builder@*": "1.0.1_@atcute+bluesky@1.0.7__@atcute+client@2.0.3_@atcute+client@2.0.3",
18
18
-
"npm:@atcute/client@^2.0.3": "2.0.3",
5
5
+
"npm:@atcute/bluesky-richtext-builder@*": "1.0.2_@atcute+bluesky@1.0.8__@atcute+client@2.0.4_@atcute+client@2.0.4",
6
6
+
"npm:@atcute/client@^2.0.3": "2.0.4",
7
7
+
"npm:fontkit@*": "2.0.4",
8
8
+
"npm:hyphenation.en-us@*": "0.2.1",
19
9
"npm:tex-linebreak@*": "0.7.1"
20
10
},
21
11
"jsr": {
22
22
-
"@denosaurs/plug@1.0.5": {
23
23
-
"integrity": "04cd988da558adc226202d88c3a434d5fcc08146eaf4baf0cea0c2284b16d2bf",
12
12
+
"@std/dotenv@0.225.2": {
13
13
+
"integrity": "e2025dce4de6c7bca21dece8baddd4262b09d5187217e231b033e088e0c4dd23"
14
14
+
}
15
15
+
},
16
16
+
"npm": {
17
17
+
"@atcute/bluesky-richtext-builder@1.0.2_@atcute+bluesky@1.0.8__@atcute+client@2.0.4_@atcute+client@2.0.4": {
18
18
+
"integrity": "sha512-sa+9B5Ygb1GcWeMpav9RVBRdFLL5snZEoFFF2RkTaNr61m/cLd5lk97QJs+t9LXUEl5cfHS3jXujywFrGXZj9w==",
24
19
"dependencies": [
25
25
-
"jsr:@std/encoding@0.214",
26
26
-
"jsr:@std/fmt",
27
27
-
"jsr:@std/fs@0.214",
28
28
-
"jsr:@std/path@0.214"
20
20
+
"@atcute/bluesky",
21
21
+
"@atcute/client"
29
22
]
30
23
},
31
31
-
"@gfx/canvas@0.5.7": {
32
32
-
"integrity": "cd054e34f64763c7e6958bedc5fef00def929c20ebb89da5687112b5d402dbe3",
24
24
+
"@atcute/bluesky@1.0.8_@atcute+client@2.0.4": {
25
25
+
"integrity": "sha512-XqAZHYh65ZyBBT5rRkEhP656THYaY6CE+EY0AZXkIDaXJNdIl9KINZW5dnSrVWifspJbM+gUw1QFlCv8Yrx01g==",
33
26
"dependencies": [
34
34
-
"jsr:@denosaurs/plug",
35
35
-
"jsr:@std/encoding@0.217.0",
36
36
-
"jsr:@std/fs@0.217.0",
37
37
-
"jsr:@std/path@0.217.0"
27
27
+
"@atcute/client"
38
28
]
39
29
},
40
40
-
"@std/assert@0.214.0": {
41
41
-
"integrity": "55d398de76a9828fd3b1aa653f4dba3eee4c6985d90c514865d2be9bd082b140"
30
30
+
"@atcute/client@2.0.4": {
31
31
+
"integrity": "sha512-bKA6KEOmrdhU2CDRNp13M4WyKN0EdrVLKJffzPo62ANSTMacz5hRJhmvQYwuo7BZSGIoDql4sH+QR6Xbk3DERg=="
42
32
},
43
43
-
"@std/assert@0.217.0": {
44
44
-
"integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642"
33
33
+
"@swc/helpers@0.5.13": {
34
34
+
"integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==",
35
35
+
"dependencies": [
36
36
+
"tslib"
37
37
+
]
45
38
},
46
46
-
"@std/dotenv@0.225.2": {
47
47
-
"integrity": "e2025dce4de6c7bca21dece8baddd4262b09d5187217e231b033e088e0c4dd23"
39
39
+
"base64-js@1.5.1": {
40
40
+
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
48
41
},
49
49
-
"@std/encoding@0.214.0": {
50
50
-
"integrity": "30a8713e1db22986c7e780555ffd2fefd1d4f9374d734bb41f5970f6c3352af5"
42
42
+
"brotli@1.3.3": {
43
43
+
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
44
44
+
"dependencies": [
45
45
+
"base64-js"
46
46
+
]
51
47
},
52
52
-
"@std/encoding@0.217.0": {
53
53
-
"integrity": "b03e8ff94c98d6b6a02c02c5cf8e5d203400155516248964fc4559abc04669dc"
48
48
+
"clone@2.1.2": {
49
49
+
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="
54
50
},
55
55
-
"@std/fmt@0.214.0": {
56
56
-
"integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4"
51
51
+
"dfa@1.2.0": {
52
52
+
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
57
53
},
58
58
-
"@std/fs@0.214.0": {
59
59
-
"integrity": "bc880fea0be120cb1550b1ed7faf92fe071003d83f2456a1e129b39193d85bea",
60
60
-
"dependencies": [
61
61
-
"jsr:@std/assert@0.214",
62
62
-
"jsr:@std/path@0.214"
63
63
-
]
54
54
+
"fast-deep-equal@3.1.3": {
55
55
+
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
64
56
},
65
65
-
"@std/fs@0.217.0": {
66
66
-
"integrity": "0bfff5f3618d68c385b28b4ffbf3a15c98293a0f1186444458b62e0111ce77b2",
57
57
+
"fontkit@2.0.4": {
58
58
+
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
67
59
"dependencies": [
68
68
-
"jsr:@std/assert@0.217",
69
69
-
"jsr:@std/path@0.217"
60
60
+
"@swc/helpers",
61
61
+
"brotli",
62
62
+
"clone",
63
63
+
"dfa",
64
64
+
"fast-deep-equal",
65
65
+
"restructure",
66
66
+
"tiny-inflate",
67
67
+
"unicode-properties",
68
68
+
"unicode-trie"
70
69
]
71
70
},
72
72
-
"@std/path@0.214.0": {
73
73
-
"integrity": "d5577c0b8d66f7e8e3586d864ebdf178bb326145a3611da5a51c961740300285",
71
71
+
"hyphenation.en-us@0.2.1": {
72
72
+
"integrity": "sha512-ItXYgvIpfN8rfXl/GTBQC7DsSb5PPsKh9gGzViK/iWzCS5mvjDebFJ6xCcIYo8dal+nSp2rUzvTT7BosrKlL8A==",
74
73
"dependencies": [
75
75
-
"jsr:@std/assert@0.214"
74
74
+
"hypher"
76
75
]
77
76
},
78
78
-
"@std/path@0.217.0": {
79
79
-
"integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11",
77
77
+
"hypher@0.2.5": {
78
78
+
"integrity": "sha512-kUTpuyzBWWDO2VakmjHC/cxesg4lKQP+Fdc+7lrK4yvjNjkV9vm5UTZMDAwOyyHTOpbkYrAMlNZHG61NnE9vYQ=="
79
79
+
},
80
80
+
"pako@0.2.9": {
81
81
+
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
82
82
+
},
83
83
+
"restructure@3.0.2": {
84
84
+
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="
85
85
+
},
86
86
+
"tex-linebreak@0.7.1": {
87
87
+
"integrity": "sha512-DioAcgbUGvacQSNJBcHoIUl2MnjWpNGz3R9tDX6gjtn/c+59ySW1dyFGMhIpl+u7sT+ywRwYAoAtk+wA5xUIsg=="
88
88
+
},
89
89
+
"tiny-inflate@1.0.3": {
90
90
+
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
91
91
+
},
92
92
+
"tslib@2.7.0": {
93
93
+
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
94
94
+
},
95
95
+
"unicode-properties@1.4.1": {
96
96
+
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
80
97
"dependencies": [
81
81
-
"jsr:@std/assert@0.217"
82
82
-
]
83
83
-
}
84
84
-
},
85
85
-
"npm": {
86
86
-
"@atcute/bluesky-richtext-builder@1.0.1_@atcute+bluesky@1.0.7__@atcute+client@2.0.3_@atcute+client@2.0.3": {
87
87
-
"integrity": "sha512-msyKHZa47id7Oe2zHpgPSuL+l9vLQXmKfi6uyMOS5yw6nLpNd5Tdb96wcgwscMmyH8UR4EoM+2oYLvyH89DcEQ==",
88
88
-
"dependencies": [
89
89
-
"@atcute/bluesky",
90
90
-
"@atcute/client"
98
98
+
"base64-js",
99
99
+
"unicode-trie"
91
100
]
92
101
},
93
93
-
"@atcute/bluesky@1.0.7_@atcute+client@2.0.3": {
94
94
-
"integrity": "sha512-2jPHzl7WbcqRtcAXanJy4Lp638ujqnoGmPCPmBlmpEDP34D7EVKQqjN/mlvglb5n539dThA9xlSgIS8yOxwzDA==",
102
102
+
"unicode-trie@2.0.0": {
103
103
+
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
95
104
"dependencies": [
96
96
-
"@atcute/client"
105
105
+
"pako",
106
106
+
"tiny-inflate"
97
107
]
98
98
-
},
99
99
-
"@atcute/client@2.0.3": {
100
100
-
"integrity": "sha512-j9GryA5l+4F0BTQWa6/1XmsuSPSq+bqNCY3mrHUGD592hMqUZxgpYDLgRWL+719V287AW/56AwvFYlbjlENp7A=="
101
101
-
},
102
102
-
"tex-linebreak@0.7.1": {
103
103
-
"integrity": "sha512-DioAcgbUGvacQSNJBcHoIUl2MnjWpNGz3R9tDX6gjtn/c+59ySW1dyFGMhIpl+u7sT+ywRwYAoAtk+wA5xUIsg=="
104
108
}
109
109
+
},
110
110
+
"remote": {
111
111
+
"https://deno.land/x/imagescript@1.3.0/ImageScript.js": "cf90773c966031edd781ed176c598f7ed495e7694cd9b86c986d2d97f783cca0",
112
112
+
"https://deno.land/x/imagescript@1.3.0/mod.ts": "18a6cb83c55e690c873505f6fe867364c678afb64934fe7aef593a6b92f79995",
113
113
+
"https://deno.land/x/imagescript@1.3.0/png/src/crc.mjs": "5cf50de181d61dd00e66a240d811018ba5070afa8bba302f393604404604de84",
114
114
+
"https://deno.land/x/imagescript@1.3.0/png/src/mem.mjs": "4968d400dae069b4bf0ef4767c1802fd2cc7d15d90eda4cfadf5b4cd19b96c6d",
115
115
+
"https://deno.land/x/imagescript@1.3.0/png/src/png.mjs": "96ef0ceff1b5a6cd9304749e5f187b4ab238509fb5f9a8be8ee934240271ed8d",
116
116
+
"https://deno.land/x/imagescript@1.3.0/png/src/zlib.mjs": "9867dc3fab1d31b664f9344b0d7e977f493d9c912a76c760d012ed2b89f7061c",
117
117
+
"https://deno.land/x/imagescript@1.3.0/utils/buffer.js": "952cb1beb8827e50a493a5d1f29a4845e8c648789406d389dd51f51205ba02d8",
118
118
+
"https://deno.land/x/imagescript@1.3.0/utils/crc32.js": "573d6222b3605890714ebc374e687ec2aa3e9a949223ea199483e47ca4864f7d",
119
119
+
"https://deno.land/x/imagescript@1.3.0/utils/png.js": "fbed9117e0a70602645d70df9c103ff6e79c03e987bd5c1685dcb4200729b6de",
120
120
+
"https://deno.land/x/imagescript@1.3.0/utils/wasm/font.js": "9e75d842608c057045698d6a7cdf5ffd27241b5cdea0391c89a1917b31294524",
121
121
+
"https://deno.land/x/imagescript@1.3.0/utils/wasm/gif.js": "8b86f7b96486bb8ff50fbc7c7487f86cb5cef85e6acd71e1def78a1aa2f12e4f",
122
122
+
"https://deno.land/x/imagescript@1.3.0/utils/wasm/jpeg.js": "75295e2fcf96b4f7bb894b3844fdaa8140d63169d28b466b5d5be89d59a7b6e6",
123
123
+
"https://deno.land/x/imagescript@1.3.0/utils/wasm/png.js": "0659536a8dd8f892c8346e268b2754b4414fad0ec1e9794dfcde1ba1c804ee02",
124
124
+
"https://deno.land/x/imagescript@1.3.0/utils/wasm/svg.js": "f5c8a9d1977b51a7c07549ceb6bbbaca9497321a193f28b3dc229a42d91bcf14",
125
125
+
"https://deno.land/x/imagescript@1.3.0/utils/wasm/tiff.js": "c2d7bdaef094df25aae1752e75167f485e89275d76a1379e39d8949580b7af4f",
126
126
+
"https://deno.land/x/imagescript@1.3.0/utils/wasm/zlib.js": "749875f83abffe24d3b977475a0cbd5f9b52bee1fbdbef61ec183cbfc17805f6",
127
127
+
"https://deno.land/x/imagescript@1.3.0/v2/framebuffer.mjs": "add44ff184636659714b3c6d4b896f628545451abffbc30b5bcc2e8d9a73d012",
128
128
+
"https://deno.land/x/imagescript@1.3.0/v2/ops/blur.mjs": "80716f1ffab8a2aeb54a036f583bf51a2b9dd37e005adc000add803df8e8a12f",
129
129
+
"https://deno.land/x/imagescript@1.3.0/v2/ops/color.mjs": "5e72cdcbf97dc939a2795223f01e3cb0544c0c56b03ea2aa026050df58348814",
130
130
+
"https://deno.land/x/imagescript@1.3.0/v2/ops/crop.mjs": "69431fa6f687fd9f0c31eff0ec27d7ac925275005e53a37f0c3fab4cc4d9a9ea",
131
131
+
"https://deno.land/x/imagescript@1.3.0/v2/ops/fill.mjs": "cf1b9488314753fbc9ebf03410ac74c2a34ea5a69fb6892cd6e8366cd1930d93",
132
132
+
"https://deno.land/x/imagescript@1.3.0/v2/ops/flip.mjs": "825a34a66567dcf15e76a719f1bf2f66fb106503cd69942292b1b0ae05c5718e",
133
133
+
"https://deno.land/x/imagescript@1.3.0/v2/ops/index.mjs": "423ba687119be2bba8cec72890577d3afa3621b6b8108912242fe937a183f2aa",
134
134
+
"https://deno.land/x/imagescript@1.3.0/v2/ops/iterator.mjs": "c2adf3d90ce00719a02c48c97634574176a3501ff026676259bd71aa8f5d69b9",
135
135
+
"https://deno.land/x/imagescript@1.3.0/v2/ops/overlay.mjs": "7e6e2c2ffd25006d52597ab8babc5f8f503d388a3fdf2fbc0eaea02799a020c9",
136
136
+
"https://deno.land/x/imagescript@1.3.0/v2/ops/resize.mjs": "814e78ebce8eaf8f1f918688db7b52a141405e06a36ed4b25d04413d69e7d17b",
137
137
+
"https://deno.land/x/imagescript@1.3.0/v2/ops/rotate.mjs": "a1b65616717bd2eed8db406affea3263b4674dada46b56441ef38167a187455d",
138
138
+
"https://deno.land/x/imagescript@1.3.0/v2/util/mem.mjs": "4968d400dae069b4bf0ef4767c1802fd2cc7d15d90eda4cfadf5b4cd19b96c6d"
105
139
}
106
140
}
+3
-3
main.ts
···
1
1
-
import { generatePng } from "./utils/canvasUtils.ts";
1
1
+
import { generateImage } from "./utils/imageUtils.ts";
2
2
import { createBskyPost } from "./utils/blueskyUtils.ts";
3
3
import { load } from "jsr:@std/dotenv";
4
4
···
64
64
let index = result.value ?? 0;
65
65
66
66
// Generate screenshot and upload it to Bluesky
67
67
-
const { image, aspectRatio } = generatePng(
68
68
-
data[index].quote, // "image.png"
67
67
+
const { image, aspectRatio } = await generateImage(
68
68
+
data[index].quote,
69
69
);
70
70
await createBskyPost(data[0], image, aspectRatio);
71
71
console.log(`Cron: posted quote of index ${index} to Bluesky.`);
-219
utils/canvasUtils.ts
···
1
1
-
import tex from "npm:tex-linebreak";
2
2
-
import {
3
3
-
Canvas,
4
4
-
CanvasRenderingContext2D,
5
5
-
createCanvas,
6
6
-
Fonts,
7
7
-
SvgCanvas,
8
8
-
SvgRenderingContext2D,
9
9
-
} from "jsr:@gfx/canvas@0.5.7";
10
10
-
11
11
-
// Font registering
12
12
-
// Fetch both regular and italic Merriweather font URLs from Google Fonts
13
13
-
const fontUrls = [
14
14
-
"https://fonts.googleapis.com/css2?family=Merriweather:wght@400&display=swap", // Regular
15
15
-
"https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@1,400&display=swap", // Italic
16
16
-
];
17
17
-
18
18
-
const fontDataArray = await Promise.all(
19
19
-
fontUrls.map(async (url) => {
20
20
-
const fontResponse = await fetch(url);
21
21
-
const cssText = await fontResponse.text();
22
22
-
const fontMatch = cssText.match(/url\(([^)]+)\)/);
23
23
-
if (!fontMatch) throw new Error("Failed to load font URL");
24
24
-
const fontFileUrl = fontMatch[1];
25
25
-
return fetch(fontFileUrl).then((res) => res.arrayBuffer());
26
26
-
}),
27
27
-
);
28
28
-
29
29
-
const lineWidth = 620;
30
30
-
const fontSize = 20;
31
31
-
const fontFamily = "Merriweather";
32
32
-
const indentSize = 30;
33
33
-
const padding = 40;
34
34
-
35
35
-
const fontRegularBuffer = new Uint8Array(fontDataArray[0]);
36
36
-
const fontItalicBuffer = new Uint8Array(fontDataArray[0]);
37
37
-
38
38
-
// Register both regular and italic fonts, set common family name
39
39
-
Fonts.register(fontRegularBuffer, "Merriweather-Regular");
40
40
-
Fonts.register(fontItalicBuffer, "Merriweather-Italic");
41
41
-
Fonts.setAlias("Merriweather", "Merriweather-Regular");
42
42
-
Fonts.setAlias("Merriweather", "Merriweather-Italic");
43
43
-
44
44
-
function generateCanvas(
45
45
-
createLocalCanvas: Function,
46
46
-
quote: string,
47
47
-
) {
48
48
-
const paragraphs = quote.split("\\n");
49
49
-
const rawTotalItems: tex.TextInputItem[][] = [];
50
50
-
const rawTotalPositionedItems: tex.PositionedItem[][] = [];
51
51
-
52
52
-
let lineOffset = 0;
53
53
-
let itemOffset = 0;
54
54
-
55
55
-
for (const paragraph of paragraphs) {
56
56
-
const items = tex.layoutItemsFromString(
57
57
-
paragraph.trim(),
58
58
-
measureText,
59
59
-
);
60
60
-
rawTotalItems.push(items);
61
61
-
62
62
-
// Create an array of line widths where the first line is indented
63
63
-
const lineWidths = items.map((_, index) =>
64
64
-
index === 0 ? lineWidth - indentSize : lineWidth
65
65
-
);
66
66
-
67
67
-
// Find where to insert line-breaks using the varying line widths
68
68
-
const breakpoints = tex.breakLines(items, lineWidths);
69
69
-
70
70
-
// Compute positions with indentation for the first line
71
71
-
const positionedItems = tex.positionItems(
72
72
-
items,
73
73
-
lineWidths,
74
74
-
breakpoints,
75
75
-
);
76
76
-
77
77
-
// Add the indent to the first line's xOffset
78
78
-
const adjustedPositionedItems = positionedItems.map((
79
79
-
element,
80
80
-
_idx,
81
81
-
) => ({
82
82
-
...element,
83
83
-
item: element.item + itemOffset,
84
84
-
line: element.line + lineOffset,
85
85
-
xOffset: element.line === 0
86
86
-
? element.xOffset + indentSize
87
87
-
: element.xOffset,
88
88
-
}));
89
89
-
90
90
-
rawTotalPositionedItems.push(adjustedPositionedItems);
91
91
-
92
92
-
lineOffset += positionedItems[positionedItems.length - 1].line + 1;
93
93
-
itemOffset += items.length;
94
94
-
}
95
95
-
96
96
-
const totalItems = rawTotalItems.flat();
97
97
-
const totalPositionedItems = rawTotalPositionedItems.flat();
98
98
-
99
99
-
const nLines = totalPositionedItems[totalPositionedItems.length - 1].line;
100
100
-
const width = lineWidth + padding * 2;
101
101
-
const height = nLines * fontSize * 1.5 + padding * 2.5;
102
102
-
103
103
-
const { canvas, ctx } = createLocalCanvas(width, height);
104
104
-
105
105
-
ctx.fillStyle = "ivory";
106
106
-
ctx.fillRect(0, 0, width, height);
107
107
-
108
108
-
let isItalic = false;
109
109
-
for (const positionedItem of totalPositionedItems) {
110
110
-
const { item, line, xOffset } = positionedItem;
111
111
-
const currentItem = totalItems[item];
112
112
-
113
113
-
if (!("text" in currentItem)) {
114
114
-
console.error(
115
115
-
`Error: totalItems[${item}] does not have a 'text' property.`,
116
116
-
);
117
117
-
continue; // Skip this item if it doesn't have 'text'
118
118
-
}
119
119
-
renderText(
120
120
-
ctx,
121
121
-
currentItem.text,
122
122
-
xOffset + 40,
123
123
-
fontSize * 1.5 * line + 40,
124
124
-
fontSize,
125
125
-
fontFamily,
126
126
-
);
127
127
-
}
128
128
-
129
129
-
return canvas;
130
130
-
131
131
-
function measureText(
132
132
-
textToMeasure: string,
133
133
-
): number {
134
134
-
const tempCanvas = new SvgCanvas(1, 1);
135
135
-
const ctx = tempCanvas.getContext();
136
136
-
ctx.font = `${fontSize}px ${fontFamily}`;
137
137
-
return ctx.measureText(textToMeasure.replace(/\*/g, "")).width;
138
138
-
}
139
139
-
140
140
-
function drawText(
141
141
-
ctx: CanvasRenderingContext2D | SvgRenderingContext2D,
142
142
-
text: string,
143
143
-
x: number,
144
144
-
y: number,
145
145
-
fontSize: number,
146
146
-
fontFamily: string,
147
147
-
) {
148
148
-
const fontStyle = isItalic ? "italic" : "normal";
149
149
-
ctx.font = `${fontStyle} ${fontSize}px ${fontFamily}`;
150
150
-
ctx.fillStyle = "black";
151
151
-
ctx.textAlign = "left";
152
152
-
ctx.textBaseline = "top";
153
153
-
ctx.fillText(text, x, y);
154
154
-
}
155
155
-
156
156
-
// Helper function to handle italicization
157
157
-
function renderText(
158
158
-
ctx: CanvasRenderingContext2D | SvgRenderingContext2D,
159
159
-
substring: string,
160
160
-
x: number,
161
161
-
y: number,
162
162
-
fontSize: number,
163
163
-
fontFamily: string,
164
164
-
) {
165
165
-
// Split the substring by asterisks
166
166
-
const parts = substring.split("*");
167
167
-
let currentX = x;
168
168
-
169
169
-
// Loop through parts and render each separately
170
170
-
parts.forEach((part, index) => {
171
171
-
if (part.length > 0) {
172
172
-
// Draw the current part
173
173
-
drawText(ctx, part, currentX, y, fontSize, fontFamily);
174
174
-
// Increment x-coordinate for the next segment
175
175
-
const partWidth = ctx.measureText(part).width;
176
176
-
currentX += partWidth;
177
177
-
}
178
178
-
179
179
-
// Toggle italics if we're at an asterisk boundary
180
180
-
if (index < parts.length - 1) {
181
181
-
isItalic = !isItalic;
182
182
-
}
183
183
-
});
184
184
-
}
185
185
-
}
186
186
-
187
187
-
// SVG wrapper
188
188
-
export function generateSvg(quote: string, path: string | null = null) {
189
189
-
const createLocalCanvas = (width: number, height: number) => {
190
190
-
const canvas = new SvgCanvas(width, height);
191
191
-
const ctx = canvas.getContext();
192
192
-
193
193
-
return { canvas, ctx };
194
194
-
};
195
195
-
196
196
-
const svgCanvas: SvgCanvas = generateCanvas(createLocalCanvas, quote);
197
197
-
svgCanvas.complete();
198
198
-
if (path) {
199
199
-
svgCanvas.save(path);
200
200
-
}
201
201
-
return svgCanvas.encode();
202
202
-
}
203
203
-
204
204
-
// PNG wrapper
205
205
-
export function generatePng(quote: string, path: string | null = null) {
206
206
-
const createLocalCanvas = (width: number, height: number) => {
207
207
-
const canvas = createCanvas(width, height);
208
208
-
const ctx = canvas.getContext("2d");
209
209
-
210
210
-
return { canvas, ctx };
211
211
-
};
212
212
-
213
213
-
const pngCanvas: Canvas = generateCanvas(createLocalCanvas, quote);
214
214
-
const aspectRatio = { width: pngCanvas.width, height: pngCanvas.height };
215
215
-
if (path) {
216
216
-
pngCanvas.save(path);
217
217
-
}
218
218
-
return { image: pngCanvas.encode("png"), aspectRatio};
219
219
-
}
+216
utils/imageUtils.ts
···
1
1
+
import tex from "npm:tex-linebreak";
2
2
+
import enUsPatterns from "npm:hyphenation.en-us";
3
3
+
import * as fontkit from "npm:fontkit";
4
4
+
import { Image } from "https://deno.land/x/imagescript@1.3.0/mod.ts";
5
5
+
import { Buffer } from "node:buffer";
6
6
+
7
7
+
const lineWidth = 620;
8
8
+
const defaultFontSize = 20;
9
9
+
const indentSize = 30;
10
10
+
const padding = 40;
11
11
+
12
12
+
interface Item {
13
13
+
text: string;
14
14
+
posX: number;
15
15
+
line: number;
16
16
+
isItalic?: boolean;
17
17
+
}
18
18
+
19
19
+
type Font = {
20
20
+
url: string;
21
21
+
family: string;
22
22
+
weight: string;
23
23
+
data?: ArrayBuffer;
24
24
+
};
25
25
+
26
26
+
const fonts: Record<string, Font> = {
27
27
+
normal: {
28
28
+
url: "https://github.com/google/fonts/raw/refs/heads/main/ofl/merriweather/Merriweather-Regular.ttf",
29
29
+
family: "Merriweather",
30
30
+
weight: "400",
31
31
+
},
32
32
+
italic: {
33
33
+
url: "https://github.com/google/fonts/raw/refs/heads/main/ofl/merriweather/Merriweather-Italic.ttf",
34
34
+
family: "Merriweather",
35
35
+
weight: "400",
36
36
+
},
37
37
+
};
38
38
+
39
39
+
await Promise.all(
40
40
+
Object.entries(fonts).map(async ([_key, font]) => {
41
41
+
const response = await fetch(font.url);
42
42
+
const fontData = await response.arrayBuffer();
43
43
+
font.data = fontData;
44
44
+
}),
45
45
+
);
46
46
+
47
47
+
function formatText(quote: string) {
48
48
+
const paragraphs = quote.split("\\n");
49
49
+
const totalItems: tex.TextInputItem[][] = [];
50
50
+
const totalPositionedItems: tex.PositionedItem[][] = [];
51
51
+
52
52
+
const _hyphenate = tex.createHyphenator(enUsPatterns);
53
53
+
54
54
+
let lineOffset = 0;
55
55
+
let itemOffset = 0;
56
56
+
57
57
+
for (const paragraph of paragraphs) {
58
58
+
const items = tex.layoutItemsFromString(
59
59
+
paragraph.trim(),
60
60
+
measureText,
61
61
+
// hyphenate, // disabled for now
62
62
+
);
63
63
+
totalItems.push(items);
64
64
+
65
65
+
// Create an array of line widths where the first line is indented
66
66
+
const lineWidths = items.map((_, index) =>
67
67
+
index === 0 ? lineWidth - indentSize : lineWidth
68
68
+
);
69
69
+
70
70
+
// Find where to insert line-breaks using the varying line widths
71
71
+
const breakpoints = tex.breakLines(items, lineWidths);
72
72
+
73
73
+
// Compute positions with indentation for the first line
74
74
+
const positionedItems = tex.positionItems(
75
75
+
items,
76
76
+
lineWidths,
77
77
+
breakpoints,
78
78
+
);
79
79
+
80
80
+
// Add the indent to the first line's xOffset
81
81
+
const adjustedPositionedItems = positionedItems.map((
82
82
+
element,
83
83
+
_idx,
84
84
+
) => ({
85
85
+
...element,
86
86
+
item: element.item + itemOffset,
87
87
+
line: element.line + lineOffset,
88
88
+
xOffset: element.line === 0
89
89
+
? element.xOffset + indentSize
90
90
+
: element.xOffset,
91
91
+
}));
92
92
+
93
93
+
totalPositionedItems.push(adjustedPositionedItems);
94
94
+
95
95
+
lineOffset += positionedItems[positionedItems.length - 1].line + 1;
96
96
+
itemOffset += items.length;
97
97
+
}
98
98
+
99
99
+
const items: Item[] = [];
100
100
+
101
101
+
let noPenalty = false;
102
102
+
for (const positionedItem of totalPositionedItems.flat()) {
103
103
+
const { xOffset, line, item } = positionedItem;
104
104
+
const box = totalItems.flat()[item];
105
105
+
let text: string;
106
106
+
let penalty = 0;
107
107
+
108
108
+
// Penalty calculation, needs some work, unused for now
109
109
+
if (!("text" in box)) {
110
110
+
text = "-";
111
111
+
penalty += 3;
112
112
+
noPenalty = true;
113
113
+
} else {
114
114
+
text = box.text;
115
115
+
}
116
116
+
if (
117
117
+
item > 0 && totalItems.flat()[item - 1].type === "penalty" &&
118
118
+
noPenalty === false
119
119
+
) {
120
120
+
penalty += 3;
121
121
+
noPenalty = false;
122
122
+
}
123
123
+
124
124
+
items.push({
125
125
+
text: text,
126
126
+
posX: xOffset, // + penalty,
127
127
+
line: line,
128
128
+
});
129
129
+
}
130
130
+
const styledItems = getItalics(items);
131
131
+
132
132
+
return styledItems;
133
133
+
}
134
134
+
135
135
+
function measureText(
136
136
+
text: string,
137
137
+
fontSize: number = defaultFontSize,
138
138
+
fontData: ArrayBuffer = fonts.normal.data!,
139
139
+
) {
140
140
+
// Parse font
141
141
+
const font = fontkit.create(Buffer.from(fontData));
142
142
+
143
143
+
// Get width (scale based on fontSize)
144
144
+
const scale = fontSize / font.unitsPerEm;
145
145
+
const width = font.layout(
146
146
+
text.replace(/\*/g, "")
147
147
+
.replace(" ", " "), // hack, because single space made it look smushed
148
148
+
).advanceWidth * scale;
149
149
+
150
150
+
return width;
151
151
+
}
152
152
+
153
153
+
function getItalics(items: Item[]) {
154
154
+
let isItalic = false;
155
155
+
const styledItems: Item[] = [];
156
156
+
157
157
+
for (const item of items) {
158
158
+
const parts = item.text.split("*");
159
159
+
let currentX = item.posX;
160
160
+
parts.forEach((part, index) => {
161
161
+
if (part.length > 0) {
162
162
+
styledItems.push({
163
163
+
text: part,
164
164
+
posX: currentX,
165
165
+
line: item.line,
166
166
+
isItalic: isItalic,
167
167
+
});
168
168
+
currentX += measureText(part);
169
169
+
}
170
170
+
171
171
+
// Toggle italics if we're at an asterisk boundary
172
172
+
if (index < parts.length - 1) {
173
173
+
isItalic = !isItalic;
174
174
+
}
175
175
+
});
176
176
+
}
177
177
+
178
178
+
return styledItems;
179
179
+
}
180
180
+
181
181
+
export async function generateImage(quote: string) {
182
182
+
const items = formatText(quote);
183
183
+
184
184
+
const nLines = items[items.length - 1].line;
185
185
+
const width = lineWidth + padding * 2;
186
186
+
const height = nLines * defaultFontSize * 1.5 + padding * 2.5;
187
187
+
188
188
+
const image = new Image(width, height);
189
189
+
image.fill(0xFFFFF0FF);
190
190
+
191
191
+
for (const item of items) {
192
192
+
const { text, posX, line } = item;
193
193
+
194
194
+
const font = item.isItalic
195
195
+
? new Uint8Array(fonts.italic.data!)
196
196
+
: new Uint8Array(fonts.normal.data!);
197
197
+
198
198
+
const textImage = Image.renderText(
199
199
+
font,
200
200
+
defaultFontSize,
201
201
+
text,
202
202
+
0x000000FF,
203
203
+
);
204
204
+
205
205
+
image.composite(
206
206
+
textImage,
207
207
+
posX + 40,
208
208
+
defaultFontSize * 1.5 * line + 40,
209
209
+
);
210
210
+
}
211
211
+
212
212
+
return {
213
213
+
image: await image.encode(),
214
214
+
aspectRatio: { width: width, height: height },
215
215
+
};
216
216
+
}