| /* |
| * Copyright © 2026 Behdad Esfahbod |
| * |
| * This is part of HarfBuzz, a text shaping library. |
| * |
| * Permission is hereby granted, without written agreement and without |
| * license or royalty fees, to use, copy, modify, and distribute this |
| * software and its documentation for any purpose, provided that the |
| * above copyright notice and the following two paragraphs appear in |
| * all copies of this software. |
| * |
| * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR |
| * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES |
| * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN |
| * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH |
| * DAMAGE. |
| * |
| * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, |
| * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND |
| * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS |
| * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO |
| * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. |
| * |
| * Author(s): Behdad Esfahbod |
| */ |
| |
| /* hb-ft-raster: like hb-raster but uses FreeType's rasterizer for |
| pixel-level comparison. Same pipeline (shaping, glyph positioning), |
| but calls FT_Outline_Render instead of hb_raster_draw_render. */ |
| |
| #include "batch.hh" |
| #include "font-options.hh" |
| #include "main-font-text.hh" |
| #include "output-options.hh" |
| #include "shape-consumer.hh" |
| #include "text-options.hh" |
| |
| #include <hb.h> |
| #include <hb-ft.h> |
| |
| #include <ft2build.h> |
| #include FT_FREETYPE_H |
| #include FT_OUTLINE_H |
| #include FT_BITMAP_H |
| #include FT_BBOX_H |
| #include FT_MULTIPLE_MASTERS_H |
| |
| #include <math.h> |
| #include <vector> |
| |
| |
| const unsigned DEFAULT_FONT_SIZE = FONT_SIZE_UPEM; |
| const unsigned SUBPIXEL_BITS = 6; |
| |
| struct ft_raster_output_t : output_options_t<true> |
| { |
| static const bool repeat_shape = false; |
| |
| ~ft_raster_output_t () |
| { |
| hb_font_destroy (font); |
| if (ft_face) FT_Done_Face (ft_face); |
| if (ft_lib) FT_Done_FreeType (ft_lib); |
| } |
| |
| void add_options (option_parser_t *parser) |
| { |
| parser->set_summary ("Rasterize text with FreeType rasterizer."); |
| parser->set_description ("Renders shaped text as a PPM raster image using FreeType."); |
| output_options_t::add_options (parser); |
| } |
| |
| template <typename app_t> |
| void init (hb_buffer_t *buffer HB_UNUSED, const app_t *font_opts) |
| { |
| lines.clear (); |
| direction = HB_DIRECTION_INVALID; |
| |
| font = hb_font_reference (font_opts->font); |
| subpixel_bits = font_opts->subpixel_bits; |
| |
| /* Set up FreeType */ |
| FT_Init_FreeType (&ft_lib); |
| |
| /* Get the font file blob from hb_face */ |
| hb_face_t *face = hb_font_get_face (font); |
| hb_blob_t *blob = hb_face_reference_blob (face); |
| unsigned blob_len; |
| const char *blob_data = hb_blob_get_data (blob, &blob_len); |
| |
| FT_New_Memory_Face (ft_lib, (const FT_Byte *) blob_data, blob_len, |
| hb_face_get_index (face), &ft_face); |
| hb_blob_destroy (blob); |
| |
| int x_scale = 0, y_scale = 0; |
| hb_font_get_scale (font, &x_scale, &y_scale); |
| unsigned ft_x_scale = x_scale ? (unsigned) fabsf ((float) x_scale) : 1; |
| unsigned ft_y_scale = y_scale ? (unsigned) fabsf ((float) y_scale) : 1; |
| |
| /* Match FT outlines to the shaping font scale. */ |
| FT_Set_Char_Size (ft_face, |
| (FT_F26Dot6) ft_x_scale * 64, |
| (FT_F26Dot6) ft_y_scale * 64, |
| 72, 72); |
| |
| #ifndef HB_NO_VAR |
| unsigned coords_len; |
| const float *coords = hb_font_get_var_coords_design (font, &coords_len); |
| if (coords_len) |
| { |
| FT_Fixed *ft_coords = (FT_Fixed *) malloc (coords_len * sizeof (FT_Fixed)); |
| for (unsigned i = 0; i < coords_len; i++) |
| ft_coords[i] = (FT_Fixed) (coords[i] * 65536.f); |
| FT_Set_Var_Design_Coordinates (ft_face, coords_len, ft_coords); |
| free (ft_coords); |
| } |
| #endif |
| } |
| |
| void new_line () { lines.emplace_back (); } |
| |
| void consume_text (hb_buffer_t * HB_UNUSED, |
| const char * HB_UNUSED, |
| unsigned int HB_UNUSED, |
| hb_bool_t HB_UNUSED) {} |
| |
| void error (const char *message) |
| { g_printerr ("%s: %s\n", g_get_prgname (), message); } |
| |
| void consume_glyphs (hb_buffer_t *buffer, |
| const char * HB_UNUSED, |
| unsigned int HB_UNUSED, |
| hb_bool_t HB_UNUSED) |
| { |
| if (lines.empty ()) lines.emplace_back (); |
| line_t &line = lines.back (); |
| |
| hb_direction_t line_dir = hb_buffer_get_direction (buffer); |
| if (line_dir == HB_DIRECTION_INVALID) |
| line_dir = HB_DIRECTION_LTR; |
| if (direction == HB_DIRECTION_INVALID) |
| direction = line_dir; |
| |
| unsigned count; |
| const hb_glyph_info_t *infos = hb_buffer_get_glyph_infos (buffer, &count); |
| const hb_glyph_position_t *positions = hb_buffer_get_glyph_positions (buffer, &count); |
| |
| hb_position_t pen_x = 0, pen_y = 0; |
| line.glyphs.reserve (line.glyphs.size () + count); |
| for (unsigned i = 0; i < count; i++) |
| { |
| line.glyphs.push_back ({ |
| infos[i].codepoint, |
| (float) (pen_x + positions[i].x_offset), |
| (float) (pen_y + positions[i].y_offset), |
| }); |
| pen_x += positions[i].x_advance; |
| pen_y += positions[i].y_advance; |
| } |
| line.advance_x = (float) pen_x; |
| line.advance_y = (float) pen_y; |
| } |
| |
| template <typename app_t> |
| void finish (hb_buffer_t *buffer HB_UNUSED, const app_t *app) |
| { |
| unsigned int num_iterations = app->num_iterations ? app->num_iterations : 1; |
| |
| /* pixels per font unit */ |
| float sx = scalbnf (1.f, -(int) subpixel_bits); |
| float sy = scalbnf (1.f, -(int) subpixel_bits); |
| |
| /* line step in font units */ |
| hb_direction_t dir = direction; |
| if (dir == HB_DIRECTION_INVALID) dir = HB_DIRECTION_LTR; |
| bool vertical = HB_DIRECTION_IS_VERTICAL (dir); |
| |
| hb_font_extents_t extents = {}; |
| hb_font_get_extents_for_direction (font, dir, &extents); |
| float step = fabsf ((float) (extents.ascender - extents.descender + extents.line_gap)); |
| if (!(step > 0.f)) |
| { |
| int x_scale = 0, y_scale = 0; |
| hb_font_get_scale (font, &x_scale, &y_scale); |
| int axis_scale = vertical ? x_scale : y_scale; |
| step = axis_scale ? fabsf ((float) axis_scale) : 1.f; |
| } |
| |
| /* First pass: compute bounding box of all glyphs in pixel space */ |
| float img_xmin = 1e30f, img_ymin = 1e30f; |
| float img_xmax = -1e30f, img_ymax = -1e30f; |
| |
| struct glyph_render_t { |
| hb_codepoint_t gid; |
| float pen_x, pen_y; |
| }; |
| std::vector<glyph_render_t> renders; |
| |
| for (unsigned li = 0; li < lines.size (); li++) |
| { |
| float off_x = vertical ? -(step * (float) li) : 0.f; |
| float off_y = vertical ? 0.f : -(step * (float) li); |
| |
| for (const auto &g : lines[li].glyphs) |
| { |
| float pen_x = (g.x + off_x) * sx; |
| float pen_y = (g.y + off_y) * sy; |
| |
| FT_Load_Glyph (ft_face, g.gid, FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP); |
| FT_Outline *outline = &ft_face->glyph->outline; |
| |
| FT_BBox bbox; |
| FT_Outline_Get_BBox (outline, &bbox); |
| |
| /* bbox is in 26.6 font-space; transform to pixel space */ |
| float gx0 = pen_x + (float) bbox.xMin * sx / 64.f; |
| float gy0 = pen_y + (float) bbox.yMin * sy / 64.f; |
| float gx1 = pen_x + (float) bbox.xMax * sx / 64.f; |
| float gy1 = pen_y + (float) bbox.yMax * sy / 64.f; |
| |
| if (gx0 < img_xmin) img_xmin = gx0; |
| if (gy0 < img_ymin) img_ymin = gy0; |
| if (gx1 > img_xmax) img_xmax = gx1; |
| if (gy1 > img_ymax) img_ymax = gy1; |
| |
| renders.push_back ({g.gid, pen_x, pen_y}); |
| } |
| } |
| |
| if (renders.empty () || img_xmin >= img_xmax || img_ymin >= img_ymax) |
| { |
| cleanup (); |
| return; |
| } |
| |
| int x0 = (int) floorf (img_xmin); |
| int y0 = (int) floorf (img_ymin); |
| int x1 = (int) ceilf (img_xmax); |
| int y1 = (int) ceilf (img_ymax); |
| unsigned width = (unsigned) (x1 - x0); |
| unsigned height = (unsigned) (y1 - y0); |
| unsigned stride = (width + 3u) & ~3u; |
| |
| std::vector<uint8_t> pixels (stride * height, 0); |
| for (unsigned int iter = 0; iter < num_iterations; iter++) |
| { |
| std::fill (pixels.begin (), pixels.end (), 0); |
| |
| /* Second pass: rasterize each glyph with FreeType */ |
| for (const auto &r : renders) |
| { |
| FT_Load_Glyph (ft_face, r.gid, FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP); |
| FT_Outline *outline = &ft_face->glyph->outline; |
| |
| /* We need to transform the outline from font-space 26.6 to pixel space, |
| then translate so that pixel (x0, y0) maps to bitmap (0, 0). */ |
| FT_Matrix matrix; |
| matrix.xx = (FT_Fixed) (sx * 65536.f); |
| matrix.xy = 0; |
| matrix.yx = 0; |
| matrix.yy = (FT_Fixed) (sy * 65536.f); |
| |
| /* pen position in 26.6 pixel space */ |
| FT_Vector delta; |
| delta.x = (FT_Pos) roundf ((r.pen_x - x0) * 64.f); |
| delta.y = (FT_Pos) roundf ((r.pen_y - y0) * 64.f); |
| |
| FT_Outline_Transform (outline, &matrix); |
| FT_Outline_Translate (outline, delta.x, delta.y); |
| |
| FT_Bitmap bitmap; |
| FT_Bitmap_Init (&bitmap); |
| bitmap.rows = height; |
| bitmap.width = width; |
| bitmap.pitch = (int) stride; |
| bitmap.buffer = pixels.data (); |
| bitmap.pixel_mode = FT_PIXEL_MODE_GRAY; |
| bitmap.num_grays = 256; |
| |
| FT_Raster_Params params = {}; |
| params.target = &bitmap; |
| params.flags = FT_RASTER_FLAG_AA; |
| FT_Outline_Render (ft_lib, outline, ¶ms); |
| } |
| |
| if (iter + 1 == num_iterations) |
| { |
| /* Write PPM (black on white), y-flipped so text reads correctly. */ |
| fprintf (out_fp, "P6\n%u %u\n255\n", width, height); |
| std::vector<uint8_t> rgb_row (width * 3); |
| for (unsigned row = 0; row < height; row++) |
| { |
| const uint8_t *src = pixels.data () + row * stride; |
| for (unsigned x = 0; x < width; x++) |
| rgb_row[x * 3 + 0] = rgb_row[x * 3 + 1] = rgb_row[x * 3 + 2] = (uint8_t) (255 - src[x]); |
| fwrite (rgb_row.data (), 1, width * 3, out_fp); |
| } |
| } |
| } |
| |
| cleanup (); |
| } |
| |
| private: |
| |
| void cleanup () |
| { |
| if (ft_face) { FT_Done_Face (ft_face); ft_face = nullptr; } |
| if (ft_lib) { FT_Done_FreeType (ft_lib); ft_lib = nullptr; } |
| hb_font_destroy (font); font = nullptr; |
| lines.clear (); |
| direction = HB_DIRECTION_INVALID; |
| } |
| |
| struct glyph_instance_t { hb_codepoint_t gid; float x, y; }; |
| struct line_t |
| { |
| float advance_x = 0.f, advance_y = 0.f; |
| std::vector<glyph_instance_t> glyphs; |
| }; |
| |
| hb_font_t *font = nullptr; |
| unsigned subpixel_bits = 0; |
| hb_direction_t direction = HB_DIRECTION_INVALID; |
| std::vector<line_t> lines; |
| FT_Library ft_lib = nullptr; |
| FT_Face ft_face = nullptr; |
| }; |
| |
| |
| int |
| main (int argc, char **argv) |
| { |
| using main_t = main_font_text_t<shape_consumer_t<ft_raster_output_t>, font_options_t, shape_text_options_t>; |
| return batch_main<main_t, true> (argc, argv); |
| } |