thx / resvg-js

A high-performance SVG renderer and toolkit, powered by Rust based resvg and napi-rs.
https://resvg-js.vercel.app/
Mozilla Public License 2.0
1.58k stars 59 forks source link

resvg error(by Next.js 14) #315

Open front-zero opened 8 months ago

front-zero commented 8 months ago

hi!

I wrote logic to convert svg to png using resvg in next.js, but the following error occurred. Could I possibly get some help?

import {NextResponse} from "next/server";
import satori from 'satori';
import sharp from "sharp";
import fs from 'fs';
import path from 'path';
import {convertSvgToPngByResvg, convertSvgToPngBySharp} from "@/utils/svgToPngUtil";

/**
 * Modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js.
 */

/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */

const U200D = String.fromCharCode(8205)
const UFE0Fg = /\uFE0F/g

function getIconCode(char: string) {
    return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, '') : char)
}

function toCodePoint(unicodeSurrogates: string) {
    const r = []
    let c = 0,
        p = 0,
        i = 0

    while (i < unicodeSurrogates.length) {
        c = unicodeSurrogates.charCodeAt(i++)
        if (p) {
            r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16))
            p = 0
        } else if (55296 <= c && c <= 56319) {
            p = c
        } else {
            r.push(c.toString(16))
        }
    }
    return r.join('-')
}

export const apis = {
    twemoji: (code: string) =>
        'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/' +
        code.toLowerCase() +
        '.svg',
    openmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/',
    blobmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/',
    noto: 'https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/',
    fluent: (code: string) =>
        'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +
        code.toLowerCase() +
        '_color.svg',
    fluentFlat: (code: string) =>
        'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +
        code.toLowerCase() +
        '_flat.svg',
}

function loadEmoji(type: keyof typeof apis, code: string) {
    const key = type + ':' + code

    if (!type || !apis[type]) {
        type = 'twemoji'
    }

    const api = apis[type]
    if (typeof api === 'function') {
        return fetch(api(code)).then((r) => r.text());
    }
    return fetch(`${api}${code.toUpperCase()}.svg`).then((r) =>
        r.text()
    )
}

export async function GET(request: Request) {

    const notoSansFontBuffer = fs.readFileSync(path.join(process.cwd(), 'public', 'fonts', 'NotoSansKR-SemiBold.ttf'));

    const svg = await satori((<div
        style={{
            display: 'flex',
            height: '100%',
            width: '100%',
            alignItems: 'center',
            justifyContent: 'center',
            flexDirection: 'column',
            backgroundImage: 'linear-gradient(to bottom, #dbf4ff, #fff1f1)',
            fontSize: 60,
            letterSpacing: -2,
            fontWeight: 700,
            textAlign: 'center',
        }}
    >
        <svg
            height={80}
            viewBox="0 0 75 65"
            fill="black"
            style={{margin: '0 75px'}}
        >
            <path d="M37.59.25l36.95 64H.64l36.95-64z"></path>
        </svg>
        <div
            style={{
                backgroundImage: 'linear-gradient(90deg, rgb(0, 124, 240), rgb(0, 223, 216))',
                backgroundClip: 'text',
                '-webkit-background-clip': 'text',
                color: 'transparent',
            }}
        >
            Develop
        </div>
        <div
            style={{
                backgroundImage: 'linear-gradient(90deg, rgb(121, 40, 202), rgb(255, 0, 128))',
                backgroundClip: 'text',
                '-webkit-background-clip': 'text',
                color: 'transparent',
            }}
        >
            Preview
        </div>
        <div
            style={{
                backgroundImage: 'linear-gradient(90deg, rgb(255, 77, 77), rgb(249, 203, 40))',
                backgroundClip: 'text',
                '-webkit-background-clip': 'text',
                color: 'transparent',
            }}
        >
            Ship ❤️
        </div>
        <img src="https://picsum.photos/150" width={150} height={150}/>
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36">
            <path fill="#DD2E44"
                  d="M35.885 11.833c0-5.45-4.418-9.868-9.867-9.868-3.308 0-6.227 1.633-8.018 4.129-1.791-2.496-4.71-4.129-8.017-4.129-5.45 0-9.868 4.417-9.868 9.868 0 .772.098 1.52.266 2.241C1.751 22.587 11.216 31.568 18 34.034c6.783-2.466 16.249-11.447 17.617-19.959.17-.721.268-1.469.268-2.242z"/>
        </svg>
    </div>), {
        width: 1200,
        height: 627,
        fonts: [
            {
                style: "normal",
                name: "Noto Sans KR",
                data: notoSansFontBuffer,
                weight: 600,
            },
        ],
        loadAdditionalAsset: async (code: string, segment: string) => {
            // console.log(code, segment, Buffer.from(segment).toString('base64'));
            // console.log(getIconCode(segment));
            // console.log(await loadEmoji('twemoji', getIconCode(segment)));
            // return loadDynamicAsset('twemoji', code, segment);
            if (code === 'emoji') {
                return `data:image/svg+xml;base64,` +
                    btoa(await loadEmoji('twemoji', getIconCode(segment)));
            }

            return code;
        }
    },);

    const pngBuffer = convertSvgToPngByResvg(svg);

    // const pngBuffer2 = convertSvgToPng(svg);

    // return new NextResponse(pngBuffer, {
    //     status: 200,
    //     headers: {
    //         'Content-Type': 'image/png',
    //         'Content-Length': String(pngBuffer.length),
    //     }
    // });

    return new NextResponse(svg, {
        status: 200,
        headers: {
            'Content-Type': 'image/svg+xml',
        }
    });
}
import sharp from "sharp";
import {Resvg} from "@resvg/resvg-js";

export function convertSvgToPngByResvg(targetSvg: Buffer | string) {
    const resvg = new Resvg(targetSvg, {});
    const pngData = resvg.render();
    return pngData.asPng();
}

export async function convertSvgToPngBySharp(targetSvg: string) {
    return sharp(Buffer.from(targetSvg)).png().toBuffer();
}

⨯ ./node_modules/@resvg/resvg-js-win32-x64-msvc/resvgjs.win32-x64-msvc.node Module parse failed: Unexpected character '�' (1:2) 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)

Import trace for requested module: ./node_modules/@resvg/resvg-js-win32-x64-msvc/resvgjs.win32-x64-msvc.node ./node_modules/@resvg/resvg-js/js-binding.js ./node_modules/@resvg/resvg-js/index.js ./src/utils/svgToPngUtil.ts ./src/app/api/satori/route.tsx ○ Compiling /not-found ... ⨯ ./node_modules/@resvg/resvg-js-win32-x64-msvc/resvgjs.win32-x64-msvc.node Module parse failed: Unexpected character '�' (1:2) 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)

image image

mathiasprisfeldt commented 8 months ago

You have to use https://www.npmjs.com/package/nextjs-node-loader/v/1.1.1

udatny commented 2 months ago

@mathiasprisfeldt do you have an example how you use it ?

mathiasprisfeldt commented 2 months ago

@udatny try this

npm install nextjs-node-loader --save-dev

// next.config.mjs
export default () => {

  const nextConfig = {
    webpack: (config) => {
      config.module.rules.push(
        {
          test: /\.node$/,
          use: [
            {
              loader: 'nextjs-node-loader',
            },
          ],
        },
      );

      return config;
    }
  };

  return nextConfig;
};
joshxfi commented 1 month ago

you need to add @resvg/resvg-js in serverComponentsExternalPackages

const nextConfig = {
  experimental: {
    serverComponentsExternalPackages: ["@resvg/resvg-js"],
  },
};

EDIT: As of Next 15, see https://nextjs.org/docs/app/api-reference/next-config-js/serverExternalPackages

const nextConfig = {
  serverExternalPackages: ["@resvg/resvg-js"],
};
udatny commented 1 month ago

@joshxfi Your hint helped me solve a tricky issue. https://github.com/eisberg-labs/nextjs-node-loader/issues/45