From a0e7d161672aa5bbb38f3249476008b7bf8582f5 Mon Sep 17 00:00:00 2001 From: Francesco Abbate Date: Thu, 4 Jun 2020 16:29:28 +0200 Subject: [PATCH] Implement subpixel LCD font rendering --- src/agg_lcd_distribution_lut.h | 78 ++++++++++++++++++++ src/font_renderer.cpp | 131 +++++++++++++++++++++++++++------ src/font_renderer.h | 9 ++- src/font_renderer_alpha.h | 86 +--------------------- src/renderer.c | 27 +++++-- tests/agg_font_render_test.c | 3 +- 6 files changed, 218 insertions(+), 116 deletions(-) create mode 100644 src/agg_lcd_distribution_lut.h diff --git a/src/agg_lcd_distribution_lut.h b/src/agg_lcd_distribution_lut.h new file mode 100644 index 00000000..ce4a0a9e --- /dev/null +++ b/src/agg_lcd_distribution_lut.h @@ -0,0 +1,78 @@ +//---------------------------------------------------------------------------- +// Anti-Grain Geometry (AGG) - Version 2.5 +// A high quality rendering engine for C++ +// Copyright (C) 2002-2006 Maxim Shemanarev +// Contact: mcseem@antigrain.com +// mcseemagg@yahoo.com +// http://antigrain.com +// +// AGG is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// AGG is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with AGG; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +//---------------------------------------------------------------------------- + +#ifndef AGG_LCD_DISTRIBUTION_LUT_INCLUDED +#define AGG_LCD_DISTRIBUTION_LUT_INCLUDED + +#include "agg_basics.h" + +namespace agg +{ + + //=====================================================lcd_distribution_lut + class lcd_distribution_lut + { + public: + lcd_distribution_lut(double prim, double second, double tert) + { + double norm = 1.0 / (prim + second*2 + tert*2); + prim *= norm; + second *= norm; + tert *= norm; + for(unsigned i = 0; i < 256; i++) + { + unsigned b = (i << 8); + unsigned s = round(second * b); + unsigned t = round(tert * b); + unsigned p = b - (2*s + 2*t); + + m_data[3*i + 1] = s; /* secondary */ + m_data[3*i + 2] = t; /* tertiary */ + m_data[3*i ] = p; /* primary */ + } + } + + unsigned convolution(const int8u* covers, int i0, int i_min, int i_max) const + { + unsigned sum = 0; + int k_min = (i0 >= i_min + 2 ? -2 : i_min - i0); + int k_max = (i0 <= i_max - 2 ? 2 : i_max - i0); + for (int k = k_min; k <= k_max; k++) + { + /* select the primary, secondary or tertiary channel */ + int channel = abs(k) % 3; + int8u c = covers[i0 + k]; + sum += m_data[3*c + channel]; + } + + return (sum + 128) >> 8; + } + + private: + unsigned short m_data[256*3]; + }; + +} + +#endif diff --git a/src/font_renderer.cpp b/src/font_renderer.cpp index a2c440be..3c20e2b5 100644 --- a/src/font_renderer.cpp +++ b/src/font_renderer.cpp @@ -1,5 +1,6 @@ #include "font_renderer.h" +#include "agg_lcd_distribution_lut.h" #include "agg_pixfmt_rgb.h" #include "agg_pixfmt_rgba.h" #include "agg_gamma_lut.h" @@ -10,21 +11,28 @@ typedef agg::blender_rgb_gamma > b class FontRendererImpl { public: + // Conventional LUT values: (1./3., 2./9., 1./9.) + // The values below are fine tuned as in the Elementary Plot library. + FontRendererImpl(bool hinting, bool kerning, float gamma_value) : m_renderer(hinting, kerning), m_gamma_lut(double(gamma_value)), - m_blender() + m_blender(), + m_lcd_lut(0.448, 0.184, 0.092) { m_blender.gamma(m_gamma_lut); } font_renderer_alpha& renderer_alpha() { return m_renderer; } blender_gamma_type& blender() { return m_blender; } + agg::gamma_lut<>& gamma() { return m_gamma_lut; } + agg::lcd_distribution_lut& lcd_distribution_lut() { return m_lcd_lut; } private: font_renderer_alpha m_renderer; agg::gamma_lut<> m_gamma_lut; blender_gamma_type m_blender; + agg::lcd_distribution_lut m_lcd_lut; }; FontRenderer *FontRendererNew(unsigned int flags, float gamma) { @@ -51,9 +59,10 @@ int FontRendererGetFontHeight(FontRenderer *font_renderer, float size) { return int((ascender - descender) * face_height * scale + 0.5); } -static void glyph_trim_rect(agg::rendering_buffer& ren_buf, GlyphBitmapInfo *gli) { +static void glyph_trim_rect(agg::rendering_buffer& ren_buf, GlyphBitmapInfo *gli, int subpixel_scale) { const int height = ren_buf.height(); - int x0 = gli->x0, y0 = gli->y0, x1 = gli->x1, y1 = gli->y1; + int x0 = gli->x0 * subpixel_scale, x1 = gli->x1 * subpixel_scale; + int y0 = gli->y0, y1 = gli->y1; for (int y = gli->y0; y < gli->y1; y++) { uint8_t *row = ren_buf.row_ptr(height - 1 - y); unsigned int row_bitsum = 0; @@ -81,35 +90,48 @@ static void glyph_trim_rect(agg::rendering_buffer& ren_buf, GlyphBitmapInfo *gli int xtriml = x0, xtrimr = x1; for (int y = y0; y < y1; y++) { uint8_t *row = ren_buf.row_ptr(height - 1 - y); - for (int x = x0; x < x1; x++) { - if (row[x]) { + for (int x = x0; x < x1; x += subpixel_scale) { + unsigned int xaccu = 0; + for (int i = 0; i < subpixel_scale; i++) { + xaccu |= row[x + i]; + } + if (xaccu > 0) { + // FIXME: fix xs comparaison below. if (x < xtriml) xtriml = x; break; } } - for (int x = x1 - 1; x >= x0; x--) { - if (row[x]) { + for (int x = x1 - subpixel_scale; x >= x0; x -= subpixel_scale) { + unsigned int xaccu = 0; + for (int i = 0; i < subpixel_scale; i++) { + xaccu |= row[x + i]; + } + if (xaccu > 0) { if (x > xtrimr) xtrimr = x + 1; break; } } } - gli->xoff += (xtriml - x0); + gli->xoff += (xtriml - x0) / subpixel_scale; gli->yoff += (y0 - gli->y0); - gli->x0 = xtriml; + gli->x0 = xtriml / subpixel_scale; gli->y0 = y0; - gli->x1 = xtrimr; + gli->x1 = xtrimr / subpixel_scale; gli->y1 = y1; } +static int ceil_to_multiple(int n, int p) { + return p * ((n + p - 1) / p); +} + int FontRendererBakeFontBitmap(FontRenderer *font_renderer, int font_height, void *pixels, int pixels_width, int pixels_height, - int first_char, int num_chars, GlyphBitmapInfo *glyphs) + int first_char, int num_chars, GlyphBitmapInfo *glyphs, int subpixel_scale) { font_renderer_alpha& renderer_alpha = font_renderer->renderer_alpha(); const int pixel_size = 1; - memset(pixels, 0x00, pixels_width * pixels_height * pixel_size); + memset(pixels, 0x00, pixels_width * pixels_height * subpixel_scale * pixel_size); double ascender, descender; renderer_alpha.get_font_vmetrics(ascender, descender); @@ -117,11 +139,12 @@ int FontRendererBakeFontBitmap(FontRenderer *font_renderer, int font_height, const int ascender_px = int(ascender * font_height + 0.5); const int descender_px = int(descender * font_height + 0.5); - const int pad_y = font_height / 10, pad_x = 1; + const int pad_y = font_height / 10; const int y_step = font_height + 2 * pad_y; - agg::rendering_buffer ren_buf((agg::int8u *) pixels, pixels_width, pixels_height, -pixels_width * pixel_size); - int x = 0, y = pixels_height; + agg::rendering_buffer ren_buf((agg::int8u *) pixels, pixels_width * subpixel_scale, pixels_height, -pixels_width * subpixel_scale * pixel_size); + const int x_start = subpixel_scale; + int x = x_start, y = pixels_height; int res = 0; const agg::alpha8 text_color(0xff); #ifdef FONT_RENDERER_HEIGHT_HACK @@ -131,8 +154,8 @@ int FontRendererBakeFontBitmap(FontRenderer *font_renderer, int font_height, #endif for (int i = 0; i < num_chars; i++) { int codepoint = first_char + i; - if (x + font_height > pixels_width) { - x = 0; + if (x + font_height * subpixel_scale > pixels_width * subpixel_scale) { + x = x_start; y -= y_step; } if (y - font_height - 2 * pad_y < 0) { @@ -142,22 +165,22 @@ int FontRendererBakeFontBitmap(FontRenderer *font_renderer, int font_height, const int y_baseline = y - pad_y - font_height; double x_next = x, y_next = y_baseline; - renderer_alpha.render_codepoint(ren_buf, font_height_reduced, text_color, x_next, y_next, codepoint); - int x_next_i = int(x_next + 1.0); + renderer_alpha.render_codepoint(ren_buf, font_height_reduced, text_color, x_next, y_next, codepoint, subpixel_scale); + int x_next_i = (subpixel_scale == 1 ? int(x_next + 1.0) : ceil_to_multiple(x_next + 0.5, subpixel_scale)); GlyphBitmapInfo& glyph_info = glyphs[i]; - glyph_info.x0 = x; + glyph_info.x0 = x / subpixel_scale; glyph_info.y0 = pixels_height - (y_baseline + ascender_px + pad_y); - glyph_info.x1 = x_next_i; + glyph_info.x1 = x_next_i / subpixel_scale; glyph_info.y1 = pixels_height - (y_baseline + descender_px - pad_y); glyph_info.xoff = 0; glyph_info.yoff = -pad_y; - glyph_info.xadvance = x_next - x; + glyph_info.xadvance = (x_next - x) / subpixel_scale; - glyph_trim_rect(ren_buf, &glyph_info); + glyph_trim_rect(ren_buf, &glyph_info, subpixel_scale); - x = x_next_i + pad_x; + x = x_next_i; #ifdef FONT_RENDERER_DEBUG fprintf(stderr, @@ -171,6 +194,7 @@ int FontRendererBakeFontBitmap(FontRenderer *font_renderer, int font_height, return res; } +// FIXME: remove the Blender template argument. template void blend_solid_hspan(agg::rendering_buffer& rbuf, Blender& blender, int x, int y, unsigned len, @@ -205,6 +229,52 @@ void blend_solid_hspan(agg::rendering_buffer& rbuf, Blender& blender, } } +static int floor_div(int a, int b) { + int rem = a % b; + if (rem < 0) { + rem += b; + } + return (a - rem) / b; +} + +void blend_solid_hspan_rgb_subpixel(agg::rendering_buffer& rbuf, agg::gamma_lut<>& gamma, agg::lcd_distribution_lut& lcd_lut, + int x_lcd, int y, unsigned len, + const agg::rgba8& c, + const agg::int8u* covers) +{ + // const int subpixel_scale = 3; + // FIXME: rowlen à verifier + // unsigned rowlen = rbuf.width() * subpixel_scale; + // FIXME: no correct boundary limits for cx and cx_max + // int cx = (x_lcd - 2 >= 0 ? -2 : -x_lcd); + // int cx_max = (len + 2 <= rowlen ? len + 1 : rowlen - 1); + const int pixel_size = 4; + int cx = -2; + int cx_max = len + 1; + + const int x_min = floor_div(x_lcd + cx, 3); + const int x_max = floor_div(x_lcd + cx_max, 3); + + const agg::int8u rgb[3] = { c.r, c.g, c.b }; + agg::int8u* p = rbuf.row_ptr(y) + x_min * pixel_size; + + // Indexes to adress RGB colors in a BGRA32 format. + const int pixel_index[3] = {2, 1, 0}; + for (int x = x_min; x <= x_max; x++) + { + for (int i = 0; i < 3; i++) { + int new_cx = x * 3 - x_lcd + i; + unsigned c_conv = lcd_lut.convolution(covers, new_cx, 0, len - 1); + unsigned alpha = (c_conv + 1) * (c.a + 1); + unsigned dst_col = gamma.dir(rgb[i]); + unsigned src_col = gamma.dir(*(p + pixel_index[i])); + *(p + pixel_index[i]) = gamma.inv((((dst_col - src_col) * alpha) + (src_col << 16)) >> 16); + } + //p[3] = 0xff; + p += 4; + } +} + // destination implicitly BGRA32. Source implictly single-byte renderer_alpha coverage. void FontRendererBlendGamma(FontRenderer *font_renderer, uint8_t *dst, int dst_stride, uint8_t *src, int src_stride, int region_width, int region_height, FontRendererColor color) { blender_gamma_type& blender = font_renderer->blender(); @@ -215,3 +285,16 @@ void FontRendererBlendGamma(FontRenderer *font_renderer, uint8_t *dst, int dst_s blend_solid_hspan(dst_ren_buf, blender, x, y, region_width, color_a, covers); } } + +// destination implicitly BGRA32. Source implictly single-byte renderer_alpha coverage. +void FontRendererBlendGammaSubpixel(FontRenderer *font_renderer, uint8_t *dst, int dst_stride, uint8_t *src, int src_stride, int region_width, int region_height, FontRendererColor color) { + const int subpixel_scale = 3; + agg::gamma_lut<>& gamma = font_renderer->gamma(); + agg::lcd_distribution_lut& lcd_lut = font_renderer->lcd_distribution_lut(); + agg::rendering_buffer dst_ren_buf(dst, region_width, region_height, dst_stride); + const agg::rgba8 color_a(color.r, color.g, color.b); + for (int x = 0, y = 0; y < region_height; y++) { + agg::int8u *covers = src + y * src_stride; + blend_solid_hspan_rgb_subpixel(dst_ren_buf, gamma, lcd_lut, x, y, region_width * subpixel_scale, color_a, covers); + } +} diff --git a/src/font_renderer.h b/src/font_renderer.h index 78c9f0a0..e4a0166c 100644 --- a/src/font_renderer.h +++ b/src/font_renderer.h @@ -35,7 +35,8 @@ int FontRendererGetFontHeight(FontRenderer *, float size); int FontRendererBakeFontBitmap(FontRenderer *, int font_height, void *pixels, int pixels_width, int pixels_height, - int first_char, int num_chars, GlyphBitmapInfo *glyph_info); + int first_char, int num_chars, GlyphBitmapInfo *glyph_info, + int subpixel_scale); void FontRendererBlendGamma(FontRenderer *, uint8_t *dst, int dst_stride, @@ -43,6 +44,12 @@ void FontRendererBlendGamma(FontRenderer *, int region_width, int region_height, FontRendererColor color); +void FontRendererBlendGammaSubpixel(FontRenderer *, + uint8_t *dst, int dst_stride, + uint8_t *src, int src_stride, + int region_width, int region_height, + FontRendererColor color); + #ifdef __cplusplus } #endif diff --git a/src/font_renderer_alpha.h b/src/font_renderer_alpha.h index a5a74091..2fc6c6ab 100644 --- a/src/font_renderer_alpha.h +++ b/src/font_renderer_alpha.h @@ -78,74 +78,15 @@ public: return m_font_loaded; } - template - void draw_text(Rasterizer& ras, Scanline& sl, - RenSolid& ren_solid, const color_type color, - const char* text, - double& x, double& y, double height) - { - const double scale_x = 100; - - m_feng.height(height); - m_feng.width(height * scale_x); - m_feng.hinting(m_hinting); - - const char* p = text; - - // Represent the delta in x scaled by scale_x. - double x_delta = 0; - double start_x = x; - - while(*p) - { - if(*p == '\n') - { - x_delta = 0; - y -= height * 1.25; - ++p; - continue; - } - - const agg::glyph_cache* glyph = m_fman.glyph(*p); - if(glyph) - { - if(m_kerning) - { - m_fman.add_kerning(&x_delta, &y); - } - - m_fman.init_embedded_adaptors(glyph, 0, 0); - if(glyph->data_type == agg::glyph_data_outline) - { - double ty = m_hinting ? floor(y + 0.5) : y; - ras.reset(); - m_mtx.reset(); - m_mtx *= agg::trans_affine_scaling(1.0 / scale_x, 1); - m_mtx *= agg::trans_affine_translation(start_x + x_delta / scale_x, ty); - ras.add_path(m_trans); - ren_solid.color(color); - agg::render_scanlines(ras, sl, ren_solid); - } - - // increment pen position - x_delta += glyph->advance_x; - y += glyph->advance_y; - } - ++p; - } - // Update x value befor returning. - x += x_delta / scale_x; - } - template void draw_codepoint(Rasterizer& ras, Scanline& sl, RenSolid& ren_solid, const color_type color, - int codepoint, double& x, double& y, double height) + int codepoint, double& x, double& y, double height, int subpixel_scale) { const double scale_x = 100; m_feng.height(height); - m_feng.width(height * scale_x); + m_feng.width(height * scale_x * subpixel_scale); m_feng.hinting(m_hinting); // Represent the delta in x scaled by scale_x. @@ -184,30 +125,11 @@ public: ren_base.clear(color); } - void render_text(agg::rendering_buffer& ren_buf, - const double text_size, - const color_type text_color, - double& x, double& y, - const char *text) - { - if (!m_font_loaded) { - return; - } - agg::scanline_u8 sl; - agg::rasterizer_scanline_aa<> ras; - ras.clip_box(0, 0, ren_buf.width(), ren_buf.height()); - - agg::pixfmt_alpha8 pf(ren_buf); - base_ren_type ren_base(pf); - renderer_solid ren_solid(ren_base); - draw_text(ras, sl, ren_solid, text_color, text, x, y, text_size); - } - void render_codepoint(agg::rendering_buffer& ren_buf, const double text_size, const color_type text_color, double& x, double& y, - int codepoint) + int codepoint, int subpixel_scale) { if (!m_font_loaded) { return; @@ -219,6 +141,6 @@ public: agg::pixfmt_alpha8 pf(ren_buf); base_ren_type ren_base(pf); renderer_solid ren_solid(ren_base); - draw_codepoint(ras, sl, ren_solid, text_color, codepoint, x, y, text_size); + draw_codepoint(ras, sl, ren_solid, text_color, codepoint, x, y, text_size, subpixel_scale); } }; diff --git a/src/renderer.c b/src/renderer.c index d560e1b2..2a1fcca6 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -105,9 +105,9 @@ RenImage* ren_new_image(int width, int height) { return image; } -RenCoverageImage* ren_new_coverage(int width, int height) { +RenCoverageImage* ren_new_coverage(int width, int height, int subpixel_scale) { assert(width > 0 && height > 0); - RenCoverageImage *image = malloc(sizeof(RenCoverageImage) + width * height * sizeof(uint8_t)); + RenCoverageImage *image = malloc(sizeof(RenCoverageImage) + width * height * subpixel_scale * sizeof(uint8_t)); check_alloc(image); image->pixels = (void*) (image + 1); image->width = width; @@ -126,15 +126,17 @@ void ren_free_coverage(RenCoverageImage *coverage) { static GlyphSet* load_glyphset(RenFont *font, int idx) { GlyphSet *set = check_alloc(calloc(1, sizeof(GlyphSet))); + const int subpixel_scale = 3; + /* init image */ int width = 128; int height = 128; retry: - set->coverage = ren_new_coverage(width, height); + set->coverage = ren_new_coverage(width, height, subpixel_scale); int res = FontRendererBakeFontBitmap(font->renderer, font->height, (void *) set->coverage->pixels, width, height, - idx << 8, 256, set->glyphs); + idx << 8, 256, set->glyphs, subpixel_scale); /* retry with a larger image buffer if the buffer wasn't large enough */ if (res < 0) { @@ -169,7 +171,7 @@ RenFont* ren_load_font(const char *filename, float size) { font = check_alloc(calloc(1, sizeof(RenFont))); font->size = size; - const float gamma = 1.8; + const float gamma = 1.5; font->renderer = FontRendererNew(FONT_RENDERER_HINTING, gamma); if (FontRendererLoadFont(font->renderer, filename)) { free(font); @@ -321,17 +323,26 @@ static void ren_draw_coverage_with_color(FontRenderer *renderer, RenCoverageImag return; } + const int subpixel_scale = 3; +#if 0 + // FIXME: find a more robust solution. + const int sub_width_rem = sub->width % subpixel_scale; + if (sub_width_rem > 0) { + sub->width += (subpixel_scale - sub_width_rem); + } +#endif + /* draw */ SDL_Surface *surf = SDL_GetWindowSurface(window); uint8_t *s = image->pixels; RenColor *d = (RenColor*) surf->pixels; - s += sub->x + sub->y * image->width; + s += (sub->x + sub->y * image->width) * subpixel_scale; d += x + y * surf->w; const int surf_pixel_size = 4; - FontRendererBlendGamma( + FontRendererBlendGammaSubpixel( renderer, (uint8_t *) d, surf->w * surf_pixel_size, - s, image->width, + s, image->width * subpixel_scale, sub->width, sub->height, (FontRendererColor) { .r = color.r, .g = color.g, .b = color.b }); } diff --git a/tests/agg_font_render_test.c b/tests/agg_font_render_test.c index 2ae900b9..99f64c4f 100644 --- a/tests/agg_font_render_test.c +++ b/tests/agg_font_render_test.c @@ -81,9 +81,10 @@ static GlyphSet* load_glyphset_agg(RenFont *font, int idx) { retry: set->image = ren_new_image(width, height); + const int subpixel_scale = 3; int res = FontRendererBakeFontBitmap(font->renderer, font->height, (void *) set->image->pixels, width, height, - idx << 8, 256, set->glyphs); + idx << 8, 256, set->glyphs, subpixel_scale); /* retry with a larger image buffer if the buffer wasn't large enough */ if (res < 0) {