1import { createCanvas, loadImage } from "https://deno.land/x/canvas/mod.ts";
2import { blob } from "https://esm.town/v/std/blob";
3import { BLOB_PREFIX, POST_PREFIX, Slam } from "https://esm.town/v/dupontgu/findSlamArticles";
4import { Counter } from "https://esm.town/v/dupontgu/persistentCounter"
5import { blueskyPostWithImage } from "https://esm.town/v/dupontgu/heavenlyOrangeMarmoset"
6
7// Replace with your actual access token and Mastodon instance URL
9const MASTODON_URL = "https://mastodon.social";
10
11interface Image {
12 url: string
13 // text locations within image, Vec3: [x, y, rotation]
14 slammer_txt_loc: number[]
15 slammed_txt_loc: number[],
17 alt_txt: string
18}
19const images: Image[] = [
20 {
21 url:"https://bloximages.newyork1.vip.townnews.com/gazette.com/content/tncms/assets/v3/editorial/d/ea/dea0914b-9915-553c-bc42-81313201d4ac/5b32bb43c0f7e.image.jpg",
22 slammer_txt_loc: [140, 190, -0.4],
23 slammed_txt_loc: [90, 280, -0.0],
40 },
41 {
42 url:"https://media.licdn.com/dms/image/v2/C4E12AQGcr20PW6r-Sw/article-cover_image-shrink_423_752/article-cover_image-shrink_423_752/0/1520115254001?e=1733356800&v=beta&t=OVWwDx4QhdF1IaMjSOOMpLRGRCnSVGp7Il_mERmKBGc",
43 slammer_txt_loc: [180, 120, -0.0],
44 slammed_txt_loc: [280, 330, -0.0],
61 },
62 {
63 url:"https://i.dailymail.co.uk/1s/2024/03/19/04/82623845-13212905-image-a-89_1710821176313.jpg",
64 slammer_txt_loc: [310, 440, -0.0],
65 slammed_txt_loc: [170, 790, -0.0],
82 },
83 {
84 url:"https://static.wikia.nocookie.net/international-pokedex/images/6/6a/Body_Slam_%28Ash%27s_Snorlax%29.png",
85 slammer_txt_loc: [280, 100, -0.0],
86 slammed_txt_loc: [240, 430, -0.0],
96 },
97 {
98 url:"https://static.wikia.nocookie.net/baki/images/2/22/Tackle.png/revision/latest?cb=20170117232328",
99 slammer_txt_loc: [150, 100, -0.0],
100 slammed_txt_loc: [510, 350, -0.0],
110 },
111 {
112 url:"https://static.wikia.nocookie.net/dragonball/images/2/2b/Gigantic_Spike.png/revision/latest/scale-to-width-down/1000?cb=20180515222853",
113 slammer_txt_loc: [270, 100, -0.0],
114 slammed_txt_loc: [340, 500, -0.0],
125]
126
127async function uploadImage(imageBuffer: any, altText: string): Promise<string> {
128 const formData = new FormData();
129 formData.append("file", new Blob([imageBuffer], { type: "image/png" }));
130 formData.append("description", altText);
131
139
140 if (!uploadResponse.ok) {
141 throw new Error("Failed to upload image");
142 }
143
146}
147
148async function postStatusWithImage(statusText: string, imageBuffer: any, altText: string) {
149 const mediaId = await uploadImage(imageBuffer, altText);
150
151 const postResponse = await fetch(`${MASTODON_URL}/api/v1/statuses`, {
157 body: JSON.stringify({
158 status: statusText,
159 media_ids: [mediaId], // Attach the uploaded image by its media ID
160 }),
161 });
170}
171
172async function overlayTextOnImage(slammer_text: string, slammed_text: string, imageDef: Image): Promise<Uint8Array> {
173 const image = await loadImage(imageDef.url);
174 let fontFile = await fetch(
175 "https://github.com/sophilabs/macgifer/raw/master/static/font/impact.ttf",
177 const fontBlob = await fontFile.blob();
178 let fontBytes = new Uint8Array(await fontBlob.arrayBuffer());
179 const canvas = createCanvas(image.width(), image.height());
180 canvas.loadFont(fontBytes, { family: "Impact" });
181 const ctx = canvas.getContext('2d');
182
183 ctx.drawImage(image, 0, 0, image.width(), image.height());
184 ctx.fillStyle = 'white';
185 ctx.strokeStyle = 'black';
189
190 const baseTextSize = 46;
191 const slammer_size = (baseTextSize * imageDef.text_scale) - (slammer_text.length * imageDef.text_scale * 0.84)
192 ctx.font = `${slammer_size}px Impact`;
193 ctx.rotate(imageDef.slammer_txt_loc[2])
194 let xAdjust = (slammer_text.length / 2) * 6
195 ctx.strokeText(slammer_text, imageDef.slammer_txt_loc[0] - xAdjust, imageDef.slammer_txt_loc[1]);
196 ctx.fillText(slammer_text, imageDef.slammer_txt_loc[0] - xAdjust, imageDef.slammer_txt_loc[1]);
197
198 // reset rotation
199 ctx.rotate(-imageDef.slammer_txt_loc[2])
200 const slammed_size = (baseTextSize * imageDef.text_scale) - (slammed_text.length * imageDef.text_scale * 0.84)
201 ctx.font = `${slammed_size}px Impact`;
202 ctx.rotate(imageDef.slammed_txt_loc[2])
203 xAdjust = (slammed_text.length / 2) * 6
204 ctx.strokeText(slammed_text, imageDef.slammed_txt_loc[0] - xAdjust, imageDef.slammed_txt_loc[1]);
205 ctx.fillText(slammed_text, imageDef.slammed_txt_loc[0] - xAdjust, imageDef.slammed_txt_loc[1]);
206
207 return canvas.toBuffer('image/png');
208}
209
216 const postCounter = new Counter("SLAM_post_counter");
217 const count = await postCounter.get()
218 const image = images[count % images.length]
219 // const image = images[images.length - 1]
220 const modifiedImageBuffer = await overlayTextOnImage(slam.slammer, slam.slammed, image);
221 const adjustedHeadline = slam.txt.replace('slams', 'SLAMS').replace('Slams', 'SLAMS')
222 const statusText = `${adjustedHeadline}\n\n via \n${slam.url}`
223 const altText = `${image.alt_txt}\n The slammer is labeled with ${slam.slammer} and the person or thing being slammed is labeled ${slam.slammed}.\n\n image source: ${image.url}`
224 const mastoUrl = await postStatusWithImage(statusText, modifiedImageBuffer, altText)
225 await blob.delete(inputs[0].key)
226 await blob.setJSON(POST_PREFIX + slam.url, {url:mastoUrl})
227 await postCounter.increment()
228 await blueskyPostWithImage(statusText, modifiedImageBuffer, altText)
229 return new Response(modifiedImageBuffer, {
230 headers: {
231 'Content-Type': 'image/png',
232 },
233 });