Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

增加 OG Image 產生功能 #16

Merged
merged 2 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public/json/ytLink.json
public/json/community.json
public/json/topics.json
public/sitemap.xml
public/images/sessions/*
src/assets/json/announcement.json
src/assets/json/session.json
src/assets/json/staff.json
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@vueuse/core": "^10.0.2",
"@vueuse/shared": "^10.1.2",
"axios": "^1.3.5",
"canvas": "^2.11.2",
"js-md5": "^0.7.3",
"lodash-es": "^4.17.21",
"markdown-it": "^13.0.1",
Expand Down
195 changes: 195 additions & 0 deletions scripts/pre-build/generateOGImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import fs from 'fs';
import path from 'path';
import { createCanvas, loadImage, registerFont, CanvasRenderingContext2D } from 'canvas';

interface Session {
id: string;
type: string;
room: string;
start: string;
end: string;
language: string;
zh: {
title: string;
description: string;
};
en: {
title: string;
description: string;
};
speakers: string[];
tags: string[];
co_write: string;
record: string;
uri: string;
}

interface Speaker {
id: string;
avatar: string;
zh: {
name: string;
bio: string;
};
en: {
name: string;
bio: string;
};
}

interface Session_types{
id: string;
zh:{name:string};
en:{name:string};
}

interface SomethingById {
[id: string]: string;
}

export default async function generateOGImage(){
return {
name: 'vite-plugin-generate-og-images',
async buildStart() {
console.log('Start OG generating');
registerFont(path.resolve(__dirname, '../../src/assets/fonts/TaipeiSansTCBeta-Bold/TaipeiSansTCBeta-Bold.ttf'), { family: 'TaipeiSansTCBeta-Bold' });
// reading session.json
const sessionsData: { sessions: Session[] } = JSON.parse(fs.readFileSync('./src/assets/json/session.json', 'utf-8'));
const speakersData: { speakers: Speaker[] } = JSON.parse(fs.readFileSync('./src/assets/json/session.json', 'utf-8'));
const session_typesData: { session_types: Session_types[] } = JSON.parse(fs.readFileSync('./src/assets/json/session.json', 'utf-8'));

// create dictionary for decode json speakerID & type
const nameById: { [id: string]: string } = {};
speakersData.speakers.forEach(speaker => {
nameById[speaker.id] = speaker.zh.name;
});
const typeById: { [id: string]: string } = {};
session_typesData.session_types.forEach(type => {
typeById[type.id] = type.zh.name;
});

// generate image
for (const session of sessionsData.sessions) {
await generateSessionImage(session,typeById,nameById);
}

console.log('All OG images have been generated.');
},
};
}

function wrapTitle(context: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number) {
const words = text.split(' ');
let line = '';

for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
context.fillText(line, x, y);
line = words[n] + ' ';
y += lineHeight;
} else {
line = testLine;
}
}
context.fillText(line, x, y);
}

function drawRoundedRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.fill();
}

function drawTag(ctx: CanvasRenderingContext2D, tag: string, x: number, y: number, padding: number = 10, radius: number = 10): number {
ctx.font = '16px TaipeiSansTCBeta-Bold'; // 设置字体大小和类型
const metrics = ctx.measureText(tag); // 测量文本
const textWidth = metrics.width;
const textHeight = 20; // 假定文本高度为20像素
const rectWidth = textWidth + 2 * padding;
const rectHeight = textHeight + 2 * padding;

// 绘制圆角矩形背景,添加透明度
ctx.fillStyle = 'rgba(100, 100, 240, 0.4)'; // 背景色加透明度
drawRoundedRect(ctx, x, y, rectWidth, rectHeight, radius);

// 绘制文本
ctx.fillStyle = '#000'; // 文本颜色
ctx.fillText(tag, x + padding, y + padding + textHeight / 2 + 4); // 稍微调整y坐标使文本垂直居中

// 返回下一个标签的起始 x 坐标,包括间隔
return x + rectWidth + 15;
}


async function generateSessionImage(session: Session,typeById: SomethingById,nameById: SomethingById): Promise<void> {
const dirPath = path.resolve(__dirname, '../../public/images/sessions');
// Check if the directory exists, if not, create it
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
console.log(`Directory created at ${dirPath}`);
}

const outputPath = path.join(dirPath, `${session.id}.png`);
// check if the picture exists
if (fs.existsSync(outputPath)) {
// console.log(`Image for session ${session.id} already exists at ${outputPath}. Skipping generation.`);
return;
}

// create canvas
const canvasWidth = 1200;
const canvasHeight = 630;
const canvas = createCanvas(canvasWidth, canvasHeight);
const ctx = canvas.getContext('2d') ;
// set background image
const background = await loadImage('./src/assets/images/og_background.jpg');
ctx.drawImage(background, 0, 0, canvasWidth, canvasHeight);

// set painter property
ctx.fillStyle = '#000000';
ctx.font = '36px TaipeiSansTCBeta-Bold';

// draw title
const x = 150;
const y = 190;
const maxWidth = 900;
const lineHeight = 40;
wrapTitle(ctx, session.zh.title, x, y, maxWidth, lineHeight);

// draw tag
const tags = session.tags;
let currentX = 150;
let currentY = 105;
tags.forEach(tag => {
currentX = drawTag(ctx, tag, currentX, currentY); // 更新 currentX 到下一个标签的位置
});
// draw speaker
const speakers = session.speakers;
let currentX_spk = 150;
let currentY_spk = 430;
speakers.forEach(speaker => {
currentX = drawTag(ctx, nameById[speaker], currentX_spk, currentY_spk); // 更新 currentX 到下一个标签的位置
});
// draw session type
const type = session.type;
ctx.font = '24px TaipeiSansTCBeta-Bold';
ctx.fillText(typeById[type], 150, 500);

// save image
const buffer = canvas.toBuffer('image/png');
fs.writeFileSync(outputPath, buffer);
console.log(`Generated image for session ${session.id} at ${outputPath}`);
}

Binary file not shown.
Binary file added src/assets/images/og_background.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions src/modules/metas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const _useMetas = (): UseMetas => {
setMetas(defaultMetaValues)
}

const { lang, title, description, ogImage, ogSiteName, ogType, ogUrl } = metaRefs
const { lang, title,description, ogImage, ogSiteName, ogType, ogUrl } = metaRefs

useHead({
htmlAttrs: {
Expand All @@ -70,7 +70,6 @@ const _useMetas = (): UseMetas => {
title,
meta: [
{ name: 'description', content: description },
{ property: 'og:title', content: title },
{ property: 'og:description', content: description },
{ property: 'og:url', content: ogUrl },
{ property: 'og:image', content: ogImage },
Expand Down
6 changes: 5 additions & 1 deletion src/modules/session/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,15 @@ export function generateSessionPopupContentHtml (session: Session, community: {
}

export function generateSessionMetaOptions (session: Session, locale: Locale): MetaOptions {
console.log('Session title:', session[locale].title);
console.log('Image part: ',`${getRootUrl()}images/sessions/${session.id}.png`);
console.log('Local:',`${locale}`)
return {
title: session[locale].title,
description: escape(truncate(session[locale].description, { length: 80 })),
ogTitle: session[locale].title,
ogUrl: `${getRootUrl()}${locale}/session/${session.id}`,
ogImage: session.speakers.length > 0 ? session.speakers[Math.floor(Math.random() * session.speakers.length)].avatar : undefined
ogImage: `${getRootUrl()}/images/sessions/${session.id}.png`,
}
}

Expand Down
4 changes: 3 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Components from 'unplugin-vue-components/vite'
import ViteIcons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import { VitePWA } from 'vite-plugin-pwa'
import generateOGImage from './scripts/pre-build/generateOGImage'

export default defineConfig(({ mode, command }) => {
const parsed = loadEnv(mode, process.cwd())
Expand Down Expand Up @@ -143,7 +144,8 @@ export default defineConfig(({ mode, command }) => {
]
: []
}
})
}),
generateOGImage()
],
ssgOptions: {
script: 'async',
Expand Down