diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ce9a44..b958496 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,13 +21,13 @@ jobs: blocks: -DUNICODE_BLOCKS=/usr/share/unicode/Blocks.txt install_packages: > sudo apt update && - sudo apt install cmake gettext libcairo2-dev libglib2.0-dev libfreetype6-dev libpango1.0-dev ninja-build pkg-config unicode-data + sudo apt install cmake gettext libcairo2-dev libcairomm-1.0-dev libglib2.0-dev libfmt-dev libfreetype6-dev libpango1.0-dev libpangomm-1.4-dev ninja-build pkg-config unicode-data - os: macos-latest env: - CMAKE_PREFIX_PATH: /usr/local/opt/gettext install_packages: > brew update && - brew install cairo cmake fontconfig freetype gettext glib pango ninja pkg-config + brew install cairo cairomm cmake fmt fontconfig freetype gettext glib pango pangomm ninja pkg-config runs-on: ${{matrix.os}} diff --git a/CMakeLists.txt b/CMakeLists.txt index cef7ee5..8daae11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,10 +4,10 @@ project(fntsample VERSION 5.4 DESCRIPTION "PDF and PostScript font samples generator" HOMEPAGE_URL "https://github.com/eugmes/fntsample" - LANGUAGES C) + LANGUAGES CXX) -set(CMAKE_C_STANDARD 99 CACHE STRING "The C standard to use") -set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_CXX_STANDARD 17 CACHE STRING "The C++ standard to use") +set(CMAKE_CXX_STANDARD_REQUIRED ON) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake") @@ -16,6 +16,7 @@ include(CPack) find_package(PkgConfig REQUIRED) find_package(Intl REQUIRED) +find_package(fmt REQUIRED) # The target was added in CMake 3.20. if(NOT TARGET Intl::Intl) @@ -33,6 +34,10 @@ pkg_check_modules(pkgs REQUIRED IMPORTED_TARGET pangoft2>=1.37.0 ) +# FIXME: this is ugly +pkg_search_module(cairomm REQUIRED IMPORTED_TARGET cairomm cairomm-1.16 cairomm-1.0) +pkg_search_module(pangomm REQUIRED IMPORTED_TARGET pangomm pangomm-2.48 pangomm-1.4) + include(DownloadUnicodeBlocks) download_unicode_blocks() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 921db45..74d32a5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,46 +3,45 @@ configure_file(fntsample.1.in fntsample.1 @ONLY) set( C_WARNING_FLAGS - -Wcast-align -Werror-implicit-function-declaration -Wchar-subscripts + -Wcast-align -Wchar-subscripts -Wall -W -Wpointer-arith -Wwrite-strings -Wformat-security -Wmissing-format-attribute -fno-common -Wundef CACHE STRING "Warning flags for C compiler" ) add_executable(gen-unicode-blocks EXCLUDE_FROM_ALL - gen_unicode_blocks.c - read_blocks.c + gen_unicode_blocks.cpp + unicode_blocks.cpp + loadable_unicode_blocks.cpp ) +target_link_libraries(gen-unicode-blocks PRIVATE fmt::fmt) + target_compile_options(gen-unicode-blocks PRIVATE ${C_WARNING_FLAGS}) add_custom_command( - OUTPUT static_unicode_blocks.c - COMMAND gen-unicode-blocks "${UNICODE_BLOCKS}" static_unicode_blocks.c + OUTPUT static_unicode_blocks.cpp + COMMAND gen-unicode-blocks "${UNICODE_BLOCKS}" static_unicode_blocks.cpp MAIN_DEPENDENCY "${UNICODE_BLOCKS}" DEPENDS gen-unicode-blocks VERBATIM ) add_executable(fntsample - fntsample.c - read_blocks.c - ${CMAKE_CURRENT_BINARY_DIR}/static_unicode_blocks.c + fntsample.cpp + unicode_blocks.cpp + loadable_unicode_blocks.cpp + ${CMAKE_CURRENT_BINARY_DIR}/static_unicode_blocks.cpp ) -add_translatable_sources(fntsample.c read_blocks.c) +add_translatable_sources(fntsample.cpp unicode_blocks.cpp loadable_unicode_blocks.cpp) target_include_directories(fntsample PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ) -target_link_libraries(fntsample PRIVATE Intl::Intl PkgConfig::pkgs) - -find_library(MATH_LIBRARY m) -if(MATH_LIBRARY) - target_link_libraries(fntsample PRIVATE ${MATH_LIBRARY}) -endif() +target_link_libraries(fntsample PRIVATE Intl::Intl PkgConfig::pkgs fmt::fmt PkgConfig::cairomm PkgConfig::pangomm) target_compile_options(fntsample PRIVATE ${C_WARNING_FLAGS}) diff --git a/src/fntsample.c b/src/fntsample.c deleted file mode 100644 index c76c7bd..0000000 --- a/src/fntsample.c +++ /dev/null @@ -1,971 +0,0 @@ -/* Copyright © Євгеній Мещеряков - * SPDX-License-Identifier: GPL-3.0-or-later - */ -#include -// TODO: freetype 2.10.3, do not include ft2build.h anymore -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "unicode_blocks.h" -#include "static_unicode_blocks.h" -#include "config.h" - -#define _(str) gettext(str) - -#define POINTS_PER_INCH 72 - -#define A4_WIDTH (8.3 * POINTS_PER_INCH) -#define A4_HEIGHT (11.7 * POINTS_PER_INCH) - -#define xmin_border (POINTS_PER_INCH / 1.5) -#define ymin_border POINTS_PER_INCH -#define cell_width ((A4_WIDTH - 2 * xmin_border) / 16) -#define cell_height ((A4_HEIGHT - 2 * ymin_border) / 16) - -static double cell_x(double x_min, int pos) { return x_min + cell_width * (pos / 16); } - -static double cell_y(int pos) { return ymin_border + cell_height * (pos % 16); } - -static struct option longopts[] = { - {"blocks-file", 1, 0, 'b'}, - {"font-file", 1, 0, 'f'}, - {"output-file", 1, 0, 'o'}, - {"help", 0, 0, 'h'}, - {"other-font-file", 1, 0, 'd'}, - {"postscript-output", 0, 0, 's'}, - {"svg", 0, 0, 'g'}, - {"print-outline", 0, 0, 'l'}, - {"write-outline", 0, 0, 'w'}, - {"include-range", 1, 0, 'i'}, - {"exclude-range", 1, 0, 'x'}, - {"style", 1, 0, 't'}, - {"font-index", 1, 0, 'n'}, - {"other-index", 1, 0, 'm'}, - {"no-embed", 0, 0, 'e'}, - {"use-pango", 0, 0, 'p'}, /* For compatibility with version <= 5.3 */ - {0, 0, 0, 0}, -}; - -struct range { - uint32_t first; - uint32_t last; - bool include; - struct range *next; -}; - -static const char *font_file_name; -static const char *other_font_file_name; -static const char *output_file_name; -static bool postscript_output; -static bool svg_output; -static bool print_outline; -static bool write_outline; -static bool no_embed; -static struct range *ranges; -static struct range *last_range; -static int font_index; -static int other_index; - -struct fntsample_style { - const char *const name; - const char *const default_val; - char *val; -}; - -static struct fntsample_style styles[] = { - {"header-font", "Sans Bold 12", NULL}, - {"font-name-font", "Serif Bold 12", NULL}, - {"table-numbers-font", "Sans 10", NULL}, - {"cell-numbers-font", "Mono 8", NULL}, - {NULL, NULL, NULL}, -}; - -struct table_fonts { - PangoFontDescription *header; - PangoFontDescription *font_name; - PangoFontDescription *table_numbers; - PangoFontDescription *cell_numbers; -}; - -static struct table_fonts table_fonts; - -static double cell_label_offset; -static double cell_glyph_bot_offset; -static double glyph_baseline_offset; -static double font_scale; - -static const struct unicode_block *unicode_blocks; - -static void usage(const char *); - -static struct fntsample_style *find_style(const char *name) -{ - for (struct fntsample_style *style = styles; style->name; style++) { - if (!strcmp(name, style->name)) { - return style; - } - } - - return NULL; -} - -static int set_style(const char *name, const char *val) -{ - struct fntsample_style *style = find_style(name); - - if (!style) { - return -1; - } - - char *new_val = strdup(val); - if (!new_val) { - return -1; - } - - if (style->val) { - free(style->val); - } - - style->val = new_val; - - return 0; -} - -static const char *get_style(const char *name) -{ - struct fntsample_style *style = find_style(name); - - if (!style) { - return NULL; - } - - return style->val ? style->val : style->default_val; -} - -static int parse_style_string(char *s) -{ - char *n = strchr(s, ':'); - if (!n) { - return -1; - } - - *n++ = '\0'; - return set_style(s, n); -} - -/* - * Update output range. - * - * Returns -1 on error. - */ -static int add_range(char *range, bool include) -{ - uint32_t first = 0, last = 0xffffffff; - char *endptr; - - char *minus = strchr(range, '-'); - - if (minus) { - if (minus != range) { - *minus = '\0'; - first = strtoul(range, &endptr, 0); - if (*endptr) { - return -1; - } - } - - if (*(minus + 1)) { - last = strtoul(minus + 1, &endptr, 0); - if (*endptr) { - return -1; - } - } else if (minus == range) { - return -1; - } - } else { - first = strtoul(range, &endptr, 0); - if (*endptr) - return -1; - last = first; - } - - if (first > last) { - return -1; - } - - struct range *r = malloc(sizeof(*r)); - if (!r) { - return -1; - } - - r->first = first; - r->last = last; - r->include = include; - r->next = NULL; - - if (ranges) { - last_range->next = r; - } else { - ranges = r; - } - - last_range = r; - - return 0; -} - -/* - * Check if character with the given code belongs - * to output range specified by the user. - */ -static bool in_range(uint32_t c) -{ - bool in = ranges ? (!ranges->include) : 1; - - for (struct range *r = ranges; r; r = r->next) { - if ((c >= r->first) && (c <= r->last)) { - in = r->include; - } - } - return in; -} - -/* - * Get glyph index for the next glyph from the given font face, that - * represents character from output range specified by the user. - * - * Returns character code, updates 'idx'. - * 'idx' can became 0 if there are no more glyphs. - */ -static FT_ULong get_next_char(FT_Face face, FT_ULong charcode, FT_UInt *idx) -{ - FT_ULong rval = charcode; - - do { - rval = FT_Get_Next_Char(face, rval, idx); - } while (*idx && !in_range(rval)); - - return rval; -} - -/* - * Locate first character from the given font face that belongs - * to the user-specified output range. - * - * Returns character code, updates 'idx' with glyph index. - * Glyph index can became 0 if there are no matching glyphs in the font. - */ -static FT_ULong get_first_char(FT_Face face, FT_UInt *idx) -{ - FT_ULong rval = FT_Get_First_Char(face, idx); - - if (*idx && !in_range(rval)) { - rval = get_next_char(face, rval, idx); - } - - return rval; -} - -/* - * Create Pango layout for the given text. - * Updates 'r' with text extents. - * Returned layout should be freed using g_object_unref(). - */ -static PangoLayout *layout_text(cairo_t *cr, PangoFontDescription *ftdesc, const char *text, - PangoRectangle *r) -{ - PangoLayout *layout = pango_cairo_create_layout(cr); - pango_layout_set_font_description(layout, ftdesc); - pango_layout_set_text(layout, text, -1); - pango_layout_get_extents(layout, r, NULL); - - return layout; -} - -static void parse_options(int argc, char *const argv[]) -{ - for (;;) { - int n; - int c = getopt_long(argc, argv, "b:f:o:hd:sglwi:x:t:n:m:ep", longopts, NULL); - - if (c == -1) { - break; - } - - switch (c) { - case 'b': - if (unicode_blocks) { - fprintf(stderr, _("Unicode blocks file should be given at most once!\n")); - exit(1); - } - - unicode_blocks = read_blocks(optarg, &n); - if (n == 0) { - fprintf(stderr, _("Failed to load any blocks from the blocks file!\n")); - exit(6); - } - break; - case 'f': - if (font_file_name) { - fprintf(stderr, _("Font file name should be given only once!\n")); - exit(1); - } - font_file_name = optarg; - break; - case 'o': - if (output_file_name) { - fprintf(stderr, _("Output file name should be given only once!\n")); - exit(1); - } - output_file_name = optarg; - break; - case 'h': - usage(argv[0]); - exit(0); - break; - case 'd': - if (other_font_file_name) { - fprintf(stderr, _("Font file name should be given only once!\n")); - exit(1); - } - other_font_file_name = optarg; - break; - case 's': - postscript_output = true; - break; - case 'g': - svg_output = true; - break; - case 'l': - print_outline = true; - break; - case 'w': - write_outline = true; - break; - case 'i': - case 'x': - if (add_range(optarg, c == 'i')) { - usage(argv[0]); - exit(1); - } - break; - case 't': - if (parse_style_string(optarg) == -1) { - usage(argv[0]); - exit(1); - } - break; - case 'n': - font_index = atoi(optarg); - break; - case 'm': - other_index = atoi(optarg); - break; - case 'e': - no_embed = true; - break; - case 'p': - /* Ignored for compatibility */ - break; - case '?': - default: - usage(argv[0]); - exit(1); - break; - } - } - - if (!font_file_name || !output_file_name) { - usage(argv[0]); - exit(1); - } - - if (font_index < 0 || other_index < 0) { - fprintf(stderr, _("Font index should be non-negative!\n")); - exit(1); - } - - if (postscript_output && svg_output) { - fprintf(stderr, _("-s and -g cannot be used together!\n")); - exit(1); - } - - if (!unicode_blocks) { - unicode_blocks = static_unicode_blocks; - } -} - -/* - * Locate unicode block that contains given character code. - * Returns this block or NULL if not found. - */ -static const struct unicode_block *get_unicode_block(unsigned long charcode) -{ - for (const struct unicode_block *block = unicode_blocks; block->name; block++) { - if ((charcode >= block->start) && (charcode <= block->end)) { - return block; - } - } - - return NULL; -} - -/* - * Check if the given character code belongs to the given Unicode block. - */ -static bool is_in_block(unsigned long charcode, const struct unicode_block *block) -{ - return ((charcode >= block->start) && (charcode <= block->end)); -} - -/* - * Format and print/write outline information, if requested by the user. - */ -static void outline(cairo_surface_t *surface, int level, int page, const char *text) -{ - if (print_outline) { - printf("%d %d %s\n", level, page, text); - } - - if (write_outline && cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_PDF) { - int len = snprintf(0, 0, "page=%d", page); - char *dest = malloc(len + 1); - sprintf(dest, "page=%d", page); - - /* FIXME passing level here is not correct. */ - cairo_pdf_surface_add_outline(surface, level, text, dest, CAIRO_PDF_OUTLINE_FLAG_OPEN); - free(dest); - } -} - -/* - * Draw header of a page. - * Header shows font name and current Unicode block. - */ -static void draw_header(cairo_t *cr, const char *face_name, const char *block_name) -{ - PangoRectangle r; - - PangoLayout *layout = layout_text(cr, table_fonts.font_name, face_name, &r); - cairo_move_to(cr, (A4_WIDTH - pango_units_to_double(r.width)) / 2.0, 30.0); - pango_cairo_show_layout_line(cr, pango_layout_get_line_readonly(layout, 0)); - g_object_unref(layout); - - layout = layout_text(cr, table_fonts.header, block_name, &r); - cairo_move_to(cr, (A4_WIDTH - pango_units_to_double(r.width)) / 2.0, 50.0); - pango_cairo_show_layout_line(cr, pango_layout_get_line_readonly(layout, 0)); - g_object_unref(layout); -} - -/* - * Highlight the cell with given coordinates. - * Used to highlight new glyphs. - */ -static void highlight_cell(cairo_t *cr, double x, double y) -{ - cairo_save(cr); - cairo_set_source_rgb(cr, 1.0, 1.0, 0.6); - cairo_rectangle(cr, x, y, cell_width, cell_height); - cairo_fill(cr); - cairo_restore(cr); -} - -/* - * Draw table grid with row and column numbers. - */ -static void draw_grid(cairo_t *cr, unsigned int x_cells, unsigned long block_start) -{ - const double x_min = (A4_WIDTH - x_cells * cell_width) / 2; - const double x_max = (A4_WIDTH + x_cells * cell_width) / 2; - const double table_height = A4_HEIGHT - ymin_border * 2; - - cairo_set_line_width(cr, 1.0); - cairo_rectangle(cr, x_min, ymin_border, x_max - x_min, table_height); - cairo_move_to(cr, x_min, ymin_border); - cairo_line_to(cr, x_min, ymin_border - 15.0); - cairo_move_to(cr, x_max, ymin_border); - cairo_line_to(cr, x_max, ymin_border - 15.0); - cairo_stroke(cr); - - cairo_set_line_width(cr, 0.5); - /* draw horizontal lines */ - for (int i = 1; i < 16; i++) { - // TODO: use better name instead of just POINTS_PER_INCH - cairo_move_to(cr, x_min, POINTS_PER_INCH + i * table_height / 16); - cairo_line_to(cr, x_max, POINTS_PER_INCH + i * table_height / 16); - } - - /* draw vertical lines */ - for (unsigned int i = 1; i < x_cells; i++) { - cairo_move_to(cr, x_min + i * cell_width, ymin_border); - cairo_line_to(cr, x_min + i * cell_width, A4_HEIGHT - ymin_border); - } - cairo_stroke(cr); - - /* draw glyph numbers */ - char buf[17]; - buf[1] = '\0'; -#define hexdigs "0123456789ABCDEF" - - for (int i = 0; i < 16; i++) { - buf[0] = hexdigs[i]; - - PangoRectangle r; - PangoLayout *layout = layout_text(cr, table_fonts.table_numbers, buf, &r); - cairo_move_to(cr, x_min - pango_units_to_double(PANGO_RBEARING(r)) - 5.0, - POINTS_PER_INCH + (i + 0.5) * table_height / 16 - + pango_units_to_double(PANGO_DESCENT(r)) / 2); - pango_cairo_show_layout_line(cr, pango_layout_get_line_readonly(layout, 0)); - cairo_move_to(cr, x_min + x_cells * cell_width + 5.0, - POINTS_PER_INCH + (i + 0.5) * table_height / 16 - + pango_units_to_double(PANGO_DESCENT(r)) / 2); - pango_cairo_show_layout_line(cr, pango_layout_get_line_readonly(layout, 0)); - g_object_unref(layout); - } - - for (unsigned int i = 0; i < x_cells; i++) { - snprintf(buf, sizeof(buf), "%03lX", block_start / 16 + i); - - PangoRectangle r; - PangoLayout *layout = layout_text(cr, table_fonts.table_numbers, buf, &r); - cairo_move_to(cr, - x_min + i * cell_width + (cell_width - pango_units_to_double(r.width)) / 2, - ymin_border - 5.0); - pango_cairo_show_layout_line(cr, pango_layout_get_line_readonly(layout, 0)); - g_object_unref(layout); - } -} - -/* - * Fill empty cell. Color of the fill depends on the character properties. - */ -static void fill_empty_cell(cairo_t *cr, double x, double y, unsigned long charcode) -{ - cairo_save(cr); - if (g_unichar_isdefined(charcode)) { - if (g_unichar_iscntrl(charcode)) - cairo_set_source_rgb(cr, 0.0, 0.0, 0.5); - else - cairo_set_source_rgb(cr, 0.5, 0.5, 0.5); - } - cairo_rectangle(cr, x, y, cell_width, cell_height); - cairo_fill(cr); - cairo_restore(cr); -} - -/* - * Draw label with character code. - */ -static void draw_charcode(cairo_t *cr, double x, double y, FT_ULong charcode) -{ - char buf[9]; - snprintf(buf, sizeof(buf), "%04lX", charcode); - - PangoRectangle r; - PangoLayout *layout = layout_text(cr, table_fonts.cell_numbers, buf, &r); - cairo_move_to(cr, x + (cell_width - pango_units_to_double(r.width)) / 2.0, - y + cell_height - cell_label_offset); - pango_cairo_show_layout_line(cr, pango_layout_get_line_readonly(layout, 0)); - g_object_unref(layout); -} - -/* - * Draws tables for all characters in the given Unicode block. - * Use font described by face and ft_face. Start from character - * with given charcode (it should belong to the given Unicode - * block). After return 'charcode' equals the last character code - * of the block. - * - * Returns number of pages drawn. - */ -static int draw_unicode_block(cairo_t *cr, PangoLayout *layout, FT_Face ft_face, - const char *font_name, unsigned long charcode, - const struct unicode_block *block, FT_Face ft_other_face) -{ - int npages = 0; - FT_UInt idx = FT_Get_Char_Index(ft_face, charcode); - - do { - unsigned long offset = ((charcode - block->start) / 0x100) * 0x100; - unsigned long tbl_start = block->start + offset; - unsigned long tbl_end = tbl_start + 0xFF > block->end ? block->end + 1 : tbl_start + 0x100; - unsigned int rows = (tbl_end - tbl_start) / 16; - double x_min = (A4_WIDTH - rows * cell_width) / 2; - - bool filled_cells[256]; /* 16x16 glyphs max */ - unsigned long curr_charcode = tbl_start; - int pos = 0; - - cairo_save(cr); - draw_header(cr, font_name, block->name); - - memset(filled_cells, '\0', sizeof(filled_cells)); - - /* - * Fill empty cells and calculate coordinates of the glyphs. - * Also highlight cells if needed. - */ - do { - /* fill empty cells before the current glyph */ - for (; curr_charcode < charcode; curr_charcode++, pos++) { - fill_empty_cell(cr, cell_x(x_min, pos), cell_y(pos), curr_charcode); - } - - /* if it is new glyph - highlight the cell */ - if (ft_other_face && !FT_Get_Char_Index(ft_other_face, charcode)) { - highlight_cell(cr, cell_x(x_min, pos), cell_y(pos)); - } - - /* draw the character */ - char buf[9]; - gint len = g_unichar_to_utf8((gunichar)charcode, buf); - pango_layout_set_text(layout, buf, len); - - double baseline = pango_units_to_double(pango_layout_get_baseline(layout)); - cairo_move_to(cr, cell_x(x_min, pos), cell_y(pos) + glyph_baseline_offset - baseline); - - if (no_embed) { - pango_cairo_layout_path(cr, layout); - } else { - pango_cairo_show_layout(cr, layout); - } - - filled_cells[pos] = true; - curr_charcode++; - pos++; - - charcode = get_next_char(ft_face, charcode, &idx); - } while (idx && (charcode < tbl_end) && is_in_block(charcode, block)); - - /* Fill remaining empty cells */ - for (; curr_charcode < tbl_end; curr_charcode++, pos++) { - fill_empty_cell(cr, cell_x(x_min, pos), cell_y(pos), curr_charcode); - } - - /* - * Charcodes are drawn here to avoid switching between the charcode - * font and the cell font for each filled cell. - */ - for (unsigned long i = 0; i < tbl_end - tbl_start; i++) { - if (filled_cells[i]) { - draw_charcode(cr, cell_x(x_min, i), cell_y(i), i + tbl_start); - } - } - - draw_grid(cr, rows, tbl_start); - npages++; - cairo_show_page(cr); - cairo_restore(cr); - } while (idx && is_in_block(charcode, block)); - - return npages; -} - -static PangoLayout *create_glyph_layout(cairo_t *cr, FcConfig *fc_config, FcPattern *fc_font) -{ - PangoFontMap *fontmap = pango_cairo_font_map_new_for_font_type(CAIRO_FONT_TYPE_FT); - pango_fc_font_map_set_config(PANGO_FC_FONT_MAP(fontmap), fc_config); - PangoContext *context = pango_font_map_create_context(fontmap); - pango_cairo_update_context(cr, context); - - PangoFontDescription *font_desc = pango_fc_font_description_from_pattern(fc_font, FALSE); - PangoLayout *layout = pango_layout_new(context); - pango_layout_set_font_description(layout, font_desc); - pango_layout_set_width(layout, pango_units_from_double(cell_width)); - pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); - - g_object_unref(context); - g_object_unref(fontmap); - pango_font_description_free(font_desc); - - return layout; -} - -/* - * The main drawing function. - */ -static void draw_glyphs(cairo_t *cr, FT_Face ft_face, FT_Face ft_other_face) -{ - FcConfig *fc_config = FcConfigCreate(); - FcConfigAppFontAddFile(fc_config, (const FcChar8 *)font_file_name); - - FcPattern *fc_pat = FcPatternCreate(); - FcPatternAddInteger(fc_pat, FC_INDEX, font_index); - - FcFontSet *fc_fontset = FcFontList(fc_config, fc_pat, NULL); - assert(fc_fontset->nfont > 0); - FcPattern *fc_font = fc_fontset->fonts[0]; - - const char *font_name; - if (FcPatternGetString(fc_font, FC_FULLNAME, 0, (FcChar8 **)&font_name) != FcResultMatch) { - font_name = "Unknown"; - } - - cairo_surface_t *surface = cairo_get_target(cr); - - int pageno = 1; - outline(surface, 0, pageno, font_name); - - PangoLayout *layout = create_glyph_layout(cr, fc_config, fc_font); - - FT_UInt idx; - FT_ULong charcode = get_first_char(ft_face, &idx); - - while (idx) { - const struct unicode_block *block = get_unicode_block(charcode); - if (block) { - outline(surface, 1, pageno, block->name); - int npages = draw_unicode_block(cr, layout, ft_face, font_name, charcode, block, - ft_other_face); - pageno += npages; - charcode = block->end; - } - - charcode = get_next_char(ft_face, charcode, &idx); - } - - g_object_unref(layout); - - FcPatternDestroy(fc_pat); - FcFontSetDestroy(fc_fontset); - FcConfigDestroy(fc_config); -} - -/* - * Print usage instructions and default values for styles - */ -static void usage(const char *cmd) -{ - fprintf(stderr, - _("Usage: %s [ OPTIONS ] -f FONT-FILE -o OUTPUT-FILE\n" - " %s -h\n\n"), - cmd, cmd); - fprintf( - stderr, - _("Options:\n" - " --blocks-file, -b BLOCKS-FILE Read Unicode blocks information from " - "BLOCKS-FILE\n" - " --font-file, -f FONT-FILE Create samples of FONT-FILE\n" - " --font-index, -n IDX Font index in FONT-FILE\n" - " --output-file, -o OUTPUT-FILE Save samples to OUTPUT-FILE\n" - " --help, -h Show this information message and exit\n" - " --other-font-file, -d OTHER-FONT Compare FONT-FILE with OTHER-FONT and highlight " - "added glyphs\n" - " --other-index, -m IDX Font index in OTHER-FONT\n" - " --postscript-output, -s Use PostScript format for output instead of PDF\n" - " --svg, -g Use SVG format for output\n" - " --print-outline, -l Print document outlines data to standard output\n" - " --write-outline, -w Write document outlines (only in PDF output)\n" - " --no-embed, -e Don't embed the font in the output file, draw " - "the glyphs instead\n" - " --include-range, -i RANGE Show characters in RANGE\n" - " --exclude-range, -x RANGE Do not show characters in RANGE\n" - " --style, -t \"STYLE: VAL\" Set STYLE to value VAL\n")); - - fprintf(stderr, _("\nSupported styles (and default values):\n")); - - for (const struct fntsample_style *style = styles; style->name; style++) { - fprintf(stderr, "\t%s (%s)\n", style->name, style->default_val); - } -} - -/* - * Initialize fonts used to print table heders and character codes. - */ -static void init_table_fonts(void) -{ - /* FIXME is this correct? */ - PangoCairoFontMap *map = (PangoCairoFontMap *)pango_cairo_font_map_get_default(); - - pango_cairo_font_map_set_resolution(map, POINTS_PER_INCH); - - table_fonts.header = pango_font_description_from_string(get_style("header-font")); - table_fonts.font_name = pango_font_description_from_string(get_style("font-name-font")); - table_fonts.table_numbers = pango_font_description_from_string(get_style("table-numbers-font")); - table_fonts.cell_numbers = pango_font_description_from_string(get_style("cell-numbers-font")); -} - -/* - * Calculate various offsets. - */ -static void calculate_offsets(cairo_t *cr) -{ - PangoRectangle extents; - /* Assume that vertical extents does not depend on actual text */ - PangoLayout *l = layout_text(cr, table_fonts.cell_numbers, "0123456789ABCDEF", &extents); - g_object_unref(l); - /* Unsolved mistery of pango's font metrics.... */ - double digits_ascent = pango_units_to_double(PANGO_DESCENT(extents)); - double digits_descent = -pango_units_to_double(PANGO_ASCENT(extents)); - - cell_label_offset = digits_descent + 2; - cell_glyph_bot_offset = cell_label_offset + digits_ascent + 2; -} - -/* - * Calculate font scaling - */ -void calc_font_scaling(FT_Face ft_face) -{ - cairo_font_face_t *cr_face = cairo_ft_font_face_create_for_ft_face(ft_face, 0); - cairo_font_options_t *options = cairo_font_options_create(); - - /* First create font with size 1 and measure it */ - cairo_matrix_t font_matrix; - cairo_matrix_init_identity(&font_matrix); - cairo_matrix_t ctm; - cairo_matrix_init_identity(&ctm); - - /* Turn off rounding, so we can get real metrics */ - cairo_font_options_set_hint_metrics(options, CAIRO_HINT_METRICS_OFF); - cairo_scaled_font_t *cr_font = cairo_scaled_font_create(cr_face, &font_matrix, &ctm, options); - cairo_font_extents_t extents; - cairo_scaled_font_extents(cr_font, &extents); - - /* Use some magic to find the best font size... */ - double tgt_size = cell_height - cell_glyph_bot_offset - 2; - if (tgt_size <= 0) { - fprintf(stderr, _("Not enough space for rendering glyphs. Make cell font smaller.\n")); - exit(5); - } - - double act_size = extents.ascent + extents.descent; - if (act_size <= 0) { - fprintf(stderr, _("The font has strange metrics: ascent + descent = %g\n"), act_size); - exit(5); - } - - font_scale = tgt_size / act_size; - if (font_scale > 1) - font_scale = trunc(font_scale); // just to make numbers nicer - if (font_scale > 20) - font_scale = 20; // Do not make font larger than in previous versions - - cairo_scaled_font_destroy(cr_font); - - /* Create the font once again, but this time scaled */ - cairo_matrix_init_scale(&font_matrix, font_scale, font_scale); - cr_font = cairo_scaled_font_create(cr_face, &font_matrix, &ctm, options); - cairo_scaled_font_extents(cr_font, &extents); - glyph_baseline_offset - = (tgt_size - (extents.ascent + extents.descent)) / 2 + 2 + extents.ascent; - cairo_scaled_font_destroy(cr_font); -} - -/* - * Configure DPF surface metadata so fntsample can be used with - * repeatable builds. - */ -static void set_repeatable_pdf_metadata(cairo_surface_t *surface) -{ - char *source_date_epoch = getenv("SOURCE_DATE_EPOCH"); - - if (source_date_epoch) { - char *endptr; - time_t now = strtoul(source_date_epoch, &endptr, 10); - - if (*endptr != 0) { - fprintf(stderr, _("Failed to parse environment variable SOURCE_DATE_EPOCH.\n")); - exit(1); - } - struct tm *build_time = gmtime(&now); - char buffer[25]; - strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", build_time); - - cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_CREATE_DATE, buffer); - } -} - -int main(int argc, char **argv) -{ - setlocale(LC_ALL, ""); - bindtextdomain(CMAKE_PROJECT_NAME, CMAKE_INSTALL_FULL_LOCALEDIR); - textdomain(CMAKE_PROJECT_NAME); - - parse_options(argc, argv); - - FT_Library library; - FT_Error error = FT_Init_FreeType(&library); - - if (error) { - /* TRANSLATORS: 'freetype' is a name of a library, and should be left untranslated */ - fprintf(stderr, _("%s: freetype error\n"), argv[0]); - exit(3); - } - - FT_Face face; - error = FT_New_Face(library, font_file_name, font_index, &face); - - if (error) { - fprintf(stderr, _("%s: failed to open font file %s\n"), argv[0], font_file_name); - exit(4); - } - - FT_Face other_face = NULL; - - if (other_font_file_name) { - error = FT_New_Face(library, other_font_file_name, other_index, &other_face); - - if (error) { - fprintf(stderr, _("%s: failed to create new font face\n"), argv[0]); - exit(4); - } - } - - cairo_surface_t *surface; - - if (postscript_output) { - surface = cairo_ps_surface_create(output_file_name, A4_WIDTH, A4_HEIGHT); - } else if (svg_output) { - surface = cairo_svg_surface_create(output_file_name, A4_WIDTH, A4_HEIGHT); - } else { - surface = cairo_pdf_surface_create(output_file_name, A4_WIDTH, A4_HEIGHT); /* A4 paper */ - set_repeatable_pdf_metadata(surface); - } - - cairo_status_t cr_status = cairo_surface_status(surface); - if (cr_status != CAIRO_STATUS_SUCCESS) { - /* TRANSLATORS: 'cairo' is a name of a library, and should be left untranslated */ - fprintf(stderr, _("%s: failed to create cairo surface: %s\n"), argv[0], - cairo_status_to_string(cr_status)); - exit(1); - } - - cairo_t *cr = cairo_create(surface); - cr_status = cairo_status(cr); - if (cr_status != CAIRO_STATUS_SUCCESS) { - fprintf(stderr, _("%s: cairo_create failed: %s\n"), argv[0], - cairo_status_to_string(cr_status)); - exit(1); - } - - cairo_surface_destroy(surface); - - init_table_fonts(); - calculate_offsets(cr); - - cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); - calc_font_scaling(face); - draw_glyphs(cr, face, other_face); - cairo_destroy(cr); - - return 0; -} diff --git a/src/fntsample.cpp b/src/fntsample.cpp new file mode 100644 index 0000000..c750f00 --- /dev/null +++ b/src/fntsample.cpp @@ -0,0 +1,966 @@ +/* Copyright © Євгеній Мещеряков + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include +// TODO: freetype 2.10.3, do not include ft2build.h anymore +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "loadable_unicode_blocks.h" +#include "static_unicode_blocks.h" +#include "unicode_range_set.h" +#include "unicode_utils.h" + +#include "config.h" + +using namespace std; + +#define _(str) gettext(str) + +// This is also available in cairomm master +class SaveGuard final { +public: + explicit SaveGuard(Cairo::RefPtr &context) + : ctx(context) + { + ctx->save(); + } + ~SaveGuard() { ctx->restore(); } + +private: + Cairo::RefPtr ctx; +}; + +constexpr double POINTS_PER_INCH = 72; + +class page_metrics { +public: + explicit page_metrics(unsigned num_columns) noexcept + : num_columns(num_columns) + , table_width(num_columns * cell_width) + , x_min((page_width - table_width) / 2) + , x_max(page_width - x_min) + { + } + + static constexpr unsigned num_rows = 16; + static constexpr unsigned max_num_columns = 16; + const unsigned num_columns; + + // NOTE: A4 paper size + static constexpr double page_width = 8.3 * POINTS_PER_INCH; + static constexpr double page_height = 11.7 * POINTS_PER_INCH; + + static constexpr double min_horiz_border = POINTS_PER_INCH / 1.5; + static constexpr double vert_border = POINTS_PER_INCH; + static constexpr double cell_width = (page_width - 2 * min_horiz_border) / max_num_columns; + static constexpr double cell_height = (page_height - 2 * vert_border) / num_rows; + + static constexpr double table_height = page_height - vert_border * 2; + const double table_width; + const double x_min; + const double x_max; + + double cell_x(unsigned pos) const noexcept { return x_min + cell_width * (pos / num_rows); } + + double cell_y(unsigned pos) const noexcept + { + return vert_border + cell_height * (pos % num_rows); + } +}; + +static option longopts[] = { + {"blocks-file", 1, 0, 'b'}, + {"font-file", 1, 0, 'f'}, + {"output-file", 1, 0, 'o'}, + {"help", 0, 0, 'h'}, + {"other-font-file", 1, 0, 'd'}, + {"postscript-output", 0, 0, 's'}, + {"svg", 0, 0, 'g'}, + {"print-outline", 0, 0, 'l'}, + {"write-outline", 0, 0, 'w'}, + {"include-range", 1, 0, 'i'}, + {"exclude-range", 1, 0, 'x'}, + {"style", 1, 0, 't'}, + {"font-index", 1, 0, 'n'}, + {"other-index", 1, 0, 'm'}, + {"no-embed", 0, 0, 'e'}, + {"use-pango", 0, 0, 'p'}, /* For compatibility with version <= 5.3 */ + {0, 0, 0, 0}, +}; + +static const char *blocks_file; +static const char *font_file_name; +static const char *other_font_file_name; +static const char *output_file_name; +static bool postscript_output; +static bool svg_output; +static bool print_outline; +static bool write_outline; +static bool no_embed; +static unicode_range_set ranges; +static int font_index; +static int other_index; + +struct fntsample_style { + const char *const name; + const char *const default_val; + char *val; +}; + +static fntsample_style styles[] = { + {"header-font", "Sans Bold 12", nullptr}, + {"font-name-font", "Serif Bold 12", nullptr}, + {"table-numbers-font", "Sans 10", nullptr}, + {"cell-numbers-font", "Mono 8", nullptr}, + {nullptr, nullptr, nullptr}, +}; + +struct table_fonts { + Pango::FontDescription header; + Pango::FontDescription font_name; + Pango::FontDescription table_numbers; + Pango::FontDescription cell_numbers; +}; + +static table_fonts table_fonts; + +static double cell_label_offset; +static double cell_glyph_bot_offset; +static double glyph_baseline_offset; +static double font_scale; + +static void usage(const char *); + +static fntsample_style *find_style(const char *name) +{ + for (fntsample_style *style = styles; style->name; style++) { + if (!strcmp(name, style->name)) { + return style; + } + } + + return nullptr; +} + +static int set_style(const char *name, const char *val) +{ + fntsample_style *style = find_style(name); + + if (!style) { + return -1; + } + + char *new_val = strdup(val); + if (!new_val) { + return -1; + } + + if (style->val) { + free(style->val); + } + + style->val = new_val; + + return 0; +} + +static const char *get_style(const char *name) +{ + fntsample_style *style = find_style(name); + + if (!style) { + return nullptr; + } + + return style->val ? style->val : style->default_val; +} + +static int parse_style_string(char *s) +{ + char *n = strchr(s, ':'); + if (!n) { + return -1; + } + + *n++ = '\0'; + return set_style(s, n); +} + +/* + * Parse output range. + */ +static optional parse_range(string_view s) +{ + auto parse_code = [](const char *first, const char *last) -> optional { + int base = 10; + assert(first < last); + + if (*first == '0') { + base = 8; + first++; + if (first < last && (*first == 'x' || *first == 'X')) { + base = 16; + first++; + } + } + + unsigned long n; + auto [ptr, ec] = from_chars(first, last, n, base); + if (ec == errc()) { + return unicode_utils::to_char32_t(n); + } + return {}; + }; + + unicode_range r {0, unicode_utils::last_codepoint}; + + auto minus = s.find('-'); + + if (minus != s.npos) { + // minus found + if (s.size() == 1) { + return {}; + } + + if (minus > 0) { + // not at the beginning + auto res = parse_code(s.data(), s.data() + minus); + if (!res) { + return {}; + } + r.start = *res; + } + + if (minus < s.size() - 1) { + // not at the end + auto res = parse_code(s.data() + minus + 1, s.data() + s.size()); + if (!res) { + return {}; + } + r.end = *res; + } + } else { + // no minus found - single codepoint + auto res = parse_code(s.data(), s.data() + s.size()); + if (!res) { + return {}; + } + r.start = r.end = *res; + } + + if (!r.is_valid()) { + return {}; + } + + return r; +} + +bool add_range(string_view s, bool include) +{ + auto res = parse_range(s); + if (!res) { + return false; + } + ranges.add(*res, include); + return true; +} + +/* + * Get glyph index for the next glyph from the given font face, that + * represents character from output range specified by the user. + * + * Returns character code, updates 'idx'. + * 'idx' can became 0 if there are no more glyphs. + */ +static FT_ULong get_next_char(FT_Face face, FT_ULong charcode, FT_UInt *idx) +{ + FT_ULong rval = charcode; + + do { + rval = FT_Get_Next_Char(face, rval, idx); + } while (*idx && !ranges.contains(rval)); + + return rval; +} + +/* + * Locate first character from the given font face that belongs + * to the user-specified output range. + * + * Returns character code, updates 'idx' with glyph index. + * Glyph index can became 0 if there are no matching glyphs in the font. + */ +static FT_ULong get_first_char(FT_Face face, FT_UInt *idx) +{ + FT_ULong rval = FT_Get_First_Char(face, idx); + + if (*idx && !ranges.contains(rval)) { + rval = get_next_char(face, rval, idx); + } + + return rval; +} + +/* + * Create Pango layout for the given text. + * Updates 'r' with text extents. + * Returned layout should be freed using g_object_unref(). + */ +static auto layout_text(Cairo::RefPtr &cr, Pango::FontDescription &ftdesc, + const char *text) +{ + auto layout = Pango::Layout::create(cr); + + layout->set_font_description(ftdesc); + layout->set_text(text); + Pango::Rectangle ink_rect, logical_rect; + layout->get_extents(ink_rect, logical_rect); + + // FIXME: can logical rect be used instead? + return make_tuple(layout, ink_rect); +} + +static void parse_options(int argc, char *const argv[]) +{ + for (;;) { + int c = getopt_long(argc, argv, "b:f:o:hd:sglwi:x:t:n:m:ep", longopts, nullptr); + + if (c == -1) { + break; + } + + switch (c) { + case 'b': + if (blocks_file) { + cerr << _("Unicode blocks file should be given at most once!\n"); + exit(1); + } + blocks_file = optarg; + break; + case 'f': + if (font_file_name) { + cerr << _("Font file name should be given only once!\n"); + exit(1); + } + font_file_name = optarg; + break; + case 'o': + if (output_file_name) { + cerr << _("Output file name should be given only once!\n"); + exit(1); + } + output_file_name = optarg; + break; + case 'h': + usage(argv[0]); + exit(0); + break; + case 'd': + if (other_font_file_name) { + cerr << _("Font file name should be given only once!\n"); + exit(1); + } + other_font_file_name = optarg; + break; + case 's': + postscript_output = true; + break; + case 'g': + svg_output = true; + break; + case 'l': + print_outline = true; + break; + case 'w': + write_outline = true; + break; + case 'i': + case 'x': + if (!add_range(optarg, c == 'i')) { + usage(argv[0]); + exit(1); + } + break; + case 't': + if (parse_style_string(optarg) == -1) { + usage(argv[0]); + exit(1); + } + break; + case 'n': + font_index = atoi(optarg); + break; + case 'm': + other_index = atoi(optarg); + break; + case 'e': + no_embed = true; + break; + case 'p': + /* Ignored for compatibility */ + break; + case '?': + default: + usage(argv[0]); + exit(1); + break; + } + } + + if (!font_file_name || !output_file_name) { + usage(argv[0]); + exit(1); + } + + if (font_index < 0 || other_index < 0) { + cerr << _("Font index should be non-negative!\n"); + exit(1); + } + + if (postscript_output && svg_output) { + cerr << _("-s and -g cannot be used together!\n"); + exit(1); + } +} + +/* + * Format and print/write outline information, if requested by the user. + */ +static void outline(Cairo::RefPtr &cr, int level, int page, const char *text) +{ + if (print_outline) { + fmt::print("{} {} {}\n", level, page, text); + } + + if (write_outline) { + // TODO: cache surface + if (auto surface = cr->get_target(); surface->get_type() == Cairo::Surface::Type::PDF) { + auto s = fmt::format("page={}", page); + /* FIXME passing level here is not correct. */ + cairo_pdf_surface_add_outline(surface->cobj(), level, text, s.c_str(), + CAIRO_PDF_OUTLINE_FLAG_OPEN); + } + } +} + +static void draw_header_line(Cairo::RefPtr &cr, Pango::FontDescription &ftdesc, + const char *text, double y) +{ + auto [layout, r] = layout_text(cr, ftdesc, text); + cr->move_to((page_metrics::page_width - pango_units_to_double(r.get_width())) / 2.0, y); + as_const(layout)->get_line(0)->show_in_cairo_context(cr); +} + +/* + * Draw header of a page. + * Header shows font name and current Unicode block. + */ +static void draw_header(Cairo::RefPtr &cr, const char *face_name, + const char *block_name) +{ + draw_header_line(cr, table_fonts.font_name, face_name, 30.0); + draw_header_line(cr, table_fonts.header, block_name, 50.0); +} + +/* + * Highlight the cell with given coordinates. + * Used to highlight new glyphs. + */ +static void highlight_cell(Cairo::RefPtr &cr, double x, double y) +{ + SaveGuard saver(cr); + cr->set_source_rgb(1.0, 1.0, 0.6); + cr->rectangle(x, y, page_metrics::cell_width, page_metrics::cell_height); + cr->fill(); +} + +/* + * Draw table grid with row and column numbers. + */ +static void draw_grid(Cairo::RefPtr &cr, const page_metrics &page, + unsigned long block_start) +{ + cr->set_line_width(1.0); + cr->rectangle(page.x_min, page.vert_border, page.table_width, page.table_height); + cr->move_to(page.x_min, page.vert_border); + cr->line_to(page.x_min, page.vert_border - 15.0); + cr->move_to(page.x_max, page.vert_border); + cr->line_to(page.x_max, page.vert_border - 15.0); + cr->stroke(); + + cr->set_line_width(0.5); + + /* draw horizontal lines */ + for (unsigned row = 1; row < page.num_rows; row++) { + const auto y = page.vert_border + row * page.cell_height; + cr->move_to(page.x_min, y); + cr->line_to(page.x_max, y); + } + + /* draw vertical lines */ + for (unsigned col = 1; col < page.num_columns; col++) { + const auto x = page.x_min + col * page.cell_width; + cr->move_to(x, page.vert_border); + cr->line_to(x, page.page_height - page.vert_border); + } + cr->stroke(); + + string buf; + + /* draw glyph numbers */ + for (unsigned row = 0; row < page.num_rows; row++) { + buf = fmt::format("{:X}", row); + + auto [layout, r] = layout_text(cr, table_fonts.table_numbers, buf.c_str()); + const auto y = page.vert_border + (row + 0.5) * page.cell_height + + pango_units_to_double(r.get_descent()) / 2; + + cr->move_to(page.x_min - pango_units_to_double(r.get_rbearing()) - 5.0, y); + auto line = as_const(layout)->get_line(0); + line->show_in_cairo_context(cr); + cr->move_to(page.x_max + 5.0, y); + line->show_in_cairo_context(cr); + } + + for (unsigned col = 0; col < page.num_columns; col++) { + buf = fmt::format("{:03X}", block_start / page.num_rows + col); + + auto [layout, r] = layout_text(cr, table_fonts.table_numbers, buf.c_str()); + cr->move_to(page.x_min + col * page.cell_width + + (page.cell_width - pango_units_to_double(r.get_width())) / 2, + page.vert_border - 5.0); + as_const(layout)->get_line(0)->show_in_cairo_context(cr); + } +} + +/* + * Fill empty cell. Color of the fill depends on the character properties. + */ +static void fill_empty_cell(Cairo::RefPtr &cr, double x, double y, + unsigned long charcode) +{ + SaveGuard saver(cr); + if (g_unichar_isdefined(charcode)) { + if (g_unichar_iscntrl(charcode)) { + cr->set_source_rgb(0.0, 0.0, 0.5); + } else { + cr->set_source_rgb(0.5, 0.5, 0.5); + } + } + cr->rectangle(x, y, page_metrics::cell_width, page_metrics::cell_height); + cr->fill(); +} + +/* + * Draw label with character code. + */ +static void draw_charcode(Cairo::RefPtr &cr, double x, double y, FT_ULong charcode) +{ + auto s = fmt::format("{:04X}", charcode); + + // FIXME + auto [layout, r] = layout_text(cr, table_fonts.cell_numbers, s.c_str()); + cr->move_to(x + (page_metrics::cell_width - pango_units_to_double(r.get_width())) / 2.0, + y + page_metrics::cell_height - cell_label_offset); + as_const(layout)->get_line(0)->show_in_cairo_context(cr); +} + +/* + * Draws tables for all characters in the given Unicode block. + * Use font described by face and ft_face. Start from character + * with given charcode (it should belong to the given Unicode + * block). After return 'charcode' equals the last character code + * of the block. + * + * Returns number of pages drawn. + */ +static int draw_unicode_block(Cairo::RefPtr &cr, + Glib::RefPtr &layout, FT_Face ft_face, + const char *font_name, unsigned long charcode, + const unicode_blocks::block &block, FT_Face ft_other_face) +{ + int npages = 0; + FT_UInt idx = FT_Get_Char_Index(ft_face, charcode); + + do { + unsigned long offset = ((charcode - block.r.start) / 0x100) * 0x100; + unsigned long tbl_start = block.r.start + offset; + unsigned long tbl_end + = tbl_start + 0xFF > block.r.end ? block.r.end + 1 : tbl_start + 0x100; + const page_metrics page((tbl_end - tbl_start) / page.num_rows); + + bitset<256> filled_cells; /* 16x16 glyphs max */ + unsigned long curr_charcode = tbl_start; + int pos = 0; + + SaveGuard saver(cr); + draw_header(cr, font_name, block.name); + + /* + * Fill empty cells and calculate coordinates of the glyphs. + * Also highlight cells if needed. + */ + do { + /* fill empty cells before the current glyph */ + for (; curr_charcode < charcode; curr_charcode++, pos++) { + fill_empty_cell(cr, page.cell_x(pos), page.cell_y(pos), curr_charcode); + } + + /* if it is new glyph - highlight the cell */ + if (ft_other_face && !FT_Get_Char_Index(ft_other_face, charcode)) { + highlight_cell(cr, page.cell_x(pos), page.cell_y(pos)); + } + + /* draw the character */ + char buf[9]; + gint len = g_unichar_to_utf8((gunichar)charcode, buf); + buf[len] = 0; + layout->set_text(buf); + + double baseline = pango_units_to_double(layout->get_baseline()); + cr->move_to(page.cell_x(pos), page.cell_y(pos) + glyph_baseline_offset - baseline); + + if (no_embed) { + layout->add_to_cairo_context(cr); + } else { + layout->show_in_cairo_context(cr); + } + + filled_cells.set(pos); + curr_charcode++; + pos++; + + charcode = get_next_char(ft_face, charcode, &idx); + } while (idx && (charcode < tbl_end) && block.contains(charcode)); + + /* Fill remaining empty cells */ + for (; curr_charcode < tbl_end; curr_charcode++, pos++) { + fill_empty_cell(cr, page.cell_x(pos), page.cell_y(pos), curr_charcode); + } + + /* + * Charcodes are drawn here to avoid switching between the charcode + * font and the cell font for each filled cell. + */ + for (unsigned long i = 0; i < tbl_end - tbl_start; i++) { + if (filled_cells.test(i)) { + draw_charcode(cr, page.cell_x(i), page.cell_y(i), i + tbl_start); + } + } + + draw_grid(cr, page, tbl_start); + npages++; + cr->show_page(); + } while (idx && block.contains(charcode)); + + return npages; +} + +static auto create_glyph_layout(Cairo::RefPtr &cr, FcConfig *fc_config, + FcPattern *fc_font) +{ + auto fontmap = Glib::wrap(pango_cairo_font_map_new_for_font_type(CAIRO_FONT_TYPE_FT)); + pango_fc_font_map_set_config(PANGO_FC_FONT_MAP(fontmap->gobj()), fc_config); + auto context = fontmap->create_context(); + context->update_from_cairo_context(cr); + + auto font_desc = Glib::wrap(pango_fc_font_description_from_pattern(fc_font, FALSE)); + auto layout = Pango::Layout::create(context); + layout->set_font_description(font_desc); + layout->set_width(pango_units_from_double(page_metrics::cell_width)); + layout->set_alignment(Pango::Alignment::CENTER); + + return layout; +} + +/* + * The main drawing function. + */ +static void draw_glyphs(const unicode_blocks &blocks, Cairo::RefPtr &cr, + FT_Face ft_face, FT_Face ft_other_face) +{ + FcConfig *fc_config = FcConfigCreate(); + FcConfigAppFontAddFile(fc_config, (const FcChar8 *)font_file_name); + + FcPattern *fc_pat = FcPatternCreate(); + FcPatternAddInteger(fc_pat, FC_INDEX, font_index); + + FcFontSet *fc_fontset = FcFontList(fc_config, fc_pat, nullptr); + assert(fc_fontset->nfont > 0); + FcPattern *fc_font = fc_fontset->fonts[0]; + + const char *font_name; + if (FcPatternGetString(fc_font, FC_FULLNAME, 0, (FcChar8 **)&font_name) != FcResultMatch) { + font_name = "Unknown"; + } + + int pageno = 1; + outline(cr, 0, pageno, font_name); + + auto layout = create_glyph_layout(cr, fc_config, fc_font); + + FT_UInt idx; + FT_ULong charcode = get_first_char(ft_face, &idx); + + while (idx) { + if (auto b = blocks.find_block(charcode); b.has_value()) { + const auto block = *b; + + outline(cr, 1, pageno, block.name); + int npages = draw_unicode_block(cr, layout, ft_face, font_name, charcode, block, + ft_other_face); + pageno += npages; + charcode = block.r.end; + } + + charcode = get_next_char(ft_face, charcode, &idx); + } + + FcPatternDestroy(fc_pat); + FcFontSetDestroy(fc_fontset); + FcConfigDestroy(fc_config); +} + +/* + * Print usage instructions and default values for styles + */ +static void usage(const char *cmd) +{ + fmt::print(cerr, + _("Usage: {0} [ OPTIONS ] -f FONT-FILE -o OUTPUT-FILE\n" + " {0} -h\n\n"), + cmd); + cerr << _( + "Options:\n" + " --blocks-file, -b BLOCKS-FILE Read Unicode blocks information from " + "BLOCKS-FILE\n" + " --font-file, -f FONT-FILE Create samples of FONT-FILE\n" + " --font-index, -n IDX Font index in FONT-FILE\n" + " --output-file, -o OUTPUT-FILE Save samples to OUTPUT-FILE\n" + " --help, -h Show this information message and exit\n" + " --other-font-file, -d OTHER-FONT Compare FONT-FILE with OTHER-FONT and highlight " + "added glyphs\n" + " --other-index, -m IDX Font index in OTHER-FONT\n" + " --postscript-output, -s Use PostScript format for output instead of PDF\n" + " --svg, -g Use SVG format for output\n" + " --print-outline, -l Print document outlines data to standard output\n" + " --write-outline, -w Write document outlines (only in PDF output)\n" + " --no-embed, -e Don't embed the font in the output file, draw " + "the glyphs instead\n" + " --include-range, -i RANGE Show characters in RANGE\n" + " --exclude-range, -x RANGE Do not show characters in RANGE\n" + " --style, -t \"STYLE: VAL\" Set STYLE to value VAL\n"); + + cerr << _("\nSupported styles (and default values):\n"); + + for (const fntsample_style *style = styles; style->name; style++) { + fmt::print(cerr, "\t{} ({})\n", style->name, style->default_val); + } +} + +/* + * Initialize fonts used to print table heders and character codes. + */ +static void init_table_fonts(void) +{ + // FIXME https://gitlab.gnome.org/GNOME/pangomm/-/issues/15 + auto map = reinterpret_cast(pango_cairo_font_map_get_default()); + pango_cairo_font_map_set_resolution(map, POINTS_PER_INCH); + + table_fonts.header = Pango::FontDescription(get_style("header-font")); + table_fonts.font_name = Pango::FontDescription(get_style("font-name-font")); + table_fonts.table_numbers = Pango::FontDescription(get_style("table-numbers-font")); + table_fonts.cell_numbers = Pango::FontDescription(get_style("cell-numbers-font")); +} + +/* + * Calculate various offsets. + */ +static void calculate_offsets(Cairo::RefPtr &cr) +{ + /* Assume that vertical extents does not depend on actual text */ + auto [layout, extents] = layout_text(cr, table_fonts.cell_numbers, "0123456789ABCDEF"); + /* Unsolved mistery of pango's font metrics.... */ + double digits_ascent = pango_units_to_double(extents.get_descent()); + double digits_descent = -pango_units_to_double(extents.get_ascent()); + + cell_label_offset = digits_descent + 2; + cell_glyph_bot_offset = cell_label_offset + digits_ascent + 2; +} + +/* + * Calculate font scaling + */ +void calc_font_scaling(FT_Face ft_face) +{ + auto cr_face = Cairo::FtFontFace::create(ft_face, 0); + auto options = Cairo::FontOptions(); + + /* First create font with size 1 and measure it */ + auto font_matrix = Cairo::identity_matrix(); + const auto ctm = Cairo::identity_matrix(); + + /* Turn off rounding, so we can get real metrics */ + options.set_hint_metrics(Cairo::FontOptions::HintMetrics::OFF); + auto cr_font = Cairo::ScaledFont::create(cr_face, font_matrix, ctm, options); + Cairo::FontExtents extents; + cr_font->get_extents(extents); + + /* Use some magic to find the best font size... */ + double tgt_size = page_metrics::cell_height - cell_glyph_bot_offset - 2; + if (tgt_size <= 0) { + cerr << _("Not enough space for rendering glyphs. Make cell font smaller.\n"); + exit(5); + } + + double act_size = extents.ascent + extents.descent; + if (act_size <= 0) { + fmt::print(cerr, _("The font has strange metrics: ascent + descent = {}\n"), act_size); + exit(5); + } + + font_scale = tgt_size / act_size; + if (font_scale > 1) { + font_scale = trunc(font_scale); // just to make numbers nicer + } + font_scale = min(font_scale, 20.0); // Do not make font larger than in previous versions + + /* Create the font once again, but this time scaled */ + font_matrix = Cairo::scaling_matrix(font_scale, font_scale); + cr_font = Cairo::ScaledFont::create(cr_face, font_matrix, ctm, options); + cr_font->get_extents(extents); + glyph_baseline_offset + = (tgt_size - (extents.ascent + extents.descent)) / 2 + 2 + extents.ascent; +} + +/* + * Configure DPF surface metadata so fntsample can be used with + * repeatable builds. + */ +static void set_repeatable_pdf_metadata(Cairo::RefPtr &surface) +{ + char *source_date_epoch = getenv("SOURCE_DATE_EPOCH"); + + if (source_date_epoch) { + char *endptr; + time_t now = strtoul(source_date_epoch, &endptr, 10); + + if (*endptr != 0) { + cerr << _("Failed to parse environment variable SOURCE_DATE_EPOCH.\n"); + exit(1); + } + tm *build_time = gmtime(&now); + char buffer[25]; + // TODO + strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", build_time); + + cairo_pdf_surface_set_metadata(surface->cobj(), CAIRO_PDF_METADATA_CREATE_DATE, buffer); + } +} + +int main(int argc, char **argv) +{ + setlocale(LC_ALL, ""); + bindtextdomain(CMAKE_PROJECT_NAME, CMAKE_INSTALL_FULL_LOCALEDIR); + textdomain(CMAKE_PROJECT_NAME); + + parse_options(argc, argv); + + unique_ptr loadable_blocks; + + if (blocks_file) { + ifstream f(blocks_file, ios::binary); + if (!f) { + fmt::print(cerr, _("{}: cannot open Unicode blocks file.\n"), argv[0]); + exit(6); + } + + // FIXME + loadable_blocks = make_unique(f); + if (loadable_blocks->error_line()) { + fmt::print(cerr, _("{}: parse error in Unicode blocks file at line {}\n"), argv[0], + loadable_blocks->error_line()); + exit(6); + } + } + + const unicode_blocks &blocks = loadable_blocks ? *loadable_blocks : get_static_blocks(); + + FT_Library library; + FT_Error error = FT_Init_FreeType(&library); + + if (error) { + /* TRANSLATORS: 'freetype' is a name of a library, and should be left untranslated */ + fmt::print(cerr, _("{}: freetype error\n"), argv[0]); + exit(3); + } + + FT_Face face; + error = FT_New_Face(library, font_file_name, font_index, &face); + + if (error) { + fmt::print(cerr, _("%{}: failed to open font file {}\n"), argv[0], font_file_name); + exit(4); + } + + FT_Face other_face = nullptr; + + if (other_font_file_name) { + error = FT_New_Face(library, other_font_file_name, other_index, &other_face); + + if (error) { + fmt::print(cerr, _("%{}: failed to create new font face\n"), argv[0]); + exit(4); + } + } + + Pango::init(); + + Cairo::RefPtr surface; + + if (postscript_output) { + surface = Cairo::PsSurface::create(output_file_name, page_metrics::page_width, + page_metrics::page_height); + } else if (svg_output) { + surface = Cairo::SvgSurface::create(output_file_name, page_metrics::page_width, + page_metrics::page_height); + } else { + surface = Cairo::PdfSurface::create(output_file_name, page_metrics::page_width, + page_metrics::page_height); + set_repeatable_pdf_metadata(surface); + } + + auto cr = Cairo::Context::create(surface); + + init_table_fonts(); + calculate_offsets(cr); + + cr->set_source_rgb(0.0, 0.0, 0.0); + calc_font_scaling(face); + draw_glyphs(blocks, cr, face, other_face); + + return 0; +} diff --git a/src/gen_unicode_blocks.c b/src/gen_unicode_blocks.c deleted file mode 100644 index 6e24fd4..0000000 --- a/src/gen_unicode_blocks.c +++ /dev/null @@ -1,62 +0,0 @@ -/* Copyright © Євгеній Мещеряков - * SPDX-License-Identifier: GPL-3.0-or-later - */ -#include "unicode_blocks.h" -#include - -static void write_header(FILE *f) -{ - fprintf(f, - "#include \"static_unicode_blocks.h\"\n" - "\n" - "const struct unicode_block static_unicode_blocks[] = {\n"); -} - -static void write_footer(FILE *f) -{ - fprintf(f, - " {0, 0, NULL},\n" - "};\n"); -} - -static void write_block(FILE *f, const struct unicode_block *block) -{ - fprintf(f, " {0x%04lx, 0x%04lx, \"%s\"},\n", block->start, block->end, block->name); -} - -static void write_blocks(FILE *f, const struct unicode_block *blocks, int n) -{ - write_header(f); - for (int i = 0; i < n; i++) { - write_block(f, blocks + i); - } - write_footer(f); -} - -int main(int argc, char **argv) -{ - if (argc != 3) { - fprintf(stderr, "Usage: %s Blocks.txt output.c\n", argv[0]); - return 1; - } - - int n; - struct unicode_block *blocks = read_blocks(argv[1], &n); - - if (!blocks) { - fprintf(stderr, "Failed to read unicode blocks file.\n"); - return 2; - } - - FILE *f = fopen(argv[2], "wb"); - if (!f) { - perror("fopen"); - return 3; - } - - write_blocks(f, blocks, n); - free(blocks); - fclose(f); - - return 0; -} diff --git a/src/gen_unicode_blocks.cpp b/src/gen_unicode_blocks.cpp new file mode 100644 index 0000000..7d6e741 --- /dev/null +++ b/src/gen_unicode_blocks.cpp @@ -0,0 +1,82 @@ +/* Copyright © Євгеній Мещеряков + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include +#include +#include + +#include "loadable_unicode_blocks.h" + +using namespace std; + +static void write_header(ostream &stream) +{ + stream << + R"(#include "static_unicode_blocks.h" + +constexpr static_unicode_blocks static_blocks { +)"; +} + +static void write_footer(ostream &stream) +{ + stream << R"(}; + +const unicode_blocks &get_static_blocks() { return static_blocks; } +)"; +} + +static void write_block(ostream &stream, const unicode_blocks::block &block) +{ + fmt::print(stream, " unicode_blocks::block {{{{0x{:04x}, 0x{:04x}}}, \"{}\"}},\n", + static_cast(block.r.start), static_cast(block.r.end), + block.name); +} + +static void write_blocks(ostream &stream, const unicode_blocks &blocks) +{ + write_header(stream); + for (size_t i = 0; i < blocks.size(); i++) { + write_block(stream, blocks[i]); + } + write_footer(stream); +} + +int main(int argc, char **argv) +{ + if (argc != 3) { + fmt::print(cerr, "Usage: {} Blocks.txt output.c\n", argv[0]); + return 1; + } + + const char *input_file_name = argv[1]; + const char *output_file_name = argv[2]; + + ifstream input(input_file_name, ios::binary); + if (!input) { + cerr << "Failed to open input file\n"; + return 1; + } + + loadable_unicode_blocks blocks(input); + input.close(); + + if (blocks.error_line()) { + fmt::print(cerr, "Parse error at line {}\n", blocks.error_line()); + return 1; + } + + ofstream output(output_file_name, ios::out | ios::trunc | ios::binary); + if (!output) { + cerr << "Failed to open output file\n"; + return 1; + } + + write_blocks(output, blocks); + if (!output) { + cerr << "Failed to write the output\n"; + return 1; + } + + return 0; +} diff --git a/src/loadable_unicode_blocks.cpp b/src/loadable_unicode_blocks.cpp new file mode 100644 index 0000000..dcb7ef0 --- /dev/null +++ b/src/loadable_unicode_blocks.cpp @@ -0,0 +1,72 @@ +/* Copyright © Євгеній Мещеряков + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "loadable_unicode_blocks.h" +#include +#include +#include +#include "unicode_utils.h" + +using namespace std; + +loadable_unicode_blocks::loadable_unicode_blocks(istream &stream) +{ + static const regex re(R"(([[:xdigit:]]+)\.\.([[:xdigit:]]+); (.*))"); + string line; + + for (size_t line_no = 1; getline(stream, line); line_no++) { + string_view v(line); + + // Remove comment if any + if (auto c_pos = v.find('#'); c_pos != v.npos) { + v.remove_suffix(v.size() - c_pos); + } + + // Trim whitespace at the end + auto ws_pos = v.end(); + for (ws_pos--; ws_pos >= v.begin(); ws_pos--) { + auto c = *ws_pos; + if (!(c == '\n' || c == '\r' || c == ' ' || c == '\t')) { + break; + } + } + v.remove_suffix(v.size() - (ws_pos + 1 - v.begin())); + + if (v.empty()) { + continue; + } + + cmatch m; + if (!regex_match(v.data(), v.data() + v.size(), m, re)) { + _error_line = line_no; + blocks.clear(); + return; + } + + auto parse_code = [](cmatch::const_reference sm) -> optional { + unsigned long n; + auto [ptr, ec] = from_chars(sm.first, sm.second, n, 16); + if (ec == std::errc()) { + return unicode_utils::to_char32_t(n); + } + return {}; + }; + + auto start = parse_code(m[1]); + auto end = parse_code(m[2]); + if (!start || !end) { + _error_line = line_no; + blocks.clear(); + return; + } + + unicode_range r {*start, *end}; + if (!r.is_valid()) { + _error_line = line_no; + blocks.clear(); + return; + } + + blocks.push_back({r, string(m[3].first, m[3].second)}); + } +} diff --git a/src/loadable_unicode_blocks.h b/src/loadable_unicode_blocks.h new file mode 100644 index 0000000..be82ea6 --- /dev/null +++ b/src/loadable_unicode_blocks.h @@ -0,0 +1,34 @@ +/* Copyright © Євгеній Мещеряков + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef LOADABLE_UNICODE_BLOCKS_H +#define LOADABLE_UNICODE_BLOCKS_H + +#include "unicode_blocks.h" +#include +#include + +class loadable_unicode_blocks final : public unicode_blocks { +public: + explicit loadable_unicode_blocks(std::istream &stream); + + size_t size() const noexcept override { return blocks.size(); } + block operator[](size_t i) const override + { + const auto &b = blocks[i]; + return {b.r, b.name.data()}; + } + + constexpr size_t error_line() const noexcept { return _error_line; } + +private: + struct entry { + unicode_range r; + std::string name; + }; + + std::vector blocks; + size_t _error_line = 0; +}; + +#endif diff --git a/src/read_blocks.c b/src/read_blocks.c deleted file mode 100644 index fd5979a..0000000 --- a/src/read_blocks.c +++ /dev/null @@ -1,69 +0,0 @@ -/* Copyright © Євгеній Мещеряков - * SPDX-License-Identifier: GPL-3.0-or-later - */ -#include "unicode_blocks.h" -#include -#include -#include - -struct unicode_block *read_blocks(const char *file_name, int *n) -{ - int nalloc = 256; - struct unicode_block *blocks = calloc(nalloc, sizeof(struct unicode_block)); - *n = 0; - - FILE *input_file = fopen(file_name, "r"); - if (!input_file) { - perror("fopen"); - exit(7); - } - - char *line = NULL; - size_t len = 0; - ssize_t nread; - - while ((nread = getline(&line, &len, input_file)) != -1) { - unsigned long block_start, block_end; - char block_name[256]; - - if (nread >= (ssize_t)sizeof(block_name)) - continue; - - int matched = sscanf(line, "%lx..%lx; %[^\r\n]", &block_start, &block_end, block_name); - if (matched == 3) { - struct unicode_block *b = blocks + *n; - b->start = block_start; - b->end = block_end; - b->name = strdup(block_name); - if (b->name == NULL) { - perror("strdup"); - exit(8); - } - - *n += 1; - if (*n >= nalloc) { - int new_nalloc = nalloc + 256; - struct unicode_block *new_blocks - = realloc(blocks, new_nalloc * sizeof(struct unicode_block)); - if (new_blocks == NULL) { - perror("realloc"); - exit(9); - } - memset(new_blocks + nalloc, 0, - (new_nalloc - nalloc) * sizeof(struct unicode_block)); - nalloc = new_nalloc; - blocks = new_blocks; - } - } - } - free(line); - - if (*n == 0) { - free(blocks); - return NULL; - } else if (*n < nalloc) { - blocks = realloc(blocks, *n * sizeof(struct unicode_block)); - } - - return blocks; -} diff --git a/src/static_unicode_blocks.h b/src/static_unicode_blocks.h index 594e52d..c505d43 100644 --- a/src/static_unicode_blocks.h +++ b/src/static_unicode_blocks.h @@ -1,11 +1,29 @@ -/* - * Author: Ievgenii Meshcheriakov - * SPDX-License-Identifier: CC-PDDC +/* Copyright © Євгеній Мещеряков + * SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef STATIC_UNICODE_BLOCKS_H #define STATIC_UNICODE_BLOCKS_H + #include "unicode_blocks.h" +#include + +template class static_unicode_blocks final : public unicode_blocks { +public: + template + constexpr explicit static_unicode_blocks(T &&... blocks) noexcept + : blocks {std::forward(blocks)...} + { + } + + size_t size() const noexcept override { return blocks.size(); } + block operator[](size_t i) const override { return blocks[i]; } + +private: + std::array blocks; +}; + +template static_unicode_blocks(T &&...)->static_unicode_blocks; -extern const struct unicode_block static_unicode_blocks[]; +const unicode_blocks &get_static_blocks(); #endif diff --git a/src/unicode_blocks.cpp b/src/unicode_blocks.cpp new file mode 100644 index 0000000..a82b8b4 --- /dev/null +++ b/src/unicode_blocks.cpp @@ -0,0 +1,23 @@ +/* Copyright © Євгеній Мещеряков + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "unicode_blocks.h" + +using namespace std; + +/* + * Locate unicode block that contains given character code. + * Returns this block or nullptr if not found. + */ +optional unicode_blocks::find_block(char32_t c) const noexcept +{ + // TODO use binary search + for (size_t i = 0; i < size(); i++) { + const auto &block = (*this)[i]; + if (block.r.contains(c)) { + return block; + } + } + + return {}; +} diff --git a/src/unicode_blocks.h b/src/unicode_blocks.h index f07924d..28891a5 100644 --- a/src/unicode_blocks.h +++ b/src/unicode_blocks.h @@ -1,18 +1,26 @@ -/* - * Author: Ievgenii Meshcheriakov - * SPDX-License-Identifier: CC-PDDC +/* Copyright © Євгеній Мещеряков + * SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef UNICODE_BLOCKS_H #define UNICODE_BLOCKS_H -#include +#include "unicode_range.h" +#include -struct unicode_block { - unsigned long start; - unsigned long end; - const char *name; -}; +// TODO: provide an iterator +class unicode_blocks { +public: + struct block { + unicode_range r; + const char *name; + + constexpr bool contains(char32_t c) const noexcept { return r.contains(c); } + }; -struct unicode_block *read_blocks(const char *file_name, int *n); + virtual size_t size() const noexcept = 0; + virtual block operator[](size_t index) const = 0; + + std::optional find_block(char32_t c) const noexcept; +}; #endif diff --git a/src/unicode_range.h b/src/unicode_range.h new file mode 100644 index 0000000..f521b30 --- /dev/null +++ b/src/unicode_range.h @@ -0,0 +1,16 @@ +/* Copyright © Євгеній Мещеряков + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef UNICODE_RANGE_H +#define UNICODE_RANGE_H + +template struct basic_range { + T start, end; + + constexpr bool contains(T e) const noexcept { return start <= e && e <= end; } + constexpr bool is_valid() const noexcept { return start <= end; } +}; + +using unicode_range = basic_range; + +#endif diff --git a/src/unicode_range_set.h b/src/unicode_range_set.h new file mode 100644 index 0000000..c08af9e --- /dev/null +++ b/src/unicode_range_set.h @@ -0,0 +1,38 @@ +/* Copyright © Євгеній Мещеряков + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef UNICODE_RANGE_SET_H +#define UNICODE_RANGE_SET_H + +#include "unicode_range.h" +#include + +template class basic_range_set { +public: + using range = basic_range; + + bool contains(T e) const noexcept + { + bool in = !specs.empty() || !specs.front().include; + for (auto spec : specs) { + if (spec.r.contains(e)) { + in = spec.include; + } + } + return in; + } + + void add(const range &r, bool include) noexcept { specs.push_back(range_spec {r, include}); } + +private: + struct range_spec { + range r; + bool include; + }; + + std::list specs; +}; + +using unicode_range_set = basic_range_set; + +#endif diff --git a/src/unicode_utils.h b/src/unicode_utils.h new file mode 100644 index 0000000..59fb0e1 --- /dev/null +++ b/src/unicode_utils.h @@ -0,0 +1,27 @@ +/* Copyright © Євгеній Мещеряков + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef UNICODE_UTILS_H +#define UNICODE_UTILS_H + +#include +#include +#include + +namespace unicode_utils { + +constexpr char32_t last_codepoint = 0x10ffff; + +template constexpr std::optional to_char32_t(U n) noexcept +{ + static_assert(std::is_unsigned_v); + static_assert(std::numeric_limits::max() >= last_codepoint); + if (n <= last_codepoint) { + return static_cast(n); + } + return {}; +} + +} + +#endif