Skip to content

Support Harfbuzz in Metrics View#5522

Merged
iorsh merged 91 commits intofontforge:masterfrom
iorsh:harfbuzz_metrics
Jul 1, 2025
Merged

Support Harfbuzz in Metrics View#5522
iorsh merged 91 commits intofontforge:masterfrom
iorsh:harfbuzz_metrics

Conversation

@iorsh
Copy link
Copy Markdown
Contributor

@iorsh iorsh commented Jan 9, 2025

This PR introduces HarfBuzz shaping in Metrics view.

  1. The current shaping functionality is preserved and can be invoked from View menu as "Built-in shaper". It's very complex, and I'm pretty sure I didn't manage to fully support everything it provides, so some people might find it better in some aspects.
  2. The new shaper comes with some generic infrastructure which can be eventually expanded to support more shapers (e.g. Uniscribe under Windows)
  3. The new code is written mostly in C++17. This version seems reasonably conservative to me.
  4. Editing of kerning values, bearings, and width is supported.
  5. The font is generated for HarfBuzz use every time the Metrics View is opened or shaper engine is switched (in the View menu). This could be slow for large fonts.

After:
aldhabi-hb
Before:
aldhabi-old
Menu:
hb_menu

Fixes #3733, fixes #3837, fixes #4158, fixes #4511, fixes #5343, fixes #5513

context->get_enc_map = SFGetMap;
context->get_char_width = MVCharWidth;
context->get_kern_offset = MVGetKernOffset;
context->script_is_rtl = ScriptIsRightToLeft;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HarfBuzz has hb_script_get_horizontal_direction() which might be more up to date (hb_buffer_guess_segment_properties() will call it if the direction is not set or set to HB_DIRECTION_INVALID). But I still think a separate UI control to set direction is needed since a few scripts can be written horizontally in both directions.

Copy link
Copy Markdown
Contributor Author

@iorsh iorsh Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ScriptIsRightToLeft() is passed for use in the legacy shaper, HarfBuzz won't use it. Regarding the UI, I have some enhancements in mind, but for now I prefer to keep UI changes at the necessary minimum. GDraw poses quite a few challenges in its workings.

auto ttf_map_it = ttf_map_.find(glyph_info.codepoint);
SplineChar* glyph_out = (ttf_map_it != ttf_map_.end())
? ttf_map_it->second
: get_notdef_glyph();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is hb_buffer_set_not_found_glyph() that allow setting any arbitrary glyph index to be used instead of 0, you can, say, set it to a special value that is guaranteed not to be a valid glyph index like INT32_MAX and intercept it here. In case it makes things any simpler/faster.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I tried it, but Ubuntu 22 has HarfBuzz 2.7.4, and hb_buffer_set_not_found_glyph() was introduced in 3.1.0. Besides, I need to check the glyph index anyway, it would be a crash otherwise.

@iorsh
Copy link
Copy Markdown
Contributor Author

iorsh commented Mar 7, 2025

The high-level HarfBuzz shaper structure is as follows:

  • When opening the "Metrics View" window, the shaper is initialized with TTF file generated on-fly.
  • While the window is active, it is responsive to the changes in glyph kerning using HarfBuzzShaper::initial_kerning_. The initial kerning is preserved, and once its value changes, the delta is applied internally over the metrics computed by HarfBuzz.
  • In a similar manner changes to glyph width are also addressed with HarfBuzzShaper::initial_width_, without the need to regenerate the TTF.
  • In case of more complex changes, such as in GSUB/GPOS tables, the user is expected to perform manual refresh, and I added "Refresh Shaper" menu item for that purpose. During the refresh the TTF file is fully regenerated.
  • In total, the shaper is regenerated whenever
    • A new "Metrics View" is opened
    • The user hits "Refresh Shaper"
    • The uses changes the shaper ("Built-in" to "HarfBuzz" or vice versa)

The building of HarfBuzz shaper is heavy, and eventually we shall make an effort to improve its performance. Currently the TTF is written to a temporary file, and as far as I can see, it's not even guaranteed to reside in RAM. In Windows it's always on the disk (see GFileTmpfile()), and in Linux it probably depends on the mount method of /tmp.

For now I would prefer to keep the scope of this PR as is, but I can see several possibilities to improve the performance in future:

  • Refactoring of _WriteTTFFont(), so that it actually uses a generic buffer which can be either kept in memory or dumped onto disk.
  • To employ the granular approach with hb_face_create_for_tables(), it would be necessary to refactor the change tracking. Currently there is a bunch of changed_??? flags scattered across multiple structures, and it would be good to harmonize them with some unified approach.
  • Inherent FF deficiencies, e.g. the names hashtable struct glyphnamehash with 257 baskets, which is totally inadequate for fonts with thousands of glyphs.

@skef
Copy link
Copy Markdown
Contributor

skef commented Apr 22, 2025

I'll take another look at this review-wise, but I suggest that @iorsh should start thinking about documentation.

Comment thread fontforge/views.h Outdated
int16_t cmax, clen;
SplineChar **chars; /* Character input stream */
struct opentype_str *glyphs;/* after going through the various gsub/gpos transformations */
ShapeMetrics *metrics; /* One for each glyph above */
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on difference between ShapeMetrics (metrics_core) and metricchar (metrics_ui)

mv->glyphs = shaper_apply_features(mv->shaper, mv->chars, feats,
script, lang, mv->pixelsize, mv->vertical);
if (mv->glyphs == NULL) {
mv->glyphs = calloc(1, sizeof(struct opentype_str));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By convention, output of ApplyTickedFeatures() is terminated by a zeroized structure and is never NULL (see source). As we move to shaper interface, this convention should hold, but we put this allocation here out of caution.

Comment thread fontforgeexe/metricsview.c Outdated
}

// TODO: Move metrics calculations completely into shapers, and make MetricsView::metric const.
mv->metrics = (ShapeMetrics*)shaper_metrics(mv->shaper);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return this from shaper_apply_features()

return shaper_defs;
}

const char* get_default_shaper() {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add user preference for default shaper.
Retain current shaper per session, which would be initialized from preference, but would not affect it.

}
}

std::set<Tag> BuiltInShaper::default_features(Tag script, Tag /*lang*/,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this was not deleted in legacy code? If still in use, put the reference in comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicates StdFeaturesOfScript(), which is still in use

Comment thread fontforgeexe/shapers/harfbuzz.cpp Outdated
// features list, but if it is unselected it should be in the features
// list with value set to 0 (disable).
const std::set<Tag> default_feats =
default_features(script, lang, vertical);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move outside the cycle

}

// No special glyph found, create it.
notdef_glyph_ = context_->get_or_make_char(context_->sf, -1, ".notdef");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in legacy shaper?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the legacy code we act similarly, calling
MVTextChanged()
WordlistEscapedInputStringToParsedDataComplex()
SFGetOrMakeChar()

Copy link
Copy Markdown
Contributor

@frank-trampe frank-trampe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all makes sense to me.

@iorsh iorsh merged commit 387146a into fontforge:master Jul 1, 2025
11 of 13 checks passed
@brigazvi
Copy link
Copy Markdown

brigazvi commented Jul 2, 2025

THIS IS HUGE!!!!
@iorsh thank you so much for all the hard work you put into this. for RTL designers like me it's makes such a difference.

in my opinion it justifies a new release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

5 participants