@liria24/og-image 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,160 +0,0 @@
1
- import type {
2
- ImageNode,
3
- ImageSource,
4
- ConstructRendererOptions,
5
- Node,
6
- RenderOptions,
7
- } from 'takumi-js/wasm'
8
- import type { GenericSchema, InferOutput } from 'valibot'
9
-
10
- type PresetRenderOptions = Omit<RenderOptions, 'width' | 'height' | 'format' | 'devicePixelRatio'>
11
- type PresetPropsSchema = GenericSchema
12
- type FontTextValue = string | null | undefined | false
13
- type FontText = string | readonly FontTextValue[]
14
-
15
- export interface GoogleFontConfig {
16
- family: string
17
- options?: Omit<import('takumi-js/helpers').GoogleFontOptions, 'text'>
18
- }
19
-
20
- interface DefinePresetOptions<TPropsSchema extends PresetPropsSchema> {
21
- version: string
22
- props: TPropsSchema
23
- fonts: readonly GoogleFontConfig[]
24
- fontText: (props: InferOutput<TPropsSchema>) => FontText
25
- content: (props: InferOutput<TPropsSchema>) => Node
26
- width?: number
27
- height?: number
28
- format?: RenderOptions['format']
29
- devicePixelRatio?: number
30
- persistentImages?: ConstructRendererOptions['persistentImages']
31
- renderOptions?: PresetRenderOptions
32
- }
33
-
34
- interface PresetRenderConfig {
35
- width: number
36
- height: number
37
- format: RenderOptions['format']
38
- devicePixelRatio: number
39
- }
40
-
41
- export interface OgImagePreset {
42
- slug: string
43
- version: string
44
- props: PresetPropsSchema
45
- fonts: readonly GoogleFontConfig[]
46
- fontText: (props: unknown) => string
47
- persistentImages?: ConstructRendererOptions['persistentImages']
48
- renderOptions: PresetRenderConfig
49
- content: (props: unknown) => Node
50
- }
51
-
52
- type DefinedOgImagePreset<TPropsSchema extends PresetPropsSchema> = Omit<
53
- OgImagePreset,
54
- 'slug' | 'props' | 'fontText' | 'content'
55
- > & {
56
- props: TPropsSchema
57
- fontText: (props: InferOutput<TPropsSchema>) => string
58
- content: (props: InferOutput<TPropsSchema>) => Node
59
- }
60
-
61
- const normalizeFontText = (value: FontText) =>
62
- typeof value === 'string'
63
- ? value
64
- : value.filter((text): text is string => Boolean(text)).join('\n')
65
-
66
- export const definePreset = <const TPropsSchema extends PresetPropsSchema>(
67
- options: DefinePresetOptions<TPropsSchema>,
68
- ): DefinedOgImagePreset<TPropsSchema> => {
69
- const {
70
- fontText,
71
- width = 1200,
72
- height = 630,
73
- format = 'png',
74
- devicePixelRatio = 1,
75
- renderOptions,
76
- ...preset
77
- } = options
78
-
79
- return {
80
- ...preset,
81
- fontText: (props) => normalizeFontText(fontText(props)),
82
- renderOptions: {
83
- width,
84
- height,
85
- format,
86
- devicePixelRatio,
87
- ...renderOptions,
88
- },
89
- }
90
- }
91
-
92
- interface SvgImageAsset {
93
- src: string
94
- svg: string
95
- }
96
-
97
- interface DefineSvgImageOptions {
98
- color: string
99
- width: number
100
- height: number
101
- src?: string
102
- }
103
-
104
- const setStyleColor = (style: string, color: string) => {
105
- const normalizedStyle = style.trim().replace(/;+$/, '')
106
- const nextStyle = normalizedStyle.match(/(^|;)\s*color\s*:/i)
107
- ? normalizedStyle.replace(/(^|;)\s*color\s*:[^;]*/i, `$1 color: ${color}`)
108
- : [normalizedStyle, `color: ${color}`].filter(Boolean).join('; ')
109
-
110
- return nextStyle.trim().replace(/;?$/, ';')
111
- }
112
-
113
- const escapeAttribute = (value: string) =>
114
- value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;')
115
-
116
- const withSvgRootColor = (svg: string, color: string) => {
117
- let foundSvgRoot = false
118
- const replaced = svg.replace(/<svg\b([^>]*)>/i, (_tag, attributes: string) => {
119
- foundSvgRoot = true
120
- const nextAttributes = attributes.match(/\sstyle=(["'])(.*?)\1/i)
121
- ? attributes.replace(
122
- /\sstyle=(["'])(.*?)\1/i,
123
- (_style, quote: string, style: string) =>
124
- ` style=${quote}${escapeAttribute(setStyleColor(style, color))}${quote}`,
125
- )
126
- : `${attributes} style="color: ${escapeAttribute(color)};"`
127
-
128
- return `<svg${nextAttributes}>`
129
- })
130
-
131
- if (!foundSvgRoot) throw new Error('SVG root element was not found.')
132
-
133
- return replaced
134
- }
135
-
136
- export const defineSvgImage = (
137
- asset: SvgImageAsset,
138
- { color, height, src = asset.src, width }: DefineSvgImageOptions,
139
- ): { image: ImageSource; node: ImageNode } => ({
140
- image: {
141
- src,
142
- data: new TextEncoder().encode(withSvgRootColor(asset.svg, color)),
143
- },
144
- node: {
145
- type: 'image',
146
- src,
147
- width,
148
- height,
149
- },
150
- })
151
-
152
- export const withPresetId = <const TPropsSchema extends PresetPropsSchema>(
153
- preset: DefinedOgImagePreset<TPropsSchema>,
154
- slug: string,
155
- ): OgImagePreset => ({
156
- ...preset,
157
- slug,
158
- fontText: preset.fontText as (props: unknown) => string,
159
- content: preset.content as (props: unknown) => Node,
160
- })
@@ -1,75 +0,0 @@
1
- import { getReasonPhrase, StatusCodes } from 'http-status-codes'
2
- import { HTTPError } from 'nitro/h3'
3
-
4
- interface ServerErrorOptions {
5
- log?: {
6
- tag?: string
7
- message: string
8
- }
9
- responseMessage?: string
10
- }
11
-
12
- const FAILURE_CACHE_CONTROL = 'no-store'
13
-
14
- export const serverError = {
15
- /** 400 */
16
- badRequest(options?: ServerErrorOptions): never {
17
- if (options?.log) console.error(options.log.message)
18
- throw new HTTPError({
19
- status: StatusCodes.BAD_REQUEST,
20
- statusText: getReasonPhrase(StatusCodes.BAD_REQUEST),
21
- message: options?.responseMessage,
22
- headers: {
23
- 'cache-control': FAILURE_CACHE_CONTROL,
24
- },
25
- })
26
- },
27
- /** 401 */
28
- unauthorized(options?: ServerErrorOptions): never {
29
- if (options?.log) console.error(options.log.message)
30
- throw new HTTPError({
31
- status: StatusCodes.UNAUTHORIZED,
32
- statusText: getReasonPhrase(StatusCodes.UNAUTHORIZED),
33
- message: options?.responseMessage,
34
- headers: {
35
- 'cache-control': FAILURE_CACHE_CONTROL,
36
- },
37
- })
38
- },
39
- /** 403 */
40
- forbidden(options?: ServerErrorOptions): never {
41
- if (options?.log) console.error(options.log.message)
42
- throw new HTTPError({
43
- status: StatusCodes.FORBIDDEN,
44
- statusText: getReasonPhrase(StatusCodes.FORBIDDEN),
45
- message: options?.responseMessage,
46
- headers: {
47
- 'cache-control': FAILURE_CACHE_CONTROL,
48
- },
49
- })
50
- },
51
- /** 404 */
52
- notFound(options?: ServerErrorOptions): never {
53
- if (options?.log) console.error(options.log.message)
54
- throw new HTTPError({
55
- status: StatusCodes.NOT_FOUND,
56
- statusText: getReasonPhrase(StatusCodes.NOT_FOUND),
57
- message: options?.responseMessage,
58
- headers: {
59
- 'cache-control': FAILURE_CACHE_CONTROL,
60
- },
61
- })
62
- },
63
- /** 500 */
64
- internalServerError(options?: ServerErrorOptions): never {
65
- if (options?.log) console.error(options.log.message)
66
- throw new HTTPError({
67
- status: StatusCodes.INTERNAL_SERVER_ERROR,
68
- statusText: getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
69
- message: options?.responseMessage,
70
- headers: {
71
- 'cache-control': FAILURE_CACHE_CONTROL,
72
- },
73
- })
74
- },
75
- }
@@ -1,11 +0,0 @@
1
- import { allPresets } from '#presets'
2
-
3
- const presetMap: Record<string, OgImagePreset> = Object.fromEntries(
4
- allPresets.map((preset) => [preset.slug, preset]),
5
- )
6
-
7
- export const getPreset = (descriptor: OgImageDescriptor): OgImagePreset => {
8
- const preset = presetMap[descriptor.slug]
9
- if (!preset || preset.version !== descriptor.version) throw new Error('Preset not found')
10
- return preset
11
- }
@@ -1,83 +0,0 @@
1
- import init, { Renderer } from '@takumi-rs/wasm'
2
- import wasmModule from '@takumi-rs/wasm/next'
3
- import { googleFont } from 'takumi-js/helpers'
4
-
5
- const RENDER_TIMEOUT_MS = 15_000 // 15 seconds
6
-
7
- const resolved = await wasmModule
8
- const module =
9
- resolved && typeof resolved === 'object' && 'default' in resolved ? resolved.default : resolved
10
- await init({ module_or_path: module })
11
-
12
- type RenderPng = (descriptor: OgImageDescriptor, context?: RenderContext) => Promise<Uint8Array>
13
-
14
- export interface RenderContext {
15
- signal?: AbortSignal
16
- }
17
-
18
- export const renderDescriptor = async (
19
- descriptor: OgImageDescriptor,
20
- context: RenderContext = {},
21
- ): Promise<Uint8Array> => {
22
- if (context.signal?.aborted) throw new Error('Render aborted')
23
-
24
- const preset = getPreset(descriptor)
25
- if (!preset) throw new Error('Unknown renderer')
26
-
27
- if (context.signal?.aborted) throw new Error('Render aborted')
28
-
29
- const renderer = new Renderer({
30
- loadDefaultFonts: false,
31
- persistentImages: preset.persistentImages,
32
- })
33
- try {
34
- const descriptors = (
35
- await Promise.all(
36
- preset.fonts.map((config) =>
37
- googleFont(config.family, {
38
- ...config.options,
39
- text: preset.fontText(descriptor.props),
40
- }),
41
- ),
42
- )
43
- ).flat()
44
-
45
- await renderer.loadFonts(descriptors, context.signal)
46
- return renderer.render(preset.content(descriptor.props), preset.renderOptions)
47
- } finally {
48
- renderer.free()
49
- }
50
- }
51
-
52
- export const pngBytes = (png: ArrayBuffer | ArrayBufferView) =>
53
- png instanceof ArrayBuffer
54
- ? new Uint8Array(png)
55
- : new Uint8Array(png.buffer, png.byteOffset, png.byteLength)
56
-
57
- export const withRenderTimeout = async (
58
- descriptor: OgImageDescriptor,
59
- renderPng: RenderPng,
60
- context: RenderContext = {},
61
- ) => {
62
- const controller = new AbortController()
63
- let timeout: ReturnType<typeof setTimeout> | undefined
64
-
65
- try {
66
- const timeoutPromise = new Promise<never>((_, reject) => {
67
- timeout = setTimeout(() => {
68
- controller.abort()
69
- reject(new Error('OG image render timed out'))
70
- }, RENDER_TIMEOUT_MS)
71
- })
72
-
73
- return await Promise.race([
74
- renderPng(descriptor, {
75
- ...context,
76
- signal: controller.signal,
77
- }),
78
- timeoutPromise,
79
- ])
80
- } finally {
81
- if (timeout) clearTimeout(timeout)
82
- }
83
- }
@@ -1,9 +0,0 @@
1
- import * as v from 'valibot'
2
-
3
- export const ogImageDescriptorBaseSchema = v.object({
4
- slug: v.string(),
5
- version: v.string(),
6
- props: v.optional(v.unknown()),
7
- })
8
-
9
- export type OgImageDescriptor = v.InferOutput<typeof ogImageDescriptorBaseSchema>
@@ -1,4 +0,0 @@
1
- export default async (value: string) => {
2
- const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))
3
- return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('')
4
- }
@@ -1,29 +0,0 @@
1
- import type { H3Event } from 'nitro'
2
- import { getValidatedQuery, getValidatedRouterParams, readValidatedBody } from 'nitro/h3'
3
- import * as v from 'valibot'
4
-
5
- const throwIfFailed = <T extends v.GenericSchema>(
6
- result: v.SafeParseResult<T>,
7
- ): v.InferOutput<T> => {
8
- if (!result.success) {
9
- if (import.meta.dev) console.error(result.issues)
10
- throw serverError.badRequest({ responseMessage: 'Validation Error' })
11
- }
12
- return result.output
13
- }
14
-
15
- export const validateBody = async <const T extends v.GenericSchema>(
16
- event: H3Event,
17
- s: T,
18
- ): Promise<v.InferOutput<T>> => throwIfFailed(await readValidatedBody(event, v.safeParser(s)))
19
-
20
- export const validateParams = async <T extends v.GenericSchema>(
21
- event: H3Event,
22
- s: T,
23
- ): Promise<v.InferOutput<T>> =>
24
- throwIfFailed(await getValidatedRouterParams(event, v.safeParser(s)))
25
-
26
- export const validateQuery = async <T extends v.GenericSchema>(
27
- event: H3Event,
28
- s: T,
29
- ): Promise<v.InferOutput<T>> => throwIfFailed(await getValidatedQuery(event, v.safeParser(s)))
package/src/client.ts DELETED
@@ -1,42 +0,0 @@
1
- import { ofetch } from 'ofetch'
2
-
3
- type Presets = 'avatio'
4
- interface PresetVersions {
5
- avatio: 'v1'
6
- }
7
-
8
- export interface RequestOgImageOptions<TProps = unknown> {
9
- /**
10
- * @default https://og.liria.me
11
- */
12
- endpoint?: string
13
- /**
14
- * @default process.env.OG_IMAGE_SECRET
15
- */
16
- secret?: string
17
- preset: Presets
18
- version: PresetVersions[Presets]
19
- props: TProps
20
- }
21
-
22
- export interface IssueImageResponse {
23
- url: string
24
- }
25
-
26
- export const requestOgImage = async ({
27
- preset,
28
- version,
29
- props,
30
- endpoint = 'https://og.liria.me',
31
- secret = process.env.OG_IMAGE_SECRET || '',
32
- }: RequestOgImageOptions) =>
33
- ofetch<IssueImageResponse>(
34
- `/images/${encodeURIComponent(preset)}/${encodeURIComponent(version)}`,
35
- {
36
- baseURL: endpoint,
37
- method: 'POST',
38
- headers: { 'content-type': 'application/json' },
39
- body: { secret, props },
40
- retry: 3,
41
- },
42
- )
package/taze.config.ts DELETED
@@ -1,15 +0,0 @@
1
- import { defineConfig } from 'taze'
2
-
3
- export default defineConfig({
4
- force: true,
5
- write: true,
6
- install: false,
7
- interactive: true,
8
- recursive: false,
9
- includeLocked: true,
10
- ignorePaths: ['**/node_modules/**'],
11
- ignoreOtherWorkspaces: true,
12
- depFields: {
13
- overrides: false,
14
- },
15
- })
package/tsconfig.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "extends": "./node_modules/.nitro/types/tsconfig.json"
3
- }
package/tsdown.config.ts DELETED
@@ -1,8 +0,0 @@
1
- import { defineConfig } from 'tsdown'
2
-
3
- export default defineConfig({
4
- entry: ['src/client.ts'],
5
- format: 'esm',
6
- dts: true,
7
- clean: true,
8
- })
package/types/assets.d.ts DELETED
@@ -1,25 +0,0 @@
1
- declare module '*.woff2' {
2
- const value: ArrayBuffer
3
- export default value
4
- }
5
-
6
- declare module '#fonts/*' {
7
- interface FontAssetDefinition {
8
- key: string
9
- name: string
10
- path: string
11
- ranges: readonly (readonly [number, number])[]
12
- }
13
-
14
- export const fontFamily: string
15
- export const fonts: readonly FontAssetDefinition[]
16
- }
17
-
18
- declare module '#images' {
19
- interface OgImageAsset {
20
- src: string
21
- svg: string
22
- }
23
-
24
- export const images: Record<string, OgImageAsset>
25
- }
@@ -1,3 +0,0 @@
1
- declare module '#presets' {
2
- export const allPresets: import('../server/utils/definePreset').OgImagePreset[]
3
- }