@@ -2031,45 +2031,58 @@ export class Image {
20312031
20322032 /**
20332033 * Creates a new image containing the rendered text.
2034- * @param font TrueType (ttf/ttc) or OpenType (otf) font buffer to use
2034+ * @param font TrueType (ttf/ttc) or OpenType (otf) font buffer to use, or several by priority: every character
2035+ * comes from the first font that has it (e.g. `[text, emoji]`), the first one sets the line height. Color
2036+ * glyphs (COLR version 0 layers and CBDT bitmaps, as in emoji fonts) keep their colors
20352037 * @param size Font size in pixels (per em)
20362038 * @param text Text to render
20372039 * @param options Color, layout and style, see {@link TextOptions}; the image grows by the outline width on every
20382040 * side
20392041 * @return The rendered text
20402042 */
2041- static renderText ( font : BinaryData , size : number , text : string , options ?: TextOptions ) : Promise < Image > ;
2043+ static renderText ( font : BinaryData | BinaryData [ ] , size : number , text : string , options ?: TextOptions ) : Promise < Image > ;
20422044 /**
20432045 * Creates a new image containing the rendered text.
2044- * @param font TrueType (ttf/ttc) or OpenType (otf) font buffer to use
2046+ * @param font TrueType (ttf/ttc) or OpenType (otf) font buffer to use, or several by priority: every character
2047+ * comes from the first font that has it (e.g. `[text, emoji]`), the first one sets the line height. Color
2048+ * glyphs (COLR version 0 layers and CBDT bitmaps, as in emoji fonts) keep their colors
20452049 * @param size Font size in pixels (per em)
20462050 * @param text Text to render
20472051 * @param [color=0xffffffff] Text color to use
20482052 * @param [layout] The text layout to use
20492053 * @param style Outline, see {@link TextStyle}; the image grows by the outline width on every side
20502054 * @return The rendered text
20512055 */
2052- static renderText ( font : BinaryData , size : number , text : string , color ?: Color , layout ?: TextLayout , style ?: TextStyle ) : Promise < Image > ;
2053- static async renderText ( font : BinaryData , size : number , text : string , colorOrOptions : Color | TextOptions = 0xffffffff , layoutArgument ?: TextLayout , styleArgument : TextStyle = { } ) : Promise < Image > {
2056+ static renderText ( font : BinaryData | BinaryData [ ] , size : number , text : string , color ?: Color , layout ?: TextLayout , style ?: TextStyle ) : Promise < Image > ;
2057+ static async renderText ( font : BinaryData | BinaryData [ ] , size : number , text : string , colorOrOptions : Color | TextOptions = 0xffffffff , layoutArgument ?: TextLayout , styleArgument : TextStyle = { } ) : Promise < Image > {
20542058 const options : TextOptions = isOptions ( colorOrOptions ) ? colorOrOptions : { ...styleArgument , color : colorOrOptions , layout : layoutArgument } ;
20552059 const { color = 0xffffffff , layout = new TextLayout ( ) , strokeWidth = 0 , strokeColor = 0x000000ff , hinting = true } = options ;
20562060 if ( isNaN ( strokeWidth ) || strokeWidth < 0 )
20572061 throw new RangeError ( 'Invalid stroke width' ) ;
2062+ const fonts = Array . isArray ( font ) ? font : [ font ] ;
2063+ if ( ! fonts . length ) throw new RangeError ( 'No font given' ) ;
20582064
2059- const rendered = textRenderer . render ( view ( font ) , size , text , layout , hinting ) ;
20602065 const [ r , g , b , a ] = Image . colorToRGBA ( color ) ;
2066+ // color glyph layers in the text color take it without its alpha, which applies to the whole color glyph
2067+ const rendered = await textRenderer . render ( fonts . map ( view ) , size , text , layout , hinting , ( r << 24 | g << 16 | b << 8 | 255 ) >>> 0 ) ;
20612068
20622069 // the outline needs room around the glyphs
20632070 const pad = Math . ceil ( strokeWidth ) ;
20642071 const width = rendered . width + 2 * pad , height = rendered . height + 2 * pad ;
20652072 const coverage = new Uint8Array ( width * height ) ;
20662073 for ( let y = 0 ; y < rendered . height ; y ++ )
20672074 coverage . set ( rendered . coverage . subarray ( y * rendered . width , ( y + 1 ) * rendered . width ) , ( y + pad ) * width + pad ) ;
2075+ let colored : Uint8ClampedArray | null = null ;
2076+ if ( rendered . color ) {
2077+ colored = new Uint8ClampedArray ( width * height * 4 ) ;
2078+ for ( let y = 0 ; y < rendered . height ; y ++ )
2079+ colored . set ( rendered . color . subarray ( y * rendered . width * 4 , ( y + 1 ) * rendered . width * 4 ) , ( ( y + pad ) * width + pad ) * 4 ) ;
2080+ }
20682081
20692082 const image = new Image ( width , height ) ;
20702083 const bitmap = image . bitmap ;
20712084
2072- if ( ! strokeWidth ) {
2085+ if ( ! strokeWidth && ! colored ) {
20732086 for ( let i = 0 ; i < coverage . length ; i ++ ) {
20742087 if ( ! coverage [ i ] ) continue ;
20752088 bitmap [ i * 4 ] = r ;
@@ -2080,19 +2093,38 @@ export class Image {
20802093 return image ;
20812094 }
20822095
2083- const stroke = outline ( coverage , width , height , strokeWidth ) ;
2096+ // the outline goes around the color glyphs as well
2097+ let shape = coverage ;
2098+ if ( colored && strokeWidth ) {
2099+ shape = coverage . slice ( ) ;
2100+ for ( let i = 0 ; i < shape . length ; i ++ ) shape [ i ] = Math . max ( shape [ i ] , colored [ i * 4 + 3 ] ) ;
2101+ }
2102+ const stroke = strokeWidth ? outline ( shape , width , height , strokeWidth ) : null ;
20842103 const [ sr , sg , sb , sa ] = Image . colorToRGBA ( strokeColor ) ;
20852104
2086- // the glyphs drawn over their outline
2105+ // color glyphs over the text (premultiplied, with the text's alpha), both over their outline
20872106 for ( let i = 0 ; i < coverage . length ; i ++ ) {
2088- const af = coverage [ i ] / 255 * a / 255 , as = stroke [ i ] * sa / 255 ;
2089- const ao = af + as * ( 1 - af ) ;
2090- if ( ao <= 0 ) continue ;
2091-
2092- bitmap [ i * 4 ] = ( r * af + sr * as * ( 1 - af ) ) / ao ;
2093- bitmap [ i * 4 + 1 ] = ( g * af + sg * as * ( 1 - af ) ) / ao ;
2094- bitmap [ i * 4 + 2 ] = ( b * af + sb * as * ( 1 - af ) ) / ao ;
2095- bitmap [ i * 4 + 3 ] = ao * 255 ;
2107+ let af = coverage [ i ] / 255 * a / 255 , pr = r * af , pg = g * af , pb = b * af ;
2108+ if ( colored && colored [ i * 4 + 3 ] ) {
2109+ const ca = colored [ i * 4 + 3 ] / 255 * a / 255 , keep = 1 - ca , f = a / 255 ;
2110+ pr = colored [ i * 4 ] * f + pr * keep ;
2111+ pg = colored [ i * 4 + 1 ] * f + pg * keep ;
2112+ pb = colored [ i * 4 + 2 ] * f + pb * keep ;
2113+ af = ca + af * keep ;
2114+ }
2115+ if ( stroke ) {
2116+ const as = stroke [ i ] * sa / 255 * ( 1 - af ) ;
2117+ pr += sr * as ;
2118+ pg += sg * as ;
2119+ pb += sb * as ;
2120+ af += as ;
2121+ }
2122+ if ( af <= 0 ) continue ;
2123+
2124+ bitmap [ i * 4 ] = pr / af ;
2125+ bitmap [ i * 4 + 1 ] = pg / af ;
2126+ bitmap [ i * 4 + 2 ] = pb / af ;
2127+ bitmap [ i * 4 + 3 ] = af * 255 ;
20962128 }
20972129
20982130 return image ;
0 commit comments