Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CJK words can not correctly show in the browser #3967

Open
ISNing opened this issue Nov 24, 2023 · 8 comments
Open

CJK words can not correctly show in the browser #3967

ISNing opened this issue Nov 24, 2023 · 8 comments
Assignees
Labels
bug Something isn't working text web

Comments

@ISNing
Copy link

ISNing commented Nov 24, 2023

Describe the bug
Currently CJK words can not correctly show in the browser
I don't know how does compose get latin font. it might be getting from local system or just package it in the web package, but according to sources i read, it seems compose obtain font from skia, or in other word, render in skia.
If the former one, we just need to add correct font fallback logic and configurations.
To load a a custom full weight font family such as noto sans in just simplified chinese can take up to more than 90 mega bytes, even in local test, it takes me several seconds to load the page with full font familly loaded, that's an unbearable cost.

And this also restricted user input. All the texts are rendered in skiko, so that all the text will have to ensure there's available font to show, If i designed a textfield that allows user to input any characters in unicode, then cjk words can't show up, emojis can't show up and any character not included in packaged font will not show up.(Then we can't simply do compress on font by only include chars we used in webpage like font-spider)

I think we must pay attention to this problem.

What I tried to avoid this problem is to load font by font name such as "Microsoft Yahei", "Noto Sans CJK", "Noto Sans SC" or all of them with space removed from SkTypeface and convert SkTypeface object into Typeface and then to FontFamily, but that doesn't work...
Or is there any other way available to load local fonts?

Thank you all for your great effort work on this great project.
Affected platforms
Select one of the platforms below:

  • Web (K/Wasm) - Canvas based API
  • Web (K/JS) - Canvas based API

Versions

  • Kotlin version*: 1.9.20
  • Compose Multiplatform version*: 1.5.10

To Reproduce

  1. Create a Text with cjk word(such as "你好") in canvas
  2. Build it and open it in browser
  3. And you can see that the word shows as two square

Expected behavior
Text should shows up correctly

Screenshots
334086eb55d2b8f9a0cc50e16e009046
The chinese words doesn' show correctly

@ISNing ISNing added bug Something isn't working submitted labels Nov 24, 2023
@eymar eymar self-assigned this Nov 24, 2023
@eymar eymar added the text label Nov 24, 2023
@yazinnnn
Copy link

yazinnnn commented Dec 8, 2023

i try to load resource by using LaunchedEffect, it works but the experience is a bit poor.

@OptIn(ExperimentalResourceApi::class)
suspend fun loadCjkFont(): FontFamily {
    val regular = resource("font/NotoSansCJKsc-Regular.ttf").readBytes()
    val bold = resource("font/NotoSansCJKsc-Bold.ttf").readBytes()
    val italic = resource("font/NotoSansCJKsc-Italic.ttf").readBytes()

    return FontFamily(
        Font(identity = "CJKRegular", data = regular, weight = FontWeight.Normal),
        Font(identity = "CJKBold", data = bold, weight = FontWeight.Bold),
        Font(identity = "CJKItalic", data = italic, style = FontStyle.Italic),
    )
}

@Composable
fun App() {
    var typography by remember { mutableStateOf<Typography?>(null) }
    LaunchedEffect(Unit) {
        val font = loadCjkFont()
        typography = Typography(defaultFontFamily = font)
    }

    MaterialTheme(typography = typography ?: MaterialTheme.typography) {...
    }
}

record

@ISNing
Copy link
Author

ISNing commented Dec 11, 2023

i try to load resource by using LaunchedEffect, it works but the experience is a bit poor.

@OptIn(ExperimentalResourceApi::class)
suspend fun loadCjkFont(): FontFamily {
    val regular = resource("font/NotoSansCJKsc-Regular.ttf").readBytes()
    val bold = resource("font/NotoSansCJKsc-Bold.ttf").readBytes()
    val italic = resource("font/NotoSansCJKsc-Italic.ttf").readBytes()

    return FontFamily(
        Font(identity = "CJKRegular", data = regular, weight = FontWeight.Normal),
        Font(identity = "CJKBold", data = bold, weight = FontWeight.Bold),
        Font(identity = "CJKItalic", data = italic, style = FontStyle.Italic),
    )
}

@Composable
fun App() {
    var typography by remember { mutableStateOf<Typography?>(null) }
    LaunchedEffect(Unit) {
        val font = loadCjkFont()
        typography = Typography(defaultFontFamily = font)
    }

    MaterialTheme(typography = typography ?: MaterialTheme.typography) {...
    }
}

record record

Thank you for your help, here's my implementation with lower invasivieness: ISNing/XWareManage@7c3d63c

But it still can't help with the huge transportation for fetching font files(even if only to load three weights)

Looking forward to official solution.

@KevinnZou
Copy link

@eymar Is there a plan to address this issue?

@eymar
Copy link
Collaborator

eymar commented Feb 22, 2024

Yes, we plan to address it after we address the issues which have no workarounds.

@zhangzih4n
Copy link

zhangzih4n commented May 30, 2024

i try to load resource by using LaunchedEffect, it works but the experience is a bit poor.

你好,请问能给一些详细的代码吗?试了一下好像不起作用

原来直接用就可以了,不需要这么麻烦 Text("首页", fontFamily = MiSansFont()

但是上面的代码在 JVM 是正常的,在 Web 却显示方块,奇怪

@ISNing
Copy link
Author

ISNing commented Jun 3, 2024

i try to load resource by using LaunchedEffect, it works but the experience is a bit poor.

@OptIn(ExperimentalResourceApi::class)
suspend fun loadCjkFont(): FontFamily {
    val regular = resource("font/NotoSansCJKsc-Regular.ttf").readBytes()
    val bold = resource("font/NotoSansCJKsc-Bold.ttf").readBytes()
    val italic = resource("font/NotoSansCJKsc-Italic.ttf").readBytes()

    return FontFamily(
        Font(identity = "CJKRegular", data = regular, weight = FontWeight.Normal),
        Font(identity = "CJKBold", data = bold, weight = FontWeight.Bold),
        Font(identity = "CJKItalic", data = italic, style = FontStyle.Italic),
    )
}

@Composable
fun App() {
    var typography by remember { mutableStateOf<Typography?>(null) }
    LaunchedEffect(Unit) {
        val font = loadCjkFont()
        typography = Typography(defaultFontFamily = font)
    }

    MaterialTheme(typography = typography ?: MaterialTheme.typography) {...
    }
}
你好,请问能给一些详细的代码吗?试了一下好像不起作用 QAQ

Hi, could you please give me some detailed code? I tried it but it doesn't work.

@Composable
@Preview
fun MainApp() {
    var fontFamily by remember { mutableStateOf<FontFamily?>(null) }
    val customFont = MiSansVfFont()
    LaunchedEffect(Unit) {
        fontFamilyby = customFont
    }
    MaterialTheme {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            Row(horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.Top) {
                Text("首页", fontFamily = fontFamily ?: MaterialTheme.typography.displayMedium.fontFamily)
                Text("EnglishText", fontFamily = fontFamily ?: MaterialTheme.typography.displayMedium.fontFamily)
            }
        }
    }
}

@Composable
fun MiSansVfFont() =
    FontFamily(
        Font(
            resource = Res.font.misans_vf_normal,
            weight = FontWeight.Normal,
            style = FontStyle.Normal
        ),
        Font(
            resource = Res.font.misans_vf_bold,
            weight = FontWeight.Bold,
            style = FontStyle.Normal
        ),
        Font(
            resource = Res.font.misans_vf_thin,
            weight = FontWeight.Thin,
            style = FontStyle.Normal
        )
    )

原来直接用就可以了,不需要这么麻烦 Text("首页", fontFamily = MiSansFont()

但是上面的代码在 JVM 是正常的,在 Web 却显示方块,很奇怪

I found that it is OK to use it directly, no need to be so complicated. Text("首页", fontFamily = MiSansFont()

But the above code works in JVM, but it is displayed as a square on the Web, which is very strange

Don't know what resource library you're using, but I guess it's because for the web target, the font files are fetched asyncnormously (calling the getter of the resource will return null at the beginning when the page is loaded), and when it comes to the first composing, the fonts aren't loaded.
Then you will have to handle the expected application recomposing when the fonts loaded manually.

(虽然我英文很烂,但是应该能看懂,懒得中英再写两遍了[捂脸])

@Omico
Copy link
Contributor

Omico commented Jun 16, 2024

Is there any method to avoid loading large fonts?
https://github.com/OmicoDev/wwm
https://wwm.omico.me
image

@eymar
Copy link
Collaborator

eymar commented Jun 17, 2024

@Omico you might try to use Local Font Access API https://developer.mozilla.org/en-US/docs/Web/API/Local_Font_Access_API, it's experimental and available only in Chrome and Edge.

eymar added a commit to JetBrains/compose-multiplatform-core that referenced this issue Jun 20, 2024
TODO:
* -[x] JetBrains/skiko#935 - should me merged
first
* -[x] Use published skiko version instead of the locally built SNAPSHOT
skiko

**Description:**

This PR updates the implementation details of `FontCache` in skikoMain:
- It relies on new skiko API to ensure that manually preloaded and
registered fonts can be used as a last fallback, when other fallbacks
didn't work out
- Update the k/wasm demo, so it preloads the emojis font

Fixes:
- JetBrains/compose-multiplatform#3051
- JetBrains/compose-multiplatform#3967

With this change, it's possible to mix different characters in one Text
without composing the text using AnnotatedString.
Although, when the most of glyphs are known to be from a particular
font, it's more performant to specify the font explicitly when using
Text(...) component or via Theme.

**Usage example:**

The fallbacks won't work automatically, the fallback fonts should be
preloaded like this:

```kotlin
val fontFamilyResolver = LocalFontFamilyResolver.current

...

LaunchedEffect(Unit) {
      val notoEmojisBytes = loadEmojisFontAsBytes() // loadRes(notoColorEmoji).toByteArray()
      val fontFamily = FontFamily(listOf(Font("NotoColorEmoji", notoEmojisBytes)))
      fontFamilyResolver.preload(fontFamily)
      fontsLoaded.value = true
}
```

## Testing
<!-- Optional -->
- Open compose multiplatform core demo: (`./gradlew
:compose:mpp:demo:wasmJsBrowserRun`)
- Go to Components -> Text Field -> Emojis
- Both text fields should show the emojis

<img width="250" alt="Screenshot 2024-06-11 at 11 22 56"
src="https://github.com/JetBrains/compose-multiplatform-core/assets/7372778/2ebd1187-18bf-412c-98f3-9fb2d7c5279f">


<!-- Optional -->
This should be tested by QA



## Release Notes

### Fixes - Web
- Allow preloading the fallback fonts. This enables the usage of emojis
and other unicode characters without manually composing the Text with
AnnotatedString.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working text web
Projects
None yet
Development

No branches or pull requests

7 participants