bskyogcard: support emoji, more languages, long starter pack names (#4668)
This commit is contained in:
		
							parent
							
								
									f6b138f709
								
							
						
					
					
						commit
						49396451ec
					
				
					 12 changed files with 413 additions and 163 deletions
				
			
		| 
						 | 
				
			
			@ -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 }}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -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"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										40
									
								
								bskyogcard/scripts/install-fonts.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								bskyogcard/scripts/install-fonts.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										37
									
								
								bskyogcard/src/util.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								bskyogcard/src/util.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue