[PATCH v2] avfilter/vf_drawtext: support color bitmap (CBDT) emoji fonts
George via ffmpeg-devel <[email protected]> Tue, 21 Jul 2026 22:47:41 -0500
| Newsgroups | gmane.comp.video.ffmpeg.devel |
|---|---|
| Message-ID | <CALahHarsCEk66MMe=KnqG_NFwnQbiaZoJnu-xz+W_YZDbXs5kA@mail.gmail.com> |
drawtext could not render color-bitmap fonts such as Noto Color Emoji: glyphs were loaded without FT_LOAD_COLOR, so CBDT/CBLC faces presented their glyphs as monochrome bitmaps and rendering failed with "Monocromatic (1bpp) fonts are not supported". - load glyphs with FT_LOAD_COLOR when the face has color (FT_HAS_COLOR); FreeType then returns premultiplied BGRA bitmaps for color glyphs. For outline glyphs of color-capable faces the flag is a no-op and rendering is unchanged; when the color glyph cannot be rasterized here (unimplemented color format, or a glyph format other than outline and bitmap - e.g. OT-SVG glyphs, or COLRv1 paints depending on the FreeType build), the glyph is loaded without FT_LOAD_COLOR, preserving the previous rendering, while other load errors are reported as real errors. The color path compiles out on FreeType builds without FT_LOAD_COLOR/FT_HAS_COLOR - blend FT_PIXEL_MODE_BGRA glyph bitmaps with per-pixel colors through a new drawutils function, ff_blend_color_bitmap(): each glyph pixel is un-premultiplied and its straight color converted once to the target pixel format with ff_draw_color() (cached per glyph); at blend time, all source pixels mapping to the same destination sample - including each subsampled chroma sample - are accumulated with coverage weighting and composited in a single blend per destination sample, so the result is order-independent. The per-pixel alpha is applied exactly once, as the coverage weight; the destination alpha component is blended towards opacity by coverage. On straight-alpha targets the color components are composited with full source-over, accumulating the destination contribution per full-resolution position ((255 - a_i) ad_i, with the never-subsampled alpha plane read per pixel) and renormalizing by the output alpha, so straight color values stay straight even where the source coverage and the destination alpha vary together inside a subsampling block; on premultiplied and alphaless targets the color components are blended directly, which the coverage weighting premultiplies as needed. Subsampling blocks truncated by the frame edges are weighted by their real in-frame pixel count. The fontcolor option does not apply to color glyphs, which carry their own colors - draw shadows of color glyphs by using the glyph alpha channel as the blend mask for shadowcolor; the border pass skips color glyphs since FT_Glyph_StrokeBorder() only works on outlines (previously borderw with a bitmap glyph made load_glyph fail outright) - bitmap-only faces cannot be scaled: select the fixed strike whose y_ppem is nearest to the requested fontsize with FT_Select_Size() and warn when it differs; glyphs render at the strike native size - do not error out on empty monochrome bitmaps (e.g. the .notdef glyph of an emoji-only font); non-empty 1bpp glyphs are still rejected Verified against computed references (Noto Color Emoji, 109px CBDT strike; figures are worst-case absolute differences in 8-bit code values): yuv420p output at an odd origin vs a reference derived from the rgba render (per-pixel BT.601 conversion + 2x2 chroma box filter): Y max 1.04, U max 0.93, V max 0.93, no sample off by more than 2; on an odd-sized (641x361) frame whose terminal chroma column/row is backed by a single luma position: Y max 0.78, U max 0.88, V max 0.87. Straight-alpha rgba over a transparent background: destination alpha exact (max 0.00), color max 1.00 at coverage above 0.45; over a 50%-alpha background: alpha max 0.36, color max 1.38. yuva420p with the destination alpha checkerboarding 255/64 inside every chroma block, vs the per-pixel source-over reference: Y max 0.86, A max 0.48, U max 3.20 and V max 3.18 with 1 of 4625 glyph-covered chroma blocks above 3 per plane (reference-recovery quantization). A premultiplied-tagged rgba frame (setparams=alpha_mode=premultiplied) receives exactly the premultiplied composite over a transparent background (max 0.00) and is byte-identical to the straight-tagged render over an opaque background. Rendering of outline fonts is unchanged: framemd5 of a DejaVuSans render with border and shadow, and of renders with the color-capable COLR faces Nabla and Noto-COLRv1, are bit-identical before/after this change on yuv420p and rgba. Also tested: partially off-frame placements, per-frame varying fontsize, and the full set under an ASan/UBSan build. Signed-off-by: George Teifel <[email protected]> --- Changes in v2: rebased onto current master (1b1f602); resent inline via the mailing list (v1 was sent as an attachment). doc/filters.texi | 8 ++ libavfilter/drawutils.c | 124 ++++++++++++++++++++++++ libavfilter/drawutils.h | 42 ++++++++ libavfilter/vf_drawtext.c | 195 +++++++++++++++++++++++++++++++++++--- 4 files changed, 358 insertions(+), 11 deletions(-) diff --git a/doc/filters.texi b/doc/filters.texi index 985a161..0972223 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -12638,6 +12638,14 @@ configure FFmpeg with @code{--enable-libfontconfig}. To enable the @var{text_shaping} option, you need to configure FFmpeg with @code{--enable-libfribidi}. +Fonts with color bitmap glyphs (for example CBDT/CBLC based color emoji +fonts such as Noto Color Emoji) are supported: such glyphs are drawn with +their intrinsic colors; the @var{fontcolor} option does not apply to them +and @var{borderw} does not draw a border around them (though it still +takes part in the text layout). Bitmap fonts cannot be scaled: the fixed strike +nearest to the requested @var{fontsize} is selected and the glyphs are +drawn at the strike's native size. + @subsection Syntax It accepts the following parameters: diff --git a/libavfilter/drawutils.c b/libavfilter/drawutils.c index 75bf8f4..10d9a1a 100644 --- a/libavfilter/drawutils.c +++ b/libavfilter/drawutils.c @@ -655,6 +655,130 @@ void ff_blend_mask(FFDrawContext *draw, FFDrawColor *color, } } +void ff_blend_color_bitmap(FFDrawContext *draw, + uint8_t *dst[], int dst_linesize[], + int dst_w, int dst_h, + const uint16_t *comps[4], const uint8_t *alpha, + int src_linesize, int src_w, int src_h, + unsigned global_alpha, int x0, int y0) +{ + const AVPixFmtDescriptor *desc = draw->desc; + unsigned nb_comp = desc->nb_components - + !!(desc->flags & AV_PIX_FMT_FLAG_ALPHA && + !(draw->flags & FF_DRAW_PROCESS_ALPHA)); + /* Straight-alpha targets need full source-over compositing: the + * destination color is weighted by the destination alpha and the + * result renormalized by the output alpha. Premultiplied and + * alphaless targets blend the color components directly. */ + int straight = desc->flags & AV_PIX_FMT_FLAG_ALPHA && + draw->alpha != AVALPHA_MODE_PREMULTIPLIED; + const AVComponentDescriptor *ac = &desc->comp[desc->nb_components - 1]; + unsigned amax = (1u << (ac->depth + ac->shift)) - 1; + int xs0 = 0, ys0 = 0; + + clip_interval(dst_w, &x0, &src_w, &xs0); + clip_interval(dst_h, &y0, &src_h, &ys0); + if (src_w <= 0 || src_h <= 0 || !global_alpha) + return; + + for (unsigned comp = 0; comp < nb_comp; comp++) { + const AVComponentDescriptor *c = &desc->comp[comp]; + /* The alpha component is last, so color components read the + * destination alpha before this call updates it. */ + int sover = straight && comp != desc->nb_components - 1; + int plane = c->plane; + unsigned hsub = draw->hsub[plane], vsub = draw->vsub[plane]; + int xd0 = x0 >> hsub, xd1 = (x0 + src_w - 1) >> hsub; + int yd0 = y0 >> vsub, yd1 = (y0 + src_h - 1) >> vsub; + + for (int yd = yd0; yd <= yd1; yd++) { + int sy0 = FFMAX(yd << vsub, y0) - y0; + int sy1 = FFMIN((yd + 1) << vsub, y0 + src_h) - y0; + /* Subsampling blocks truncated by the image edges are backed + * by fewer pixels and weighted accordingly. */ + int bh = FFMIN((yd + 1) << vsub, dst_h) - (yd << vsub); + uint8_t *p = pointer_at(draw, dst, dst_linesize, plane, + xd0 << hsub, yd << vsub) + c->offset; + + for (int xd = xd0; xd <= xd1; xd++) { + int sx0 = FFMAX(xd << hsub, x0) - x0; + int sx1 = FFMIN((xd + 1) << hsub, x0 + src_w) - x0; + int bw = FFMIN((xd + 1) << hsub, dst_w) - (xd << hsub); + /* weight sum of a fully covered, fully opaque block */ + unsigned denom = 255u * bw * bh; + uint32_t sum_a = 0, sum_c = 0; + + for (int sy = sy0; sy < sy1; sy++) { + const uint8_t *arow = alpha + + (ys0 + sy) * src_linesize + xs0; + const uint16_t *crow = comps[comp] + + (ys0 + sy) * src_linesize + xs0; + + for (int sx = sx0; sx < sx1; sx++) { + unsigned a = arow[sx]; + + if (global_alpha != 255) + a = (a * global_alpha + 127) / 255; + sum_a += a; + sum_c += a * crow[sx]; + } + } + if (sum_a) { + unsigned vd = c->depth <= 8 ? *p : AV_RL16(p); + uint64_t out; + + if (sover) { + /* Per full-res position i of the block, the blend + * retains (255 - a_i) ad_i of the destination + * (a_i = 0 where the source does not cover); the + * alpha plane is never subsampled. + * out = sum(a_i cs_i + (255 - a_i) ad_i cd) / + * sum(a_i amax + (255 - a_i) ad_i); + * sum_a > 0 keeps the divisor positive. */ + uint64_t sum_r = 0; + + for (int yb = 0; yb < bh; yb++) { + int sy = (yd << vsub) + yb - y0; + const uint8_t *ap = pointer_at(draw, dst, + dst_linesize, ac->plane, xd << hsub, + (yd << vsub) + yb) + ac->offset; + + for (int xb = 0; xb < bw; xb++) { + int sx = (xd << hsub) + xb - x0; + unsigned a = 0; + + if (sx >= 0 && sx < src_w && + sy >= 0 && sy < src_h) { + a = alpha[(ys0 + sy) * src_linesize + + xs0 + sx]; + if (global_alpha != 255) + a = (a * global_alpha + 127) / 255; + } + sum_r += (255 - a) * + (uint64_t)(ac->depth <= 8 ? + ap[0] : AV_RL16(ap)); + ap += draw->pixelstep[ac->plane]; + } + } + uint64_t num = (uint64_t)sum_c * amax + sum_r * vd; + uint64_t den = (uint64_t)sum_a * amax + sum_r; + + out = (num + den / 2) / den; + } else { + out = ((uint64_t)vd * (denom - sum_a) + sum_c + + denom / 2) / denom; + } + if (c->depth <= 8) + *p = out; + else + AV_WL16(p, out); + } + p += draw->pixelstep[plane]; + } + } + } +} + int ff_draw_round_to_sub(FFDrawContext *draw, int sub_dir, int round_dir, int value) { diff --git a/libavfilter/drawutils.h b/libavfilter/drawutils.h index a7fc967..63de58c 100644 --- a/libavfilter/drawutils.h +++ b/libavfilter/drawutils.h @@ -153,6 +153,48 @@ void ff_blend_mask(FFDrawContext *draw, FFDrawColor *color, const uint8_t *mask, int mask_linesize, int mask_w, int mask_h, int l2depth, unsigned endianness, int x0, int y0); +/** + * Blend a bitmap with per-pixel colors and per-pixel alpha. + * + * On subsampled targets, all the source pixels mapping to the same + * destination sample are accumulated with coverage weighting and blended + * in a single operation per destination sample, so the result does not + * depend on any per-pixel compositing order. Subsampling blocks truncated + * by the image edges are weighted by their real in-image pixel count. + * + * The per-pixel alpha is applied exactly once, as the coverage weight of + * the blend; the source component values must therefore be straight + * (non-premultiplied) colors converted with an opaque alpha, which also + * makes the destination alpha component blend towards opacity by + * coverage. On straight-alpha targets the color components are + * composited with full source-over (the destination color weighted by + * the destination alpha, renormalized by the output alpha); premultiplied + * and alphaless targets blend the color components directly. + * + * @param draw draw context + * @param dst destination image + * @param dst_linesize line stride of the destination + * @param dst_w width of the destination image + * @param dst_h height of the destination image + * @param comps source component values, as produced by + * ff_draw_color() from straight colors with alpha + * 255, stored component-major: + * comps[comp][y * src_linesize + x] + * @param alpha per-pixel blend weights (0-255) + * @param src_linesize line stride of comps and alpha, in pixels + * @param src_w width of the source rectangle + * @param src_h height of the source rectangle + * @param global_alpha extra alpha multiplier (0-255, 255 for opaque) + * @param x0 horizontal position of the overlay + * @param y0 vertical position of the overlay + */ +void ff_blend_color_bitmap(FFDrawContext *draw, + uint8_t *dst[], int dst_linesize[], + int dst_w, int dst_h, + const uint16_t *comps[4], const uint8_t *alpha, + int src_linesize, int src_w, int src_h, + unsigned global_alpha, int x0, int y0); + /** * Round a dimension according to subsampling. * diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c index 2ccb036..bd555b1 100644 --- a/libavfilter/vf_drawtext.c +++ b/libavfilter/vf_drawtext.c @@ -52,6 +52,7 @@ #include "libavutil/opt.h" #include "libavutil/random_seed.h" #include "libavutil/parseutils.h" +#include "libavutil/pixdesc.h" #include "libavutil/time.h" #include "libavutil/timecode.h" #include "libavutil/time_internal.h" @@ -207,6 +208,13 @@ typedef struct Glyph { /** Outlined glyph bitmaps with 1/4 pixel precision in both directions */ FT_BitmapGlyph border_bglyph[16]; FT_BBox bbox; + /** Straight-alpha component values of a color (BGRA) glyph converted + * to the target pixel format with ff_draw_color(), component-major; + * lazily built on the first draw */ + uint16_t *comp_cache; + /** Alpha channel of a color (BGRA) glyph; blend weights for the glyph + * itself and blend mask for its shadow */ + uint8_t *alpha_cache; } Glyph; /** Global text metrics */ @@ -435,7 +443,28 @@ static av_cold int set_fontsize(AVFilterContext *ctx, unsigned int fontsize) int err; DrawTextContext *s = ctx->priv; - if ((err = FT_Set_Pixel_Sizes(s->face, 0, fontsize))) { + if (!FT_IS_SCALABLE(s->face) && s->face->num_fixed_sizes > 0) { + /* Bitmap fonts (e.g. CBDT/CBLC color emoji fonts) cannot be scaled; + * use the fixed strike closest to the requested size. */ + int64_t req64 = (int64_t)fontsize << 6; + int best = 0; + + for (int i = 1; i < s->face->num_fixed_sizes; i++) { + if (FFABS((int64_t)s->face->available_sizes[i].y_ppem - req64) < + FFABS((int64_t)s->face->available_sizes[best].y_ppem - req64)) + best = i; + } + err = FT_Select_Size(s->face, best); + if (!err && s->face->available_sizes[best].y_ppem != req64) + av_log(ctx, AV_LOG_WARNING, + "Font does not scale, using the nearest fixed strike " + "(%d pixels) for the requested size %u\n", + (int)((s->face->available_sizes[best].y_ppem + 32) >> 6), + fontsize); + } else { + err = FT_Set_Pixel_Sizes(s->face, 0, fontsize); + } + if (err) { av_log(ctx, AV_LOG_ERROR, "Could not set font size to %d pixels: %s\n", fontsize, FT_ERRMSG(err)); return AVERROR(EINVAL); @@ -743,8 +772,40 @@ static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code, in glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL); cached = !!glyph; if (!glyph) { - if (FT_Load_Glyph(s->face, code, s->ft_load_flags)) { - return AVERROR(EINVAL); + int loaded = 0; + +#if defined(FT_LOAD_COLOR) && defined(FT_HAS_COLOR) + /* Load the color version of a glyph when the face has one (color + * fonts expose them as BGRA bitmaps, e.g. CBDT/CBLC emoji fonts). + * Fall back on the regular glyph when the color glyph cannot be + * rasterized here: an unimplemented color format, or a loadable + * glyph format other than outline and bitmap (e.g. OT-SVG glyphs, + * or COLRv1 paints depending on the FreeType build). Any other + * load error is a real error and is reported as such. */ + if (FT_HAS_COLOR(s->face)) { + FT_Error ft_err = FT_Load_Glyph(s->face, code, + s->ft_load_flags | FT_LOAD_COLOR); + + if (!ft_err) { + loaded = s->face->glyph->format == FT_GLYPH_FORMAT_OUTLINE || + s->face->glyph->format == FT_GLYPH_FORMAT_BITMAP; + } else if (ft_err != FT_Err_Unimplemented_Feature) { + av_log(ctx, AV_LOG_ERROR, + "Could not load glyph %u: %s\n", code, + FT_ERRMSG(ft_err)); + return AVERROR(EINVAL); + } + } +#endif + if (!loaded) { + FT_Error ft_err = FT_Load_Glyph(s->face, code, s->ft_load_flags); + + if (ft_err) { + av_log(ctx, AV_LOG_ERROR, + "Could not load glyph %u: %s\n", code, + FT_ERRMSG(ft_err)); + return AVERROR(EINVAL); + } } glyph = av_mallocz(sizeof(*glyph)); if (!glyph) { @@ -757,7 +818,9 @@ static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code, in ret = AVERROR(EINVAL); goto error; } - if (s->borderw) { + /* A stroked border can only be derived from outline glyphs; bitmap + * glyphs (color emojis) are drawn without a border. */ + if (s->borderw && glyph->glyph->format == FT_GLYPH_FORMAT_OUTLINE) { FT_Glyph tmp = glyph->glyph; if (FT_Glyph_StrokeBorder(&tmp, s->stroker, 0, 0)) { ret = AVERROR_EXTERNAL; @@ -776,7 +839,8 @@ static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code, in av_tree_insert(&s->glyphs, glyph, glyph_cmp, &node); cached = 1; } else { - if (s->borderw && !glyph->border_glyph) { + if (s->borderw && !glyph->border_glyph && + glyph->glyph->format == FT_GLYPH_FORMAT_OUTLINE) { FT_Glyph tmp = glyph->glyph; if (FT_Glyph_StrokeBorder(&tmp, s->stroker, 0, 0)) { ret = AVERROR_EXTERNAL; @@ -801,12 +865,14 @@ static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code, in } glyph->bglyph[idx] = (FT_BitmapGlyph)tmp_glyph; } - if (glyph->bglyph[idx]->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) { + if (glyph->bglyph[idx]->bitmap.pixel_mode == FT_PIXEL_MODE_MONO && + glyph->bglyph[idx]->bitmap.width && + glyph->bglyph[idx]->bitmap.rows) { av_log(ctx, AV_LOG_ERROR, "Monocromatic (1bpp) fonts are not supported.\n"); ret = AVERROR(EINVAL); goto error; } - if (s->borderw && !glyph->border_bglyph[idx]) { + if (s->borderw && glyph->border_glyph && !glyph->border_bglyph[idx]) { FT_Glyph tmp_glyph = glyph->border_glyph; if (FT_Glyph_To_Bitmap(&tmp_glyph, FT_RENDER_MODE_NORMAL, &shift, 0)) { ret = AVERROR_EXTERNAL; @@ -1119,6 +1185,8 @@ static int glyph_enu_free(void *opaque, void *elem) if (glyph->border_glyph && glyph->border_glyph != glyph->glyph) FT_Done_Glyph(glyph->border_glyph); FT_Done_Glyph(glyph->glyph); + av_freep(&glyph->comp_cache); + av_freep(&glyph->alpha_cache); av_free(elem); return 0; } @@ -1278,10 +1346,73 @@ static void update_alpha(DrawTextContext *s) s->alpha = 256 * alpha; } +#if defined(FT_LOAD_COLOR) && defined(FT_HAS_COLOR) +/* Builds the drawing caches of a color (BGRA) glyph: the glyph's pixel + * colors converted to the target pixel format, and its alpha channel */ +static int build_color_glyph_caches(DrawTextContext *s, Glyph *glyph, + const FT_Bitmap *bitmap) +{ + const int w = bitmap->width, h = bitmap->rows; + const AVPixFmtDescriptor *desc = s->dc.desc; + size_t nb_px, comp_size; + + if (glyph->comp_cache) + return 0; + + if (av_size_mult(w, h, &nb_px) < 0 || + av_size_mult(nb_px, desc->nb_components * sizeof(*glyph->comp_cache), + &comp_size) < 0) + return AVERROR(EINVAL); + + glyph->comp_cache = av_malloc(comp_size); + glyph->alpha_cache = av_malloc(nb_px); + if (!glyph->comp_cache || !glyph->alpha_cache) { + av_freep(&glyph->comp_cache); + av_freep(&glyph->alpha_cache); + return AVERROR(ENOMEM); + } + + for (int y = 0; y < h; y++) { + const uint8_t *p = bitmap->buffer + y * bitmap->pitch; + + for (int x = 0; x < w; x++) { + /* Color glyphs are stored as premultiplied BGRA */ + const uint8_t *px = p + 4 * x; + unsigned a = px[3]; + uint8_t rgba[4]; + FFDrawColor color; + + rgba[0] = a ? (px[2] * 255u + a / 2) / a : 0; + rgba[1] = a ? (px[1] * 255u + a / 2) / a : 0; + rgba[2] = a ? (px[0] * 255u + a / 2) / a : 0; + /* Store straight component values with a full alpha: the pixel + * alpha is applied exactly once, as the coverage weight of the + * blend, which also premultiplies where the target needs it, + * and the destination alpha component must be blended towards + * opacity by coverage. */ + rgba[3] = 255; + ff_draw_color(&s->dc, &color, rgba); + glyph->alpha_cache[y * w + x] = a; + for (int i = 0; i < desc->nb_components; i++) { + int plane = desc->comp[i].plane; + int offset = desc->comp[i].offset; + uint16_t v = desc->comp[i].depth > 8 ? + color.comp[plane].u16[offset / 2] : + color.comp[plane].u8[offset]; + + glyph->comp_cache[i * nb_px + y * w + x] = v; + } + } + } + + return 0; +} +#endif /* FT_LOAD_COLOR && FT_HAS_COLOR */ + static int draw_glyphs(AVFilterContext *ctx, AVFrame *frame, FFDrawColor *color, TextMetrics *metrics, - int x, int y, int borderw) + int x, int y, int borderw, int shadow) { DrawTextContext *s = ctx->priv; int g, l, x1, y1, w1, h1, idx; @@ -1327,6 +1458,14 @@ static int draw_glyphs(AVFilterContext *ctx, AVFrame *frame, idx = get_subpixel_idx(info->shift_x64, info->shift_y64); b_glyph = borderw ? glyph->border_bglyph[idx] : glyph->bglyph[idx]; + if (!b_glyph) { + /* Color (BGRA) glyphs have no stroked border variant: draw + * their shadow from the normal glyph, skip the border pass. */ + if (shadow) + b_glyph = glyph->bglyph[idx]; + else + continue; + } bitmap = b_glyph->bitmap; x1 = x + info->x + b_glyph->left; y1 = y + info->y - b_glyph->top + offset_y; @@ -1359,6 +1498,40 @@ static int draw_glyphs(AVFilterContext *ctx, AVFrame *frame, w1 = FFMIN(clip_x - x1, w1 - dx); h1 = FFMIN(clip_y - y1, h1 - dy); +#if defined(FT_LOAD_COLOR) && defined(FT_HAS_COLOR) + if (bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { + int err = build_color_glyph_caches(s, glyph, &bitmap); + if (err < 0) + return err; + if (shadow) { + /* Draw the shadow using the glyph's alpha channel as + * the blend mask for the shadow color */ + ff_blend_mask(&s->dc, color, frame->data, + frame->linesize, clip_x, clip_y, + glyph->alpha_cache + dx + dy * bitmap.width, + bitmap.width, w1, h1, 3, 0, x1, y1); + } else { + /* Draw the glyph with its own per-pixel colors; the + * fontcolor option does not apply to color glyphs. + * The source rectangle is already clipped against the + * drawing region, so pass the frame dimensions to let + * frame-edge subsampling blocks be weighted by their + * real in-frame pixel count. */ + const uint16_t *comps[4] = { NULL }; + size_t nb_px = (size_t)bitmap.width * bitmap.rows; + + for (int i = 0; i < s->dc.desc->nb_components; i++) + comps[i] = glyph->comp_cache + i * nb_px + + dy * bitmap.width + dx; + ff_blend_color_bitmap(&s->dc, frame->data, + frame->linesize, frame->width, frame->height, + comps, glyph->alpha_cache + dy * bitmap.width + dx, + bitmap.width, w1, h1, s->alpha, x1, y1); + } + continue; + } +#endif + ff_blend_mask(&s->dc, color, frame->data, frame->linesize, clip_x, clip_y, bitmap.buffer + pdx, bitmap.pitch, w1, h1, 3, 0, x1, y1); } @@ -1843,20 +2016,20 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame) if (s->shadowx || s->shadowy) { if ((ret = draw_glyphs(ctx, frame, &shadowcolor, &metrics, - s->shadowx, s->shadowy, s->borderw)) < 0) { + s->shadowx, s->shadowy, s->borderw, 1)) < 0) { goto fail; } } if (s->borderw) { if ((ret = draw_glyphs(ctx, frame, &bordercolor, &metrics, - 0, 0, s->borderw)) < 0) { + 0, 0, s->borderw, 0)) < 0) { goto fail; } } if ((ret = draw_glyphs(ctx, frame, &fontcolor, &metrics, 0, - 0, 0)) < 0) { + 0, 0, 0)) < 0) { goto fail; } } -- 2.43.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]