satori icon indicating copy to clipboard operation
satori copied to clipboard

Add support for jpeg output

Open transitive-bullshit opened this issue 3 years ago • 13 comments

Feature Request

Description

I'd like to be able to serve my OG images in JPEG format instead of PNG format. This can provide a huge savings in terms of network bandwidth for certain types of OG images (which can be exacerbated when viewing content on mobile aggregators).

Additional Context

Not sure if this belongs in satori or @vercel/og, but since @vercel/og seems to be closed source for the time being, I'm opening an issue here.

Here are some related resources:

There are quite a few other examples I could find from a cursory search that use either png or jpeg images, so it seems to be 50/50 depending on the nature of the open graph images. PNG will work well for more sparse / vector graphics, whereas JPEG will work better for more photo-like images, and it should be up to the user to choose which format works better for their use case.

As an example, this OG image created with Satori generates a 906 KB PNG. When converted, it is only a 174 KB JPEG.

image

transitive-bullshit avatar Oct 18 '22 17:10 transitive-bullshit

@transitive-bullshit you can use @napi-rs/canvas package to convert SVG generated by satori to jpeg or any other format, like that:

import satori from "satori";
import { Canvas, Image } from "@napi-rs/canvas";
import { writeFile } from "node:fs/promises";

const svg = await satori(<div />, { width, height });

const image = new Image();
image.src = Buffer.from(svg, "utf8");

const canvas = new Canvas(width, height);
const ctx = canvas.getContext("2d");

ctx.drawImage(image, 0, 0);

const jpegData = await canvas.encode("jpeg");
await writeFile("./out.jpeg", jpegData);

jeetiss avatar Oct 20 '22 17:10 jeetiss

Perhaps @napi-rs/image might work and it would be even smaller. Although I'm not sure if it supports SVG though 🤔

styfle avatar Oct 20 '22 18:10 styfle

Thanks, guys; appreciate the quick replies 🙏

I don't think I was clear enough in my original description, however. I meant this issue to be more about supporting JPEG output from Vercel OG images, whereas @napi-rs/canvas looks to be Node.js-only.

As I mentioned above, this issue probably belongs in @vercel/og, but since @vercel/og seems to be closed source for the time being, I'm opening an issue here instead.

ImageResponse from @vercel/og seems to only support PNG output right now, whereas I think it should support JPEG output as well.

Something like:

import { ImageResponse } from '@vercel/og'

export default async function OGImage(req: NextRequest) {
  return new ImageResponse(
    (
      <div />
    ),
    {
      width: 1200,
      height: 630,
      type: 'jpeg' // | 'png' | 'svg'
    }
  )
}

export const config = {
  runtime: 'experimental-edge'
}

@napi-rs/image looks promising.

transitive-bullshit avatar Oct 20 '22 23:10 transitive-bullshit

We are relying on the Resvg dependency which only supports PNG at the moment. If adding another converter (PNG → JPEG) it will be too expensive in both size and speed.

shuding avatar Oct 24 '22 21:10 shuding

Indeed, we we need to convert SVG → JPEG to make it worth while.

styfle avatar Oct 25 '22 05:10 styfle

@styfle Yes, converting SVG directly to other image formats is a good option.

yisibl avatar Nov 22 '22 03:11 yisibl

Perhaps @napi-rs/image might work and it would be even smaller. Although I'm not sure if it supports SVG though 🤔

Now @napi-rs/image support svg as input! Basic example:

import satori from "satori";
import { Transformer } from "@napi-rs/image"

export async function createOG() {
  const svg = await satori(<div>test</div>, { width: 600, height: 400 })

  const trasformer = Transformer.fromSvg(svg); // satori result
  const jpeg = await trasformer.jpeg();

  return jpeg; // buffer
}

ceopaludetto avatar Feb 27 '23 18:02 ceopaludetto

Now @napi-rs/image support svg as input! Basic example

Great, this is driven by the powerful resvg.

yisibl avatar Feb 28 '23 03:02 yisibl

Looking forward to a lightweight @napi-rs/image build with only svg → png and svg → jpeg features 👍

shuding avatar Mar 15 '23 16:03 shuding

getting:

- wait compiling /blog/[slug]/opengraph-image/[[...__metadata_id__]]/route (client and server)...
- error ./node_modules/.pnpm/@[email protected]/node_modules/@napi-rs/image-darwin-arm64/image.darwin-arm64.node
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
(Source code omitted for this binary file)

anyone know what is going on here?

lucgagan avatar May 27 '23 00:05 lucgagan

Had to add:

export default {
  webpack: (config) => {
    config.externalsType = 'commonjs';
    config.externals = config.externals || [];
    config.externals.push({
      '@napi-rs/image': "@napi-rs/image",
    });

    return config;
  },
};

to next.config.js

lucgagan avatar May 27 '23 01:05 lucgagan

I was about to use one of the solutions above when I remembered that https://wsrv.nl allows converting images on the fly. For example:

Create your og image as normal

const ogURL = `${baseUrl}/api/og?name=${encodeURIComponent(
      name
    )}&quote=${encodeURIComponent(quote)}&image=${encodeURIComponent(image)}`


Convert it using wsrv

 const convertedURL = `https://wsrv.nl/?url=${encodeURIComponent(
      ogURL
    )}&output=jpg`

nativedone avatar Oct 19 '23 22:10 nativedone

@napi-rs/image won't work on Edge Functions fwi.

J

jdgamble555 avatar Jan 13 '24 17:01 jdgamble555