diff --git a/docs/raqm-sections.txt b/docs/raqm-sections.txt index 83ca4d8..a40ae1b 100644 --- a/docs/raqm-sections.txt +++ b/docs/raqm-sections.txt @@ -20,6 +20,7 @@ raqm_add_font_feature raqm_layout raqm_get_glyphs raqm_get_par_resolved_direction +raqm_get_par_detected_direction raqm_get_direction_at_index raqm_index_to_position raqm_position_to_index diff --git a/src/raqm.c b/src/raqm.c index 07eb2da..770fe2c 100644 --- a/src/raqm.c +++ b/src/raqm.c @@ -1488,6 +1488,37 @@ raqm_get_par_resolved_direction (raqm_t *rq) return rq->resolved_dir; } +static raqm_direction_t +_raqm_detect_direction (raqm_t *rq); + +/** + * raqm_get_par_detected_direction: + * @rq: a #raqm_t. + * + * Gets the natural direction of the text as detected from its content + * following the Unicode Bidirectional Algorithm, regardless of the base + * direction set with raqm_set_par_direction(). Unlike + * raqm_get_par_resolved_direction(), this does not require raqm_layout() to + * have been called, and can report that the text is direction neutral. + * + * Return value: + * #RAQM_DIRECTION_LTR or #RAQM_DIRECTION_RTL if the text has a strong + * direction, or #RAQM_DIRECTION_DEFAULT if the text is direction neutral (has + * no strong characters) or no text has been set. #RAQM_DIRECTION_TTB is never + * returned, as vertical direction is a layout property, not a property of the + * text content. + * + * Since: 0.11 + */ +raqm_direction_t +raqm_get_par_detected_direction (raqm_t *rq) +{ + if (!rq || !rq->text || rq->text_len == 0) + return RAQM_DIRECTION_DEFAULT; + + return _raqm_detect_direction (rq); +} + /** * raqm_get_direction_at_index: * @rq: a #raqm_t. @@ -1608,6 +1639,31 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count) return runs; } + +static raqm_direction_t +_raqm_detect_direction (raqm_t *rq) +{ + raqm_direction_t dir = RAQM_DIRECTION_DEFAULT; + size_t isolate = 0; + + /* UBA: P2 and P3. */ + for (size_t i = 0; i < rq->text_len; i++) + { + SBBidiType type = SBCodepointGetBidiType (rq->text[i]); + + if (type == SBBidiTypeLRI || type == SBBidiTypeRLI || type == SBBidiTypeFSI) + isolate++; + else if (type == SBBidiTypePDI && isolate > 0) + isolate--; + else if (isolate == 0 && SBBidiTypeIsStrong (type)) + { + dir = (type == SBBidiTypeL) ? RAQM_DIRECTION_LTR : RAQM_DIRECTION_RTL; + break; + } + } + + return dir; +} #else static void _raqm_reverse_run (_raqm_bidi_run *run, const size_t len) @@ -1761,6 +1817,30 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count) return runs; } + +static raqm_direction_t +_raqm_detect_direction (raqm_t *rq) +{ + raqm_direction_t dir = RAQM_DIRECTION_DEFAULT; + FriBidiParType par_type; + FriBidiCharType *types; + + types = calloc (rq->text_len, sizeof (FriBidiCharType)); + if (types) + { + fribidi_get_bidi_types (rq->text, rq->text_len, types); + par_type = fribidi_get_par_direction (types, rq->text_len); + + if (par_type == FRIBIDI_PAR_LTR) + dir = RAQM_DIRECTION_LTR; + else if (par_type == FRIBIDI_PAR_RTL) + dir = RAQM_DIRECTION_RTL; + + free (types); + } + + return dir; +} #endif static bool diff --git a/src/raqm.h b/src/raqm.h index 44813f5..4b60f14 100644 --- a/src/raqm.h +++ b/src/raqm.h @@ -183,6 +183,9 @@ raqm_get_glyphs (raqm_t *rq, RAQM_API raqm_direction_t raqm_get_par_resolved_direction (raqm_t *rq); +RAQM_API raqm_direction_t +raqm_get_par_detected_direction (raqm_t *rq); + RAQM_API raqm_direction_t raqm_get_direction_at_index (raqm_t *rq, size_t index); diff --git a/tests/detected-direction-test.c b/tests/detected-direction-test.c new file mode 100644 index 0000000..c8ebe3d --- /dev/null +++ b/tests/detected-direction-test.c @@ -0,0 +1,96 @@ +/* + * Detected paragraph direction test. + * + * Verifies that raqm_get_par_detected_direction reports the natural direction + * of the text as detected from its content, regardless of the base direction + * set by the user, and reports direction-neutral text as + * RAQM_DIRECTION_DEFAULT. + */ + +#include +#include +#include +#include + +#include "raqm.h" + +static raqm_direction_t +detect (const char *utf8, raqm_direction_t base) +{ + raqm_direction_t dir; + raqm_t *rq = raqm_create (); + assert (rq); + assert (raqm_set_text_utf8 (rq, utf8, strlen (utf8))); + assert (raqm_set_par_direction (rq, base)); + dir = raqm_get_par_detected_direction (rq); + raqm_destroy (rq); + return dir; +} + +int +main (void) +{ + const char *ltr = "English"; + const char *rtl = "عربي"; + const char *neutral = "123.!"; + + /* Detected direction follows the content. */ + assert (detect (ltr, RAQM_DIRECTION_DEFAULT) == RAQM_DIRECTION_LTR); + assert (detect (rtl, RAQM_DIRECTION_DEFAULT) == RAQM_DIRECTION_RTL); + assert (detect (neutral, RAQM_DIRECTION_DEFAULT) == RAQM_DIRECTION_DEFAULT); + + /* ... regardless of the base direction set by the user. */ + assert (detect (ltr, RAQM_DIRECTION_RTL) == RAQM_DIRECTION_LTR); + assert (detect (rtl, RAQM_DIRECTION_LTR) == RAQM_DIRECTION_RTL); + assert (detect (neutral, RAQM_DIRECTION_RTL) == RAQM_DIRECTION_DEFAULT); + assert (detect (neutral, RAQM_DIRECTION_LTR) == RAQM_DIRECTION_DEFAULT); + + /* It is decided by the first strong character (Unicode Bidi rule P2). */ + assert (detect ("A عربي", RAQM_DIRECTION_DEFAULT) == RAQM_DIRECTION_LTR); + assert (detect ("عربي A", RAQM_DIRECTION_DEFAULT) == RAQM_DIRECTION_RTL); + + /* Leading weak/neutral characters are skipped to the first strong one. */ + assert (detect ("123 English", RAQM_DIRECTION_DEFAULT) == RAQM_DIRECTION_LTR); + assert (detect ("123 عربي", RAQM_DIRECTION_DEFAULT) == RAQM_DIRECTION_RTL); + + /* A vertical base direction is a layout property, not a content one, so it + * is ignored and never reported back. */ + assert (detect (ltr, RAQM_DIRECTION_TTB) == RAQM_DIRECTION_LTR); + assert (detect (rtl, RAQM_DIRECTION_TTB) == RAQM_DIRECTION_RTL); + assert (detect (neutral, RAQM_DIRECTION_TTB) == RAQM_DIRECTION_DEFAULT); + + /* The direction is read from the text content whichever setter was used. */ + { + /* UTF-16: Arabic ARE (U+0639 U+0631 U+0628 U+064A), all in the BMP. */ + const uint16_t utf16_rtl[] = { 0x0639, 0x0631, 0x0628, 0x064A }; + /* UTF-32: an astral strong-LTR character (U+1D400, outside the BMP). */ + const uint32_t utf32_ltr[] = { 0x1D400 }; + raqm_t *rq; + + rq = raqm_create (); + assert (rq); + assert (raqm_set_text_utf16 (rq, utf16_rtl, 4)); + assert (raqm_get_par_detected_direction (rq) == RAQM_DIRECTION_RTL); + raqm_destroy (rq); + + rq = raqm_create (); + assert (rq); + assert (raqm_set_text (rq, utf32_ltr, 1)); + assert (raqm_get_par_detected_direction (rq) == RAQM_DIRECTION_LTR); + raqm_destroy (rq); + } + + /* Text that was never set, explicitly empty text, or a NULL handle is all + * direction neutral. */ + { + raqm_t *rq = raqm_create (); + assert (rq); + assert (raqm_get_par_detected_direction (rq) == RAQM_DIRECTION_DEFAULT); + assert (raqm_set_text_utf8 (rq, "", 0)); + assert (raqm_get_par_detected_direction (rq) == RAQM_DIRECTION_DEFAULT); + raqm_destroy (rq); + } + assert (raqm_get_par_detected_direction (NULL) == RAQM_DIRECTION_DEFAULT); + + return 0; +} diff --git a/tests/meson.build b/tests/meson.build index c6b35b8..dc31d2e 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -78,6 +78,21 @@ test( args: [files('GraphemeBreakTest.txt')], ) +detected_direction_test = executable( + 'detected-direction-test', + 'detected-direction-test.c', + include_directories: include_directories(['../src']), + link_with: libraqm_test, + override_options: ['b_ndebug=false'], + dependencies: deps, + install: false, +) + +test( + 'detected-direction', + detected_direction_test, +) + foreach filename : tests testname = filename.split('.')[0]