Skip to content

Commit f7bb72b

Browse files
authored
fix: reworked sharp compatibility checks for JPEG rendering (#370)
1 parent efcc4eb commit f7bb72b

File tree

8 files changed

+78
-39
lines changed

8 files changed

+78
-39
lines changed

client/app.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ async function ejectComponent(component: string) {
531531
<Pane size="60" class="flex h-full justify-center items-center relative n-panel-grids-center pr-4" style="padding-top: 30px;">
532532
<div class="flex justify-between items-center text-sm w-full absolute pr-[30px] top-0 left-0">
533533
<div class="flex items-center text-lg space-x-1 w-[100px]">
534-
<NButton v-if="!!globalDebug?.compatibility?.sharp || renderer === 'chromium'" icon="carbon:jpg" :class="imageFormat === 'jpeg' || imageFormat === 'jpg' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" @click="patchOptions({ extension: 'jpg' })" />
534+
<NButton v-if="!!globalDebug?.compatibility?.sharp || renderer === 'chromium' || options?.extension === 'jpeg'" icon="carbon:jpg" :class="imageFormat === 'jpeg' || imageFormat === 'jpg' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" @click="patchOptions({ extension: 'jpg' })" />
535535
<NButton icon="carbon:png" :class="imageFormat === 'png' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" @click="patchOptions({ extension: 'png' })" />
536536
<NButton v-if="renderer !== 'chromium'" icon="carbon:svg" :class="imageFormat === 'svg' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" @click="patchOptions({ extension: 'svg' })" />
537537
<NButton v-if="!isPageScreenshot" icon="carbon:html" :class="imageFormat === 'html' ? 'border border-zinc-300 dark:border-zinc-700 opacity-100' : ''" @click="patchOptions({ extension: 'html' })" />

playground/components/OgImage/WithImage.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ const containerStyles = {
2424
<div :style="{ position: 'absolute', top: '170px', left: '250px', fontSize: '25px', display: 'flex', alignItems: 'center' }">
2525
<img src="/harlan-wilton.jpeg" width="100" height="100" :style="{ borderRadius: '12px', marginRight: '8px' }">
2626
</div>
27-
<img src="harlan-wilton.jpeg" width="100" height="100" :style="{ borderRadius: '12px', marginRight: '8px' }">
28-
<img src="assets/static/images/picture.jpg" width="100" height="100" :style="{ borderRadius: '12px', marginRight: '8px' }">
27+
<img src="/harlan-wilton.jpeg" width="100" height="100" :style="{ borderRadius: '12px', marginRight: '8px' }">
28+
<img src="/assets/static/images/picture.jpg" width="100" height="100" :style="{ borderRadius: '12px', marginRight: '8px' }">
2929
<img src="https://avatars.githubusercontent.com/u/5326365?v=4" width="200" height="200" alt="absolute test">
3030
</div>
3131
</template>

playground/nuxt.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export default defineNuxtConfig({
5555
plugins: ['plugins/hooks.ts'],
5656
prerender: {
5757
routes: [
58+
'/satori/jpeg',
5859
'/chromium/component',
5960
'/chromium/delayed',
6061
'/chromium/screenshot',

playground/pages/satori/jpeg.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts" setup>
2+
import { defineOgImage } from '#imports'
3+
4+
defineOgImage({
5+
extension: 'jpeg',
6+
sharp: {
7+
quality: 10,
8+
progressive: true,
9+
},
10+
props: {
11+
title: 'This is a long title 😮 that will exceed 60 chars',
12+
description: 'And perhpas a long description? 😮 Yup, here it is. A max of 100 characters, will see. Guess we can go really long, maybe even 3 lines, will see what happens now.',
13+
},
14+
})
15+
</script>
16+
17+
<template>
18+
<div>inline</div>
19+
</template>

src/module.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -223,29 +223,37 @@ export default defineNuxtModule<ModuleOptions>({
223223
publicDirAbs = publicDirAbs in nuxt.options.alias ? nuxt.options.alias[publicDirAbs] : resolve(nuxt.options.rootDir, publicDirAbs)
224224
}
225225
if (isProviderEnabledForEnv('satori', nuxt, config)) {
226-
let isUsingSharp = false
226+
let attemptSharpUsage = false
227227
if (isProviderEnabledForEnv('sharp', nuxt, config)) {
228228
// avoid any sharp logic if user explicitly opts-out
229229
const userConfiguredExtension = config.defaults.extension
230230
const hasConfiguredJpegs = userConfiguredExtension && ['jpeg', 'jpg'].includes(userConfiguredExtension)
231-
if (!!config.sharpOptions || (hasConfiguredJpegs && config.defaults.renderer !== 'chromium')) {
232-
isUsingSharp = true
233-
const hasSharpDependency = await hasResolvableDependency('sharp')
234-
if (hasSharpDependency && !targetCompatibility.sharp) {
231+
const hasSharpDependency = await hasResolvableDependency('sharp')
232+
if (hasSharpDependency) {
233+
if (!targetCompatibility.sharp) {
235234
logger.warn(`Rendering JPEGs requires sharp which does not work with ${preset}. Images will be rendered as PNG at runtime.`)
236235
config.compatibility = defu(config.compatibility, <CompatibilityFlagEnvOverrides>{
237236
runtime: { sharp: false },
238237
})
239238
}
240-
else if (!hasSharpDependency) {
241-
// sharp is supported but not installed
242-
logger.warn('You have enabled `JPEG` images. These require the `sharp` dependency which is missing, installing it for you.')
243-
await ensureDependencies(['sharp'])
244-
logger.warn('Support for `sharp` is limited so check the compatibility guide.')
239+
else {
240+
// if we can import it then we'll use it
241+
await import('sharp')
242+
.catch(() => {})
243+
.then(() => {
244+
attemptSharpUsage = true
245+
})
245246
}
246247
}
248+
else if (hasConfiguredJpegs && config.defaults.renderer !== 'chromium') {
249+
// sharp is supported but not installed
250+
logger.warn('You have enabled `JPEG` images. These require the `sharp` dependency which is missing, installing it for you.')
251+
await ensureDependencies(['sharp'])
252+
logger.warn('Support for `sharp` is limited so check the compatibility guide.')
253+
attemptSharpUsage = true
254+
}
247255
}
248-
if (!isUsingSharp) {
256+
if (!attemptSharpUsage) {
249257
// disable sharp
250258
config.compatibility = defu(config.compatibility, <CompatibilityFlagEnvOverrides>{
251259
runtime: { sharp: false },

src/runtime/server/og-image/satori/renderer.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SatoriOptions } from 'satori'
2+
import type { JpegOptions } from 'sharp'
23
import type { OgImageRenderEventContext, Renderer, ResolvedFontConfig } from '../../../types'
34
import { fontCache } from '#og-image-cache'
45
import { theme } from '#og-image-virtual/unocss-config.mjs'
@@ -8,6 +9,8 @@ import { normaliseFontInput, useOgImageRuntimeConfig } from '../../../shared'
89
import { loadFont } from './font'
910
import { useResvg, useSatori, useSharp } from './instances'
1011
import { createVNodes } from './vnodes'
12+
// @ts-expect-error untyped
13+
import compatibility from '#og-image/compatibility'
1114

1215
const fontPromises: Record<string, Promise<ResolvedFontConfig>> = {}
1316

@@ -78,11 +81,35 @@ async function createPng(event: OgImageRenderEventContext) {
7881

7982
async function createJpeg(event: OgImageRenderEventContext) {
8083
const { sharpOptions } = useOgImageRuntimeConfig()
81-
const png = await createPng(event)
82-
const sharp = await useSharp()
83-
return sharp(png, defu(event.options.sharp, sharpOptions)).jpeg().toBuffer()
84-
return sharp(svgBuffer, defu(event.options.sharp, sharpOptions))
85-
.jpeg(sharp as JpegOptions)
84+
if (compatibility.sharp === false) {
85+
if (import.meta.dev) {
86+
throw new Error('Sharp dependency is not accessible. Please check you have it installed and are using a compatible runtime.')
87+
}
88+
else {
89+
// TODO this should be an error in next major
90+
console.error('Sharp dependency is not accessible. Please check you have it installed and are using a compatible runtime. Falling back to png.')
91+
}
92+
return createPng(event)
93+
}
94+
const svg = await createSvg(event)
95+
if (!svg) {
96+
throw new Error('Failed to create SVG for JPEG rendering.')
97+
}
98+
const svgBuffer = Buffer.from(svg)
99+
const sharp = await useSharp().catch(() => {
100+
if (import.meta.dev) {
101+
throw new Error('Sharp dependency could not be loaded. Please check you have it installed and are using a compatible runtime.')
102+
}
103+
return null
104+
})
105+
if (!sharp) {
106+
// TODO this should be an error in next major
107+
console.error('Sharp dependency is not accessible. Please check you have it installed and are using a compatible runtime. Falling back to png.')
108+
return createPng(event)
109+
}
110+
const options = defu(event.options.sharp, sharpOptions)
111+
return sharp(svgBuffer, options)
112+
.jpeg(options as JpegOptions)
86113
.toBuffer()
87114
}
88115

src/runtime/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { NitroOptions } from 'nitropack'
77
import type { NitroApp } from 'nitropack/types'
88
import type { SatoriOptions } from 'satori'
99
import type { html } from 'satori-html'
10-
import type { SharpOptions } from 'sharp'
10+
import type { JpegOptions, SharpOptions } from 'sharp'
1111
import type { Ref } from 'vue'
1212

1313
export interface OgImageRenderEventContext {
@@ -130,7 +130,7 @@ export interface OgImageOptions<T extends keyof OgImageComponents = 'NuxtSeo'> {
130130
resvg?: ResvgRenderOptions
131131
satori?: SatoriOptions
132132
screenshot?: Partial<ScreenshotOptions>
133-
sharp?: SharpOptions
133+
sharp?: SharpOptions & JpegOptions
134134
fonts?: InputFontConfig[]
135135
// cache
136136
cacheMaxAgeSeconds?: number

test/integration/dev-debugging.test.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -256,33 +256,17 @@ describe('dev', () => {
256256
const debug = await $fetch('/__og-image__/debug.json')
257257
delete debug.runtimeConfig.baseCacheKey
258258
delete debug.runtimeConfig.version
259+
delete debug.runtimeConfig.compatibility
259260
delete debug.componentNames
260261
delete debug.baseCacheKey
261-
delete debug.compatibility.chromium // github ci will have playwright
262+
delete debug.compatibility // github ci will have playwright
262263
expect(debug).toMatchInlineSnapshot(`
263264
{
264-
"compatibility": {
265-
"css-inline": "node",
266-
"resvg": "node",
267-
"satori": "node",
268-
"sharp": "node",
269-
},
270265
"runtimeConfig": {
271266
"app": {
272267
"baseURL": "/",
273268
},
274269
"colorPreference": "light",
275-
"compatibility": {
276-
"dev": {
277-
"chromium": "chrome-launcher",
278-
},
279-
"prerender": {
280-
"chromium": "",
281-
},
282-
"runtime": {
283-
"chromium": "",
284-
},
285-
},
286270
"componentDirs": [
287271
"OgImage",
288272
"og-image",

0 commit comments

Comments
 (0)