bskyogcard: support emoji, more languages, long starter pack names (#4668)
parent
f6b138f709
commit
49396451ec
|
@ -1,8 +1,6 @@
|
|||
name: build-and-push-ogcard-aws
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- divy/bskycard
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
|
|
|
@ -115,3 +115,6 @@ src/locale/locales/**/*.js
|
|||
*.apk
|
||||
*.aab
|
||||
*.ipa
|
||||
|
||||
# ogcard assets
|
||||
bskyogcard/src/assets/fonts/noto-*
|
||||
|
|
|
@ -10,7 +10,7 @@ RUN yarn install --frozen-lockfile
|
|||
COPY ./bskyogcard ./
|
||||
|
||||
# build then prune dev deps
|
||||
RUN yarn build
|
||||
RUN yarn install-fonts && yarn build
|
||||
RUN yarn install --production --ignore-scripts --prefer-offline
|
||||
|
||||
# Uses assets from build stage to reduce build size
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "node --loader ts-node/esm ./src/bin.ts",
|
||||
"build": "tsc && cp -r src/assets dist/assets"
|
||||
"dev": "node --watch-path ./src --loader ts-node/esm ./src/bin.ts",
|
||||
"build": "tsc && cp -r src/assets dist/",
|
||||
"install-fonts": "node --loader ts-node/esm scripts/install-fonts.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.12.19-next.0",
|
||||
|
@ -15,10 +17,12 @@
|
|||
"http-terminator": "^3.2.0",
|
||||
"pino": "^9.2.0",
|
||||
"react": "^18.3.1",
|
||||
"satori": "^0.10.13"
|
||||
"satori": "^0.10.13",
|
||||
"twemoji": "^14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import {writeFile} from 'node:fs/promises'
|
||||
import * as path from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
|
||||
const __DIRNAME = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const FONTS = [
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-jp@5.0/japanese-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-tc@5.0/chinese-traditional-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-sc@5.0/chinese-simplified-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hk@5.0/chinese-hongkong-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-kr@5.0/korean-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-thai@5.0/thai-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-arabic@5.0/arabic-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hebrew@5.0/hebrew-700-normal.ttf',
|
||||
]
|
||||
|
||||
async function main() {
|
||||
await Promise.all(
|
||||
FONTS.map(async urlStr => {
|
||||
const url = new URL(urlStr)
|
||||
const res = await fetch(url)
|
||||
const font = await res.arrayBuffer()
|
||||
const filename = url.pathname
|
||||
.split('/')
|
||||
.slice(-2)
|
||||
.join('/')
|
||||
.replace(/@[\d.]+\//, '-')
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: fetching failed for ${filename}`)
|
||||
}
|
||||
await writeFile(
|
||||
path.join(__DIRNAME, '..', 'src', 'assets', 'fonts', filename),
|
||||
Buffer.from(font),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
main()
|
|
@ -43,6 +43,7 @@ export function StarterPack(props: {
|
|||
} else {
|
||||
imagesAcross.push(...imagesExceptCreator.slice(0, 7))
|
||||
}
|
||||
const isLongTitle = record ? record.name.length > 30 : false
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
@ -130,7 +131,9 @@ export function StarterPack(props: {
|
|||
<div
|
||||
style={{
|
||||
padding: '75px 30px 0px',
|
||||
fontSize: 65,
|
||||
fontSize: isLongTitle ? 55 : 65,
|
||||
display: 'flex',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
{record?.name || 'Starter Pack'}
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {readFileSync} from 'node:fs'
|
||||
import {readdirSync, readFileSync} from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
|
||||
import {AtpAgent} from '@atproto/api'
|
||||
import * as path from 'path'
|
||||
import {fileURLToPath} from 'url'
|
||||
|
||||
import {Config} from './config.js'
|
||||
|
||||
|
@ -28,12 +28,14 @@ export class AppContext {
|
|||
|
||||
static async fromConfig(cfg: Config, overrides?: Partial<AppContextOptions>) {
|
||||
const appviewAgent = new AtpAgent({service: cfg.service.appviewUrl})
|
||||
const fonts = [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: readFileSync(path.join(__DIRNAME, 'assets', 'Inter-Bold.ttf')),
|
||||
},
|
||||
]
|
||||
const fontDirectory = path.join(__DIRNAME, 'assets', 'fonts')
|
||||
const fontFiles = readdirSync(fontDirectory)
|
||||
const fonts = fontFiles.map(file => {
|
||||
return {
|
||||
name: path.basename(file, path.extname(file)),
|
||||
data: readFileSync(path.join(fontDirectory, file)),
|
||||
}
|
||||
})
|
||||
return new AppContext({
|
||||
cfg,
|
||||
appviewAgent,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {subsystemLogger} from '@atproto/common'
|
||||
|
||||
export const httpLogger = subsystemLogger('bskyogcard')
|
||||
export const renderLogger = subsystemLogger('bskyogcard:render')
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from '../components/StarterPack.js'
|
||||
import {AppContext} from '../context.js'
|
||||
import {httpLogger} from '../logger.js'
|
||||
import {loadEmojiAsSvg} from '../util.js'
|
||||
import {handler, originVerifyMiddleware} from './util.js'
|
||||
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
|
@ -65,6 +66,11 @@ export default function (ctx: AppContext, app: Express) {
|
|||
fonts: ctx.fonts,
|
||||
height: STARTERPACK_HEIGHT,
|
||||
width: STARTERPACK_WIDTH,
|
||||
loadAdditionalAsset: async (code, text) => {
|
||||
if (code === 'emoji') {
|
||||
return await loadEmojiAsSvg(text)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
const output = await resvg.renderAsync(svg)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import twemoji from 'twemoji'
|
||||
|
||||
import {renderLogger} from './logger.js'
|
||||
|
||||
const U200D = String.fromCharCode(0x200d)
|
||||
const UFE0F_REGEXP = /\uFE0F/g
|
||||
|
||||
export async function loadEmojiAsSvg(chars: string) {
|
||||
const cached = emojiCache.get(chars)
|
||||
if (cached) return cached
|
||||
const iconCode = twemoji.convert.toCodePoint(
|
||||
chars.indexOf(U200D) < 0 ? chars.replace(UFE0F_REGEXP, '') : chars,
|
||||
)
|
||||
const res = await fetch(getEmojiUrl(iconCode))
|
||||
const body = await res.arrayBuffer()
|
||||
if (!res.ok) {
|
||||
renderLogger.warn(
|
||||
{status: res.status, err: Buffer.from(body).toString()},
|
||||
'could not fetch emoji',
|
||||
)
|
||||
return
|
||||
}
|
||||
const svg =
|
||||
'data:image/svg+xml;base64,' + Buffer.from(body).toString('base64')
|
||||
emojiCache.set(chars, svg)
|
||||
return svg
|
||||
}
|
||||
|
||||
const emojiCache = new Map<string, string>()
|
||||
|
||||
function getEmojiUrl(code: string) {
|
||||
return (
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/' +
|
||||
code.toLowerCase() +
|
||||
'.svg'
|
||||
)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue