From 198bd524eb6c20e9e4ab10dff0468994b5f8d858 Mon Sep 17 00:00:00 2001 From: MoYingJi Date: Sat, 24 Oct 2026 18:41:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=BA=E6=AD=8C=E8=AF=8D=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=9B=BE=E7=89=87=E7=9A=84=20Canvas=20=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=20lang=20=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/lyric/poster.spec.ts | 22 +++++++++--- src/utils/lyric/poster.ts | 61 ++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/src/utils/lyric/poster.spec.ts b/src/utils/lyric/poster.spec.ts index 3a35a0bc9..d63f211f4 100644 --- a/src/utils/lyric/poster.spec.ts +++ b/src/utils/lyric/poster.spec.ts @@ -9,19 +9,20 @@ vi.mock("@/utils/color", () => ({ hexToRgb: () => "20, 20, 28" })); * @returns 字体加载桩、测量记录和绘制记录 */ const setupCanvas = () => { - const measured: Array<{ text: string; font: string }> = []; - const drawn: Array<{ text: string; font: string }> = []; + const measured: Array<{ text: string; font: string; lang: string }> = []; + const drawn: Array<{ text: string; font: string; lang: string }> = []; const load = vi.fn().mockResolvedValue([]); Object.defineProperty(document, "fonts", { configurable: true, value: { load } }); const context = { font: "", + lang: "inherit", scale: vi.fn(), fillRect: vi.fn(), measureText: (text: string) => { - measured.push({ text, font: context.font }); + measured.push({ text, font: context.font, lang: context.lang }); return { width: text.length * (context.font.includes('"Wide"') ? 100 : 20) }; }, - fillText: (text: string) => drawn.push({ text, font: context.font }), + fillText: (text: string) => drawn.push({ text, font: context.font, lang: context.lang }), }; vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue( context as unknown as CanvasRenderingContext2D, @@ -67,6 +68,19 @@ describe("歌词海报字体", () => { expect(drawn.find((entry) => entry.text === "歌名")?.font).toContain('"Global"'); }); + it("只为原文歌词设置语言,翻译和音译使用默认语言", async () => { + const { measured, drawn } = setupCanvas(); + await createLyricPoster({ + track, + lines: [{ text: "骨と直", language: "ja", translation: "骨与直", romaji: "hone to choku" }], + }); + + expect(drawn.find((entry) => entry.text === "骨と直")?.lang).toBe("ja"); + expect(measured.some((entry) => entry.text === "骨と直" && entry.lang === "ja")).toBe(true); + expect(drawn.find((entry) => entry.text === "骨与直")?.lang).toBe("inherit"); + expect(drawn.find((entry) => entry.text === "hone to choku")?.lang).toBe("inherit"); + }); + it("未设置歌词字体时使用全局字体", async () => { const { drawn } = setupCanvas(); await createLyricPoster({ diff --git a/src/utils/lyric/poster.ts b/src/utils/lyric/poster.ts index e3a03dc42..41deded3e 100644 --- a/src/utils/lyric/poster.ts +++ b/src/utils/lyric/poster.ts @@ -156,6 +156,29 @@ export const createLyricPoster = async (options: LyricPosterOptions): Promise { + if (language) return language; + const detected: LyricLine[] = [ + { + words: [{ word: text, startTime: 0, endTime: 0 }], + translatedLyric: "", + romanLyric: "", + startTime: 0, + endTime: 0, + isBG: false, + isDuet: false, + }, + ]; + applyLyricLanguages(detected); + return detected[0].language; + }; + /** * 按文本语言解析字体,保证测量和绘制使用相同字体 * @param text - 原文、翻译或音译 @@ -164,28 +187,14 @@ export const createLyricPoster = async (options: LyricPosterOptions): Promise { - if (!language) { - const detected: LyricLine[] = [ - { - words: [{ word: text, startTime: 0, endTime: 0 }], - translatedLyric: "", - romanLyric: "", - startTime: 0, - endTime: 0, - isBG: false, - isDuet: false, - }, - ]; - applyLyricLanguages(detected); - language = detected[0].language; - } - const family = language + const resolvedLanguage = languageForText(text, language); + const family = resolvedLanguage ? { "zh-CN": fonts.chinese, ja: fonts.japanese, ko: fonts.korean, "und-Latn": fonts.latin, - }[language] + }[resolvedLanguage] : undefined; return `${style} ${family ? `${family}, ` : ""}${lyricFont}`; }; @@ -197,6 +206,8 @@ export const createLyricPoster = async (options: LyricPosterOptions): Promise { + const mainLanguage = languageForText(line.text, line.language); const mainFont = fontForText(line.text, "bold 34px", line.language); const translationFont = fontForText(line.translation ?? "", "24px"); const romajiFont = fontForText(line.romaji ?? "", "italic 22px"); + textContext.lang = mainLanguage ?? "inherit"; ctx.font = mainFont; const main = wrapText(ctx, line.text || " ", CONTENT_WIDTH); + textContext.lang = "inherit"; ctx.font = translationFont; const translation = line.translation ? wrapText(ctx, line.translation, CONTENT_WIDTH) : []; ctx.font = romajiFont; const romaji = line.romaji ? wrapText(ctx, line.romaji, CONTENT_WIDTH) : []; - return { main, translation, romaji, mainFont, translationFont, romajiFont, duet: !!line.duet }; + return { + main, + translation, + romaji, + mainFont, + translationFont, + romajiFont, + mainLanguage, + duet: !!line.duet, + }; }); let lyricsHeight = 0; @@ -287,11 +310,13 @@ export const createLyricPoster = async (options: LyricPosterOptions): Promise