2081 lines
50 KiB
C
2081 lines
50 KiB
C
/*
|
||
* Copyright © 2015 Information Technology Authority (ITA) <foss@ita.gov.om>
|
||
* Copyright © 2016 Khaled Hosny <khaledhosny@eglug.org>
|
||
*
|
||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
* of this software and associated documentation files (the "Software"), to
|
||
* deal in the Software without restriction, including without limitation the
|
||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||
* sell copies of the Software, and to permit persons to whom the Software is
|
||
* furnished to do so, subject to the following conditions:
|
||
*
|
||
* The above copyright notice and this permission notice shall be included in
|
||
* all copies or substantial portions of the Software.
|
||
*
|
||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||
* IN THE SOFTWARE.
|
||
*
|
||
*/
|
||
|
||
#ifdef HAVE_CONFIG_H
|
||
#include "config.h"
|
||
#undef HAVE_CONFIG_H // Workaround for Fribidi 1.0.5 and earlier
|
||
#endif
|
||
|
||
#include <assert.h>
|
||
#include <string.h>
|
||
|
||
#include <fribidi/fribidi.h>
|
||
#include <harfbuzz/hb.h>
|
||
#include <harfbuzz/hb-ft.h>
|
||
|
||
#include "raqm.h"
|
||
|
||
#if FRIBIDI_MAJOR_VERSION >= 1
|
||
#define USE_FRIBIDI_EX_API
|
||
#endif
|
||
|
||
/**
|
||
* SECTION:raqm
|
||
* @title: Raqm
|
||
* @short_description: A library for complex text layout
|
||
* @include: raqm.h
|
||
*
|
||
* Raqm is a light weight text layout library with strong emphasis on
|
||
* supporting languages and writing systems that require complex text layout.
|
||
*
|
||
* The main object in Raqm API is #raqm_t, it stores all the states of the
|
||
* input text, its properties, and the output of the layout process.
|
||
*
|
||
* To start, you create a #raqm_t object, add text and font(s) to it, run the
|
||
* layout process, and finally query about the output. For example:
|
||
*
|
||
* |[<!-- language="C" -->
|
||
* #include "raqm.h"
|
||
*
|
||
* int
|
||
* main (int argc, char *argv[])
|
||
* {
|
||
* const char *fontfile;
|
||
* const char *text;
|
||
* const char *direction;
|
||
* const char *language;
|
||
* int ret = 1;
|
||
*
|
||
* FT_Library library = NULL;
|
||
* FT_Face face = NULL;
|
||
*
|
||
* if (argc < 5)
|
||
* {
|
||
* printf ("Usage: %s FONT_FILE TEXT DIRECTION LANG\n", argv[0]);
|
||
* return 1;
|
||
* }
|
||
*
|
||
* fontfile = argv[1];
|
||
* text = argv[2];
|
||
* direction = argv[3];
|
||
* language = argv[4];
|
||
*
|
||
* if (FT_Init_FreeType (&library) == 0)
|
||
* {
|
||
* if (FT_New_Face (library, fontfile, 0, &face) == 0)
|
||
* {
|
||
* if (FT_Set_Char_Size (face, face->units_per_EM, 0, 0, 0) == 0)
|
||
* {
|
||
* raqm_t *rq = raqm_create ();
|
||
* if (rq != NULL)
|
||
* {
|
||
* raqm_direction_t dir = RAQM_DIRECTION_DEFAULT;
|
||
*
|
||
* if (strcmp (direction, "r") == 0)
|
||
* dir = RAQM_DIRECTION_RTL;
|
||
* else if (strcmp (direction, "l") == 0)
|
||
* dir = RAQM_DIRECTION_LTR;
|
||
*
|
||
* if (raqm_set_text_utf8 (rq, text, strlen (text)) &&
|
||
* raqm_set_freetype_face (rq, face) &&
|
||
* raqm_set_par_direction (rq, dir) &&
|
||
* raqm_set_language (rq, language, 0, strlen (text)) &&
|
||
* raqm_layout (rq))
|
||
* {
|
||
* size_t count, i;
|
||
* raqm_glyph_t *glyphs = raqm_get_glyphs (rq, &count);
|
||
*
|
||
* ret = !(glyphs != NULL || count == 0);
|
||
*
|
||
* printf("glyph count: %zu\n", count);
|
||
* for (i = 0; i < count; i++)
|
||
* {
|
||
* printf ("gid#%d off: (%d, %d) adv: (%d, %d) idx: %d\n",
|
||
* glyphs[i].index,
|
||
* glyphs[i].x_offset,
|
||
* glyphs[i].y_offset,
|
||
* glyphs[i].x_advance,
|
||
* glyphs[i].y_advance,
|
||
* glyphs[i].cluster);
|
||
* }
|
||
* }
|
||
*
|
||
* raqm_destroy (rq);
|
||
* }
|
||
* }
|
||
*
|
||
* FT_Done_Face (face);
|
||
* }
|
||
*
|
||
* FT_Done_FreeType (library);
|
||
* }
|
||
*
|
||
* return ret;
|
||
* }
|
||
* ]|
|
||
* To compile this example:
|
||
* |[<prompt>
|
||
* cc -o test test.c `pkg-config --libs --cflags raqm`
|
||
* ]|
|
||
*/
|
||
|
||
/* For enabling debug mode */
|
||
/*#define RAQM_DEBUG 1*/
|
||
#ifdef RAQM_DEBUG
|
||
#define RAQM_DBG(...) fprintf (stderr, __VA_ARGS__)
|
||
#else
|
||
#define RAQM_DBG(...)
|
||
#endif
|
||
|
||
#ifdef RAQM_TESTING
|
||
# define RAQM_TEST(...) printf (__VA_ARGS__)
|
||
# define SCRIPT_TO_STRING(script) \
|
||
char buff[5]; \
|
||
hb_tag_to_string (hb_script_to_iso15924_tag (script), buff); \
|
||
buff[4] = '\0';
|
||
#else
|
||
# define RAQM_TEST(...)
|
||
#endif
|
||
|
||
typedef enum {
|
||
RAQM_FLAG_NONE = 0,
|
||
RAQM_FLAG_UTF8 = 1 << 0
|
||
} _raqm_flags_t;
|
||
|
||
typedef struct {
|
||
FT_Face ftface;
|
||
hb_language_t lang;
|
||
hb_script_t script;
|
||
} _raqm_text_info;
|
||
|
||
typedef struct _raqm_run raqm_run_t;
|
||
|
||
struct _raqm {
|
||
int ref_count;
|
||
|
||
uint32_t *text;
|
||
char *text_utf8;
|
||
size_t text_len;
|
||
|
||
_raqm_text_info *text_info;
|
||
|
||
raqm_direction_t base_dir;
|
||
raqm_direction_t resolved_dir;
|
||
|
||
hb_feature_t *features;
|
||
size_t features_len;
|
||
|
||
raqm_run_t *runs;
|
||
raqm_glyph_t *glyphs;
|
||
|
||
_raqm_flags_t flags;
|
||
|
||
int ft_loadflags;
|
||
int invisible_glyph;
|
||
};
|
||
|
||
struct _raqm_run {
|
||
int pos;
|
||
int len;
|
||
|
||
hb_direction_t direction;
|
||
hb_script_t script;
|
||
hb_font_t *font;
|
||
hb_buffer_t *buffer;
|
||
|
||
raqm_run_t *next;
|
||
};
|
||
|
||
static uint32_t
|
||
_raqm_u8_to_u32_index (raqm_t *rq,
|
||
uint32_t index);
|
||
|
||
static bool
|
||
_raqm_init_text_info (raqm_t *rq)
|
||
{
|
||
hb_language_t default_lang;
|
||
|
||
if (rq->text_info)
|
||
return true;
|
||
|
||
rq->text_info = malloc (sizeof (_raqm_text_info) * rq->text_len);
|
||
if (!rq->text_info)
|
||
return false;
|
||
|
||
default_lang = hb_language_get_default ();
|
||
for (size_t i = 0; i < rq->text_len; i++)
|
||
{
|
||
rq->text_info[i].ftface = NULL;
|
||
rq->text_info[i].lang = default_lang;
|
||
rq->text_info[i].script = HB_SCRIPT_INVALID;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
static void
|
||
_raqm_free_text_info (raqm_t *rq)
|
||
{
|
||
if (!rq->text_info)
|
||
return;
|
||
|
||
for (size_t i = 0; i < rq->text_len; i++)
|
||
{
|
||
if (rq->text_info[i].ftface)
|
||
FT_Done_Face (rq->text_info[i].ftface);
|
||
}
|
||
|
||
free (rq->text_info);
|
||
rq->text_info = NULL;
|
||
}
|
||
|
||
static bool
|
||
_raqm_compare_text_info (_raqm_text_info a,
|
||
_raqm_text_info b)
|
||
{
|
||
if (a.ftface != b.ftface)
|
||
return false;
|
||
|
||
if (a.lang != b.lang)
|
||
return false;
|
||
|
||
if (a.script != b.script)
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* raqm_create:
|
||
*
|
||
* Creates a new #raqm_t with all its internal states initialized to their
|
||
* defaults.
|
||
*
|
||
* Return value:
|
||
* A newly allocated #raqm_t with a reference count of 1. The initial reference
|
||
* count should be released with raqm_destroy() when you are done using the
|
||
* #raqm_t. Returns %NULL in case of error.
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
raqm_t *
|
||
raqm_create (void)
|
||
{
|
||
raqm_t *rq;
|
||
|
||
rq = malloc (sizeof (raqm_t));
|
||
if (!rq)
|
||
return NULL;
|
||
|
||
rq->ref_count = 1;
|
||
|
||
rq->text = NULL;
|
||
rq->text_utf8 = NULL;
|
||
rq->text_len = 0;
|
||
|
||
rq->text_info = NULL;
|
||
|
||
rq->base_dir = RAQM_DIRECTION_DEFAULT;
|
||
rq->resolved_dir = RAQM_DIRECTION_DEFAULT;
|
||
|
||
rq->features = NULL;
|
||
rq->features_len = 0;
|
||
|
||
rq->runs = NULL;
|
||
rq->glyphs = NULL;
|
||
|
||
rq->flags = RAQM_FLAG_NONE;
|
||
|
||
rq->ft_loadflags = -1;
|
||
rq->invisible_glyph = 0;
|
||
|
||
return rq;
|
||
}
|
||
|
||
/**
|
||
* raqm_reference:
|
||
* @rq: a #raqm_t.
|
||
*
|
||
* Increases the reference count on @rq by one. This prevents @rq from being
|
||
* destroyed until a matching call to raqm_destroy() is made.
|
||
*
|
||
* Return value:
|
||
* The referenced #raqm_t.
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
raqm_t *
|
||
raqm_reference (raqm_t *rq)
|
||
{
|
||
if (rq)
|
||
rq->ref_count++;
|
||
|
||
return rq;
|
||
}
|
||
|
||
static void
|
||
_raqm_free_runs (raqm_t *rq)
|
||
{
|
||
raqm_run_t *runs = rq->runs;
|
||
while (runs)
|
||
{
|
||
raqm_run_t *run = runs;
|
||
runs = runs->next;
|
||
|
||
hb_buffer_destroy (run->buffer);
|
||
hb_font_destroy (run->font);
|
||
free (run);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* raqm_destroy:
|
||
* @rq: a #raqm_t.
|
||
*
|
||
* Decreases the reference count on @rq by one. If the result is zero, then @rq
|
||
* and all associated resources are freed.
|
||
* See cairo_reference().
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
void
|
||
raqm_destroy (raqm_t *rq)
|
||
{
|
||
if (!rq || --rq->ref_count != 0)
|
||
return;
|
||
|
||
free (rq->text);
|
||
free (rq->text_utf8);
|
||
_raqm_free_text_info (rq);
|
||
_raqm_free_runs (rq);
|
||
free (rq->glyphs);
|
||
free (rq);
|
||
}
|
||
|
||
/**
|
||
* raqm_set_text:
|
||
* @rq: a #raqm_t.
|
||
* @text: a UTF-32 encoded text string.
|
||
* @len: the length of @text.
|
||
*
|
||
* Adds @text to @rq to be used for layout. It must be a valid UTF-32 text, any
|
||
* invalid character will be replaced with U+FFFD. The text should typically
|
||
* represent a full paragraph, since doing the layout of chunks of text
|
||
* separately can give improper output.
|
||
*
|
||
* Return value:
|
||
* %true if no errors happened, %false otherwise.
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
bool
|
||
raqm_set_text (raqm_t *rq,
|
||
const uint32_t *text,
|
||
size_t len)
|
||
{
|
||
if (!rq || !text)
|
||
return false;
|
||
|
||
rq->text_len = len;
|
||
|
||
/* Empty string, don’t fail but do nothing */
|
||
if (!len)
|
||
return true;
|
||
|
||
free (rq->text);
|
||
|
||
rq->text = malloc (sizeof (uint32_t) * rq->text_len);
|
||
if (!rq->text)
|
||
return false;
|
||
|
||
_raqm_free_text_info (rq);
|
||
if (!_raqm_init_text_info (rq))
|
||
return false;
|
||
|
||
memcpy (rq->text, text, sizeof (uint32_t) * rq->text_len);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* raqm_set_text_utf8:
|
||
* @rq: a #raqm_t.
|
||
* @text: a UTF-8 encoded text string.
|
||
* @len: the length of @text.
|
||
*
|
||
* Same as raqm_set_text(), but for text encoded in UTF-8 encoding.
|
||
*
|
||
* Return value:
|
||
* %true if no errors happened, %false otherwise.
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
bool
|
||
raqm_set_text_utf8 (raqm_t *rq,
|
||
const char *text,
|
||
size_t len)
|
||
{
|
||
uint32_t *unicode;
|
||
size_t ulen;
|
||
bool ok;
|
||
|
||
if (!rq || !text)
|
||
return false;
|
||
|
||
/* Empty string, don’t fail but do nothing */
|
||
if (!len)
|
||
{
|
||
rq->text_len = len;
|
||
return true;
|
||
}
|
||
|
||
RAQM_TEST ("Text is: %s\n", text);
|
||
|
||
rq->flags |= RAQM_FLAG_UTF8;
|
||
|
||
rq->text_utf8 = malloc (sizeof (char) * len);
|
||
if (!rq->text_utf8)
|
||
return false;
|
||
|
||
unicode = malloc (sizeof (uint32_t) * len);
|
||
if (!unicode)
|
||
return false;
|
||
|
||
memcpy (rq->text_utf8, text, sizeof (char) * len);
|
||
|
||
ulen = fribidi_charset_to_unicode (FRIBIDI_CHAR_SET_UTF8,
|
||
text, len, unicode);
|
||
|
||
ok = raqm_set_text (rq, unicode, ulen);
|
||
|
||
free (unicode);
|
||
return ok;
|
||
}
|
||
|
||
/**
|
||
* raqm_set_par_direction:
|
||
* @rq: a #raqm_t.
|
||
* @dir: the direction of the paragraph.
|
||
*
|
||
* Sets the paragraph direction, also known as block direction in CSS. For
|
||
* horizontal text, this controls the overall direction in the Unicode
|
||
* Bidirectional Algorithm, so when the text is mainly right-to-left (with or
|
||
* without some left-to-right) text, then the base direction should be set to
|
||
* #RAQM_DIRECTION_RTL and vice versa.
|
||
*
|
||
* The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph
|
||
* direction based on the first character with strong bidi type (see [rule
|
||
* P2](http://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm),
|
||
* which can be good enough for many cases but has problems when a mainly
|
||
* right-to-left paragraph starts with a left-to-right character and vice versa
|
||
* as the detected paragraph direction will be the wrong one, or when text does
|
||
* not contain any characters with string bidi types (e.g. only punctuation or
|
||
* numbers) as this will default to left-to-right paragraph direction.
|
||
*
|
||
* For vertical, top-to-bottom text, #RAQM_DIRECTION_TTB should be used. Raqm,
|
||
* however, provides limited vertical text support and does not handle rotated
|
||
* horizontal text in vertical text, instead everything is treated as vertical
|
||
* text.
|
||
*
|
||
* Return value:
|
||
* %true if no errors happened, %false otherwise.
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
bool
|
||
raqm_set_par_direction (raqm_t *rq,
|
||
raqm_direction_t dir)
|
||
{
|
||
if (!rq)
|
||
return false;
|
||
|
||
rq->base_dir = dir;
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* raqm_set_language:
|
||
* @rq: a #raqm_t.
|
||
* @lang: a BCP47 language code.
|
||
* @start: index of first character that should use @face.
|
||
* @len: number of characters using @face.
|
||
*
|
||
* Sets a [BCP47 language
|
||
* code](https://www.w3.org/International/articles/language-tags/) to be used
|
||
* for @len-number of characters staring at @start. The @start and @len are
|
||
* input string array indices (i.e. counting bytes in UTF-8 and scaler values
|
||
* in UTF-32).
|
||
*
|
||
* This method can be used repeatedly to set different languages for different
|
||
* parts of the text.
|
||
*
|
||
* Return value:
|
||
* %true if no errors happened, %false otherwise.
|
||
*
|
||
* Stability:
|
||
* Unstable
|
||
*
|
||
* Since: 0.2
|
||
*/
|
||
bool
|
||
raqm_set_language (raqm_t *rq,
|
||
const char *lang,
|
||
size_t start,
|
||
size_t len)
|
||
{
|
||
hb_language_t language;
|
||
size_t end = start + len;
|
||
|
||
if (!rq)
|
||
return false;
|
||
|
||
if (!rq->text_len)
|
||
return true;
|
||
|
||
if (rq->flags & RAQM_FLAG_UTF8)
|
||
{
|
||
start = _raqm_u8_to_u32_index (rq, start);
|
||
end = _raqm_u8_to_u32_index (rq, end);
|
||
}
|
||
|
||
if (start >= rq->text_len || end > rq->text_len)
|
||
return false;
|
||
|
||
if (!rq->text_info)
|
||
return false;
|
||
|
||
language = hb_language_from_string (lang, -1);
|
||
for (size_t i = start; i < end; i++)
|
||
{
|
||
rq->text_info[i].lang = language;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* raqm_add_font_feature:
|
||
* @rq: a #raqm_t.
|
||
* @feature: (transfer none): a font feature string.
|
||
* @len: length of @feature, -1 for %NULL-terminated.
|
||
*
|
||
* Adds a font feature to be used by the #raqm_t during text layout. This is
|
||
* usually used to turn on optional font features that are not enabled by
|
||
* default, for example `dlig` or `ss01`, but can be also used to turn off
|
||
* default font features.
|
||
*
|
||
* @feature is string representing a single font feature, in the syntax
|
||
* understood by hb_feature_from_string().
|
||
*
|
||
* This function can be called repeatedly, new features will be appended to the
|
||
* end of the features list and can potentially override previous features.
|
||
*
|
||
* Return value:
|
||
* %true if parsing @feature succeeded, %false otherwise.
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
bool
|
||
raqm_add_font_feature (raqm_t *rq,
|
||
const char *feature,
|
||
int len)
|
||
{
|
||
hb_bool_t ok;
|
||
hb_feature_t fea;
|
||
|
||
if (!rq)
|
||
return false;
|
||
|
||
ok = hb_feature_from_string (feature, len, &fea);
|
||
if (ok)
|
||
{
|
||
rq->features_len++;
|
||
rq->features = realloc (rq->features,
|
||
sizeof (hb_feature_t) * (rq->features_len));
|
||
if (!rq->features)
|
||
return false;
|
||
|
||
rq->features[rq->features_len - 1] = fea;
|
||
}
|
||
|
||
return ok;
|
||
}
|
||
|
||
static hb_font_t *
|
||
_raqm_create_hb_font (raqm_t *rq,
|
||
FT_Face face)
|
||
{
|
||
hb_font_t *font;
|
||
|
||
#ifdef HAVE_HB_FT_FONT_CREATE_REFERENCED
|
||
font = hb_ft_font_create_referenced (face);
|
||
#else
|
||
FT_Reference_Face (face);
|
||
font = hb_ft_font_create (face, (hb_destroy_func_t) FT_Done_Face);
|
||
#endif
|
||
|
||
#ifdef HAVE_HB_FT_FONT_SET_LOAD_FLAGS
|
||
if (rq->ft_loadflags >= 0)
|
||
hb_ft_font_set_load_flags (font, rq->ft_loadflags);
|
||
#else
|
||
(void)rq;
|
||
#endif
|
||
|
||
return font;
|
||
}
|
||
|
||
static bool
|
||
_raqm_set_freetype_face (raqm_t *rq,
|
||
FT_Face face,
|
||
size_t start,
|
||
size_t end)
|
||
{
|
||
if (!rq)
|
||
return false;
|
||
|
||
if (!rq->text_len)
|
||
return true;
|
||
|
||
if (start >= rq->text_len || end > rq->text_len)
|
||
return false;
|
||
|
||
if (!rq->text_info)
|
||
return false;
|
||
|
||
for (size_t i = start; i < end; i++)
|
||
{
|
||
if (rq->text_info[i].ftface)
|
||
FT_Done_Face (rq->text_info[i].ftface);
|
||
rq->text_info[i].ftface = face;
|
||
FT_Reference_Face (face);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* raqm_set_freetype_face:
|
||
* @rq: a #raqm_t.
|
||
* @face: an #FT_Face.
|
||
*
|
||
* Sets an #FT_Face to be used for all characters in @rq.
|
||
*
|
||
* See also raqm_set_freetype_face_range().
|
||
*
|
||
* Return value:
|
||
* %true if no errors happened, %false otherwise.
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
bool
|
||
raqm_set_freetype_face (raqm_t *rq,
|
||
FT_Face face)
|
||
{
|
||
return _raqm_set_freetype_face (rq, face, 0, rq->text_len);
|
||
}
|
||
|
||
/**
|
||
* raqm_set_freetype_face_range:
|
||
* @rq: a #raqm_t.
|
||
* @face: an #FT_Face.
|
||
* @start: index of first character that should use @face.
|
||
* @len: number of characters using @face.
|
||
*
|
||
* Sets an #FT_Face to be used for @len-number of characters staring at @start.
|
||
* The @start and @len are input string array indices (i.e. counting bytes in
|
||
* UTF-8 and scaler values in UTF-32).
|
||
*
|
||
* This method can be used repeatedly to set different faces for different
|
||
* parts of the text. It is the responsibility of the client to make sure that
|
||
* face ranges cover the whole text.
|
||
*
|
||
* See also raqm_set_freetype_face().
|
||
*
|
||
* Return value:
|
||
* %true if no errors happened, %false otherwise.
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
bool
|
||
raqm_set_freetype_face_range (raqm_t *rq,
|
||
FT_Face face,
|
||
size_t start,
|
||
size_t len)
|
||
{
|
||
size_t end = start + len;
|
||
|
||
if (!rq)
|
||
return false;
|
||
|
||
if (!rq->text_len)
|
||
return true;
|
||
|
||
if (rq->flags & RAQM_FLAG_UTF8)
|
||
{
|
||
start = _raqm_u8_to_u32_index (rq, start);
|
||
end = _raqm_u8_to_u32_index (rq, end);
|
||
}
|
||
|
||
return _raqm_set_freetype_face (rq, face, start, end);
|
||
}
|
||
|
||
/**
|
||
* raqm_set_freetype_load_flags:
|
||
* @rq: a #raqm_t.
|
||
* @flags: FreeType load flags.
|
||
*
|
||
* Sets the load flags passed to FreeType when loading glyphs, should be the
|
||
* same flags used by the client when rendering FreeType glyphs.
|
||
*
|
||
* This requires version of HarfBuzz that has hb_ft_font_set_load_flags(), for
|
||
* older version the flags will be ignored.
|
||
*
|
||
* Return value:
|
||
* %true if no errors happened, %false otherwise.
|
||
*
|
||
* Since: 0.3
|
||
*/
|
||
bool
|
||
raqm_set_freetype_load_flags (raqm_t *rq,
|
||
int flags)
|
||
{
|
||
if (!rq)
|
||
return false;
|
||
|
||
rq->ft_loadflags = flags;
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* raqm_set_invisible_glyph:
|
||
* @rq: a #raqm_t.
|
||
* @gid: glyph id to use for invisible glyphs.
|
||
*
|
||
* Sets the glyph id to be used for invisible glyhphs.
|
||
*
|
||
* If @gid is negative, invisible glyphs will be suppressed from the output.
|
||
* This requires HarfBuzz 1.8.0 or later. If raqm is used with an earlier
|
||
* HarfBuzz version, the return value will be %false and the shaping behavior
|
||
* does not change.
|
||
*
|
||
* If @gid is zero, invisible glyphs will be rendered as space.
|
||
* This works on all versions of HarfBuzz.
|
||
*
|
||
* If @gid is a positive number, it will be used for invisible glyphs.
|
||
* This requires a version of HarfBuzz that has
|
||
* hb_buffer_set_invisible_glyph(). For older versions, the return value
|
||
* will be %false and the shaping behavior does not change.
|
||
*
|
||
* Return value:
|
||
* %true if no errors happened, %false otherwise.
|
||
*
|
||
* Since: 0.6
|
||
*/
|
||
bool
|
||
raqm_set_invisible_glyph (raqm_t *rq,
|
||
int gid)
|
||
{
|
||
if (!rq)
|
||
return false;
|
||
|
||
#ifndef HAVE_HB_BUFFER_SET_INVISIBLE_GLYPH
|
||
if (gid > 0)
|
||
return false;
|
||
#endif
|
||
|
||
#if !defined(HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES) || \
|
||
!HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES
|
||
if (gid < 0)
|
||
return false;
|
||
#endif
|
||
|
||
rq->invisible_glyph = gid;
|
||
return true;
|
||
}
|
||
|
||
static bool
|
||
_raqm_itemize (raqm_t *rq);
|
||
|
||
static bool
|
||
_raqm_shape (raqm_t *rq);
|
||
|
||
/**
|
||
* raqm_layout:
|
||
* @rq: a #raqm_t.
|
||
*
|
||
* Run the text layout process on @rq. This is the main Raqm function where the
|
||
* Unicode Bidirectional Text algorithm will be applied to the text in @rq,
|
||
* text shaping, and any other part of the layout process.
|
||
*
|
||
* Return value:
|
||
* %true if the layout process was successful, %false otherwise.
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
bool
|
||
raqm_layout (raqm_t *rq)
|
||
{
|
||
if (!rq)
|
||
return false;
|
||
|
||
if (!rq->text_len)
|
||
return true;
|
||
|
||
if (!rq->text_info)
|
||
return false;
|
||
|
||
for (size_t i = 0; i < rq->text_len; i++)
|
||
{
|
||
if (!rq->text_info[i].ftface)
|
||
return false;
|
||
}
|
||
|
||
if (!_raqm_itemize (rq))
|
||
return false;
|
||
|
||
if (!_raqm_shape (rq))
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
static uint32_t
|
||
_raqm_u32_to_u8_index (raqm_t *rq,
|
||
uint32_t index);
|
||
|
||
/**
|
||
* raqm_get_glyphs:
|
||
* @rq: a #raqm_t.
|
||
* @length: (out): output array length.
|
||
*
|
||
* Gets the final result of Raqm layout process, an array of #raqm_glyph_t
|
||
* containing the glyph indices in the font, their positions and other possible
|
||
* information.
|
||
*
|
||
* Return value: (transfer none):
|
||
* An array of #raqm_glyph_t, or %NULL in case of error. This is owned by @rq
|
||
* and must not be freed.
|
||
*
|
||
* Since: 0.1
|
||
*/
|
||
raqm_glyph_t *
|
||
raqm_get_glyphs (raqm_t *rq,
|
||
size_t *length)
|
||
{
|
||
size_t count = 0;
|
||
|
||
if (!rq || !rq->runs || !length)
|
||
{
|
||
if (length)
|
||
*length = 0;
|
||
return NULL;
|
||
}
|
||
|
||
for (raqm_run_t *run = rq->runs; run != NULL; run = run->next)
|
||
count += hb_buffer_get_length (run->buffer);
|
||
|
||
*length = count;
|
||
|
||
if (rq->glyphs)
|
||
free (rq->glyphs);
|
||
|
||
rq->glyphs = malloc (sizeof (raqm_glyph_t) * count);
|
||
if (!rq->glyphs)
|
||
{
|
||
*length = 0;
|
||
return NULL;
|
||
}
|
||
|
||
RAQM_TEST ("Glyph information:\n");
|
||
|
||
count = 0;
|
||
for (raqm_run_t *run = rq->runs; run != NULL; run = run->next)
|
||
{
|
||
size_t len;
|
||
hb_glyph_info_t *info;
|
||
hb_glyph_position_t *position;
|
||
|
||
len = hb_buffer_get_length (run->buffer);
|
||
info = hb_buffer_get_glyph_infos (run->buffer, NULL);
|
||
position = hb_buffer_get_glyph_positions (run->buffer, NULL);
|
||
|
||
for (size_t i = 0; i < len; i++)
|
||
{
|
||
rq->glyphs[count + i].index = info[i].codepoint;
|
||
rq->glyphs[count + i].cluster = info[i].cluster;
|
||
rq->glyphs[count + i].x_advance = position[i].x_advance;
|
||
rq->glyphs[count + i].y_advance = position[i].y_advance;
|
||
rq->glyphs[count + i].x_offset = position[i].x_offset;
|
||
rq->glyphs[count + i].y_offset = position[i].y_offset;
|
||
rq->glyphs[count + i].ftface = rq->text_info[info[i].cluster].ftface;
|
||
|
||
RAQM_TEST ("glyph [%d]\tx_offset: %d\ty_offset: %d\tx_advance: %d\tfont: %s\n",
|
||
rq->glyphs[count + i].index, rq->glyphs[count + i].x_offset,
|
||
rq->glyphs[count + i].y_offset, rq->glyphs[count + i].x_advance,
|
||
rq->glyphs[count + i].ftface->family_name);
|
||
}
|
||
|
||
count += len;
|
||
}
|
||
|
||
if (rq->flags & RAQM_FLAG_UTF8)
|
||
{
|
||
#ifdef RAQM_TESTING
|
||
RAQM_TEST ("\nUTF-32 clusters:");
|
||
for (size_t i = 0; i < count; i++)
|
||
RAQM_TEST (" %02d", rq->glyphs[i].cluster);
|
||
RAQM_TEST ("\n");
|
||
#endif
|
||
|
||
for (size_t i = 0; i < count; i++)
|
||
rq->glyphs[i].cluster = _raqm_u32_to_u8_index (rq,
|
||
rq->glyphs[i].cluster);
|
||
|
||
#ifdef RAQM_TESTING
|
||
RAQM_TEST ("UTF-8 clusters: ");
|
||
for (size_t i = 0; i < count; i++)
|
||
RAQM_TEST (" %02d", rq->glyphs[i].cluster);
|
||
RAQM_TEST ("\n");
|
||
#endif
|
||
}
|
||
return rq->glyphs;
|
||
}
|
||
|
||
static bool
|
||
_raqm_resolve_scripts (raqm_t *rq);
|
||
|
||
static hb_direction_t
|
||
_raqm_hb_dir (raqm_t *rq, FriBidiLevel level)
|
||
{
|
||
hb_direction_t dir = HB_DIRECTION_LTR;
|
||
|
||
if (rq->base_dir == RAQM_DIRECTION_TTB)
|
||
dir = HB_DIRECTION_TTB;
|
||
else if (FRIBIDI_LEVEL_IS_RTL (level))
|
||
dir = HB_DIRECTION_RTL;
|
||
|
||
return dir;
|
||
}
|
||
|
||
typedef struct {
|
||
size_t pos;
|
||
size_t len;
|
||
FriBidiLevel level;
|
||
} _raqm_bidi_run;
|
||
|
||
static void
|
||
_raqm_reverse_run (_raqm_bidi_run *run, const size_t len)
|
||
{
|
||
assert (run);
|
||
|
||
for (size_t i = 0; i < len / 2; i++)
|
||
{
|
||
_raqm_bidi_run temp = run[i];
|
||
run[i] = run[len - 1 - i];
|
||
run[len - 1 - i] = temp;
|
||
}
|
||
}
|
||
|
||
static _raqm_bidi_run *
|
||
_raqm_reorder_runs (const FriBidiCharType *types,
|
||
const size_t len,
|
||
const FriBidiParType base_dir,
|
||
/* input and output */
|
||
FriBidiLevel *levels,
|
||
/* output */
|
||
size_t *run_count)
|
||
{
|
||
FriBidiLevel level;
|
||
FriBidiLevel last_level = -1;
|
||
FriBidiLevel max_level = 0;
|
||
size_t run_start = 0;
|
||
size_t run_index = 0;
|
||
_raqm_bidi_run *runs = NULL;
|
||
size_t count = 0;
|
||
|
||
if (len == 0)
|
||
{
|
||
*run_count = 0;
|
||
return NULL;
|
||
}
|
||
|
||
assert (types);
|
||
assert (levels);
|
||
|
||
/* L1. Reset the embedding levels of some chars:
|
||
4. any sequence of white space characters at the end of the line. */
|
||
for (int i = len - 1;
|
||
i >= 0 && FRIBIDI_IS_EXPLICIT_OR_BN_OR_WS (types[i]); i--)
|
||
{
|
||
levels[i] = FRIBIDI_DIR_TO_LEVEL (base_dir);
|
||
}
|
||
|
||
/* Find max_level of the line. We don't reuse the paragraph
|
||
* max_level, both for a cleaner API, and that the line max_level
|
||
* may be far less than paragraph max_level. */
|
||
for (int i = len - 1; i >= 0; i--)
|
||
{
|
||
if (levels[i] > max_level)
|
||
max_level = levels[i];
|
||
}
|
||
|
||
for (size_t i = 0; i < len; i++)
|
||
{
|
||
if (levels[i] != last_level)
|
||
count++;
|
||
|
||
last_level = levels[i];
|
||
}
|
||
|
||
runs = malloc (sizeof (_raqm_bidi_run) * count);
|
||
|
||
while (run_start < len)
|
||
{
|
||
size_t run_end = run_start;
|
||
while (run_end < len && levels[run_start] == levels[run_end])
|
||
{
|
||
run_end++;
|
||
}
|
||
|
||
runs[run_index].pos = run_start;
|
||
runs[run_index].level = levels[run_start];
|
||
runs[run_index].len = run_end - run_start;
|
||
run_start = run_end;
|
||
run_index++;
|
||
}
|
||
|
||
/* L2. Reorder. */
|
||
for (level = max_level; level > 0; level--)
|
||
{
|
||
for (int i = count - 1; i >= 0; i--)
|
||
{
|
||
if (runs[i].level >= level)
|
||
{
|
||
int end = i;
|
||
for (i--; (i >= 0 && runs[i].level >= level); i--)
|
||
;
|
||
_raqm_reverse_run (runs + i + 1, end - i);
|
||
}
|
||
}
|
||
}
|
||
|
||
*run_count = count;
|
||
return runs;
|
||
}
|
||
|
||
static bool
|
||
_raqm_itemize (raqm_t *rq)
|
||
{
|
||
FriBidiParType par_type = FRIBIDI_PAR_ON;
|
||
FriBidiCharType *types;
|
||
#ifdef USE_FRIBIDI_EX_API
|
||
FriBidiBracketType *btypes;
|
||
#endif
|
||
FriBidiLevel *levels;
|
||
_raqm_bidi_run *runs = NULL;
|
||
raqm_run_t *last;
|
||
int max_level;
|
||
size_t run_count;
|
||
bool ok = true;
|
||
|
||
#ifdef RAQM_TESTING
|
||
switch (rq->base_dir)
|
||
{
|
||
case RAQM_DIRECTION_RTL:
|
||
RAQM_TEST ("Direction is: RTL\n\n");
|
||
break;
|
||
case RAQM_DIRECTION_LTR:
|
||
RAQM_TEST ("Direction is: LTR\n\n");
|
||
break;
|
||
case RAQM_DIRECTION_TTB:
|
||
RAQM_TEST ("Direction is: TTB\n\n");
|
||
break;
|
||
case RAQM_DIRECTION_DEFAULT:
|
||
default:
|
||
RAQM_TEST ("Direction is: DEFAULT\n\n");
|
||
break;
|
||
}
|
||
#endif
|
||
|
||
types = calloc (rq->text_len, sizeof (FriBidiCharType));
|
||
#ifdef USE_FRIBIDI_EX_API
|
||
btypes = calloc (rq->text_len, sizeof (FriBidiBracketType));
|
||
#endif
|
||
levels = calloc (rq->text_len, sizeof (FriBidiLevel));
|
||
if (!types || !levels
|
||
#ifdef USE_FRIBIDI_EX_API
|
||
|| !btypes
|
||
#endif
|
||
)
|
||
{
|
||
ok = false;
|
||
goto done;
|
||
}
|
||
|
||
if (rq->base_dir == RAQM_DIRECTION_RTL)
|
||
par_type = FRIBIDI_PAR_RTL;
|
||
else if (rq->base_dir == RAQM_DIRECTION_LTR)
|
||
par_type = FRIBIDI_PAR_LTR;
|
||
|
||
if (rq->base_dir == RAQM_DIRECTION_TTB)
|
||
{
|
||
/* Treat every thing as LTR in vertical text */
|
||
max_level = 1;
|
||
memset (types, FRIBIDI_TYPE_LTR, rq->text_len);
|
||
memset (levels, 0, rq->text_len);
|
||
rq->resolved_dir = RAQM_DIRECTION_LTR;
|
||
}
|
||
else
|
||
{
|
||
fribidi_get_bidi_types (rq->text, rq->text_len, types);
|
||
#ifdef USE_FRIBIDI_EX_API
|
||
fribidi_get_bracket_types (rq->text, rq->text_len, types, btypes);
|
||
max_level = fribidi_get_par_embedding_levels_ex (types, btypes,
|
||
rq->text_len, &par_type,
|
||
levels);
|
||
#else
|
||
max_level = fribidi_get_par_embedding_levels (types, rq->text_len,
|
||
&par_type, levels);
|
||
#endif
|
||
|
||
if (par_type == FRIBIDI_PAR_LTR)
|
||
rq->resolved_dir = RAQM_DIRECTION_LTR;
|
||
else
|
||
rq->resolved_dir = RAQM_DIRECTION_RTL;
|
||
}
|
||
|
||
if (max_level == 0)
|
||
{
|
||
ok = false;
|
||
goto done;
|
||
}
|
||
|
||
if (!_raqm_resolve_scripts (rq))
|
||
{
|
||
ok = false;
|
||
goto done;
|
||
}
|
||
|
||
/* Get the number of bidi runs */
|
||
runs = _raqm_reorder_runs (types, rq->text_len, par_type, levels, &run_count);
|
||
if (!runs)
|
||
{
|
||
ok = false;
|
||
goto done;
|
||
}
|
||
|
||
#ifdef RAQM_TESTING
|
||
RAQM_TEST ("Number of runs before script itemization: %zu\n\n", run_count);
|
||
|
||
RAQM_TEST ("Fribidi Runs:\n");
|
||
for (size_t i = 0; i < run_count; i++)
|
||
{
|
||
RAQM_TEST ("run[%zu]:\t start: %zu\tlength: %zu\tlevel: %d\n",
|
||
i, runs[i].pos, runs[i].len, runs[i].level);
|
||
}
|
||
RAQM_TEST ("\n");
|
||
#endif
|
||
|
||
last = NULL;
|
||
for (size_t i = 0; i < run_count; i++)
|
||
{
|
||
raqm_run_t *run = calloc (1, sizeof (raqm_run_t));
|
||
if (!run)
|
||
{
|
||
ok = false;
|
||
goto done;
|
||
}
|
||
|
||
if (!rq->runs)
|
||
rq->runs = run;
|
||
|
||
if (last)
|
||
last->next = run;
|
||
|
||
run->direction = _raqm_hb_dir (rq, runs[i].level);
|
||
|
||
if (HB_DIRECTION_IS_BACKWARD (run->direction))
|
||
{
|
||
run->pos = runs[i].pos + runs[i].len - 1;
|
||
run->script = rq->text_info[run->pos].script;
|
||
run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface);
|
||
for (int j = runs[i].len - 1; j >= 0; j--)
|
||
{
|
||
_raqm_text_info info = rq->text_info[runs[i].pos + j];
|
||
if (!_raqm_compare_text_info (rq->text_info[run->pos], info))
|
||
{
|
||
raqm_run_t *newrun = calloc (1, sizeof (raqm_run_t));
|
||
if (!newrun)
|
||
{
|
||
ok = false;
|
||
goto done;
|
||
}
|
||
newrun->pos = runs[i].pos + j;
|
||
newrun->len = 1;
|
||
newrun->direction = _raqm_hb_dir (rq, runs[i].level);
|
||
newrun->script = info.script;
|
||
newrun->font = _raqm_create_hb_font (rq, info.ftface);
|
||
run->next = newrun;
|
||
run = newrun;
|
||
}
|
||
else
|
||
{
|
||
run->len++;
|
||
run->pos = runs[i].pos + j;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
run->pos = runs[i].pos;
|
||
run->script = rq->text_info[run->pos].script;
|
||
run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface);
|
||
for (size_t j = 0; j < runs[i].len; j++)
|
||
{
|
||
_raqm_text_info info = rq->text_info[runs[i].pos + j];
|
||
if (!_raqm_compare_text_info (rq->text_info[run->pos], info))
|
||
{
|
||
raqm_run_t *newrun = calloc (1, sizeof (raqm_run_t));
|
||
if (!newrun)
|
||
{
|
||
ok = false;
|
||
goto done;
|
||
}
|
||
newrun->pos = runs[i].pos + j;
|
||
newrun->len = 1;
|
||
newrun->direction = _raqm_hb_dir (rq, runs[i].level);
|
||
newrun->script = info.script;
|
||
newrun->font = _raqm_create_hb_font (rq, info.ftface);
|
||
run->next = newrun;
|
||
run = newrun;
|
||
}
|
||
else
|
||
run->len++;
|
||
}
|
||
}
|
||
|
||
last = run;
|
||
last->next = NULL;
|
||
}
|
||
|
||
#ifdef RAQM_TESTING
|
||
run_count = 0;
|
||
for (raqm_run_t *run = rq->runs; run != NULL; run = run->next)
|
||
run_count++;
|
||
RAQM_TEST ("Number of runs after script itemization: %zu\n\n", run_count);
|
||
|
||
run_count = 0;
|
||
RAQM_TEST ("Final Runs:\n");
|
||
for (raqm_run_t *run = rq->runs; run != NULL; run = run->next)
|
||
{
|
||
SCRIPT_TO_STRING (run->script);
|
||
RAQM_TEST ("run[%zu]:\t start: %d\tlength: %d\tdirection: %s\tscript: %s\tfont: %s\n",
|
||
run_count++, run->pos, run->len,
|
||
hb_direction_to_string (run->direction), buff,
|
||
rq->text_info[run->pos].ftface->family_name);
|
||
}
|
||
RAQM_TEST ("\n");
|
||
#endif
|
||
|
||
done:
|
||
free (runs);
|
||
free (types);
|
||
#ifdef USE_FRIBIDI_EX_API
|
||
free (btypes);
|
||
#endif
|
||
free (levels);
|
||
|
||
return ok;
|
||
}
|
||
|
||
/* Stack to handle script detection */
|
||
typedef struct {
|
||
size_t capacity;
|
||
size_t size;
|
||
int *pair_index;
|
||
hb_script_t *script;
|
||
} _raqm_stack_t;
|
||
|
||
/* Special paired characters for script detection */
|
||
static size_t paired_len = 34;
|
||
static const FriBidiChar paired_chars[] =
|
||
{
|
||
0x0028, 0x0029, /* ascii paired punctuation */
|
||
0x003c, 0x003e,
|
||
0x005b, 0x005d,
|
||
0x007b, 0x007d,
|
||
0x00ab, 0x00bb, /* guillemets */
|
||
0x2018, 0x2019, /* general punctuation */
|
||
0x201c, 0x201d,
|
||
0x2039, 0x203a,
|
||
0x3008, 0x3009, /* chinese paired punctuation */
|
||
0x300a, 0x300b,
|
||
0x300c, 0x300d,
|
||
0x300e, 0x300f,
|
||
0x3010, 0x3011,
|
||
0x3014, 0x3015,
|
||
0x3016, 0x3017,
|
||
0x3018, 0x3019,
|
||
0x301a, 0x301b
|
||
};
|
||
|
||
static void
|
||
_raqm_stack_free (_raqm_stack_t *stack)
|
||
{
|
||
free (stack->script);
|
||
free (stack->pair_index);
|
||
free (stack);
|
||
}
|
||
|
||
/* Stack handling functions */
|
||
static _raqm_stack_t *
|
||
_raqm_stack_new (size_t max)
|
||
{
|
||
_raqm_stack_t *stack;
|
||
stack = calloc (1, sizeof (_raqm_stack_t));
|
||
if (!stack)
|
||
return NULL;
|
||
|
||
stack->script = malloc (sizeof (hb_script_t) * max);
|
||
if (!stack->script)
|
||
{
|
||
_raqm_stack_free (stack);
|
||
return NULL;
|
||
}
|
||
|
||
stack->pair_index = malloc (sizeof (int) * max);
|
||
if (!stack->pair_index)
|
||
{
|
||
_raqm_stack_free (stack);
|
||
return NULL;
|
||
}
|
||
|
||
stack->size = 0;
|
||
stack->capacity = max;
|
||
|
||
return stack;
|
||
}
|
||
|
||
static bool
|
||
_raqm_stack_pop (_raqm_stack_t *stack)
|
||
{
|
||
if (!stack->size)
|
||
{
|
||
RAQM_DBG ("Stack is Empty\n");
|
||
return false;
|
||
}
|
||
|
||
stack->size--;
|
||
|
||
return true;
|
||
}
|
||
|
||
static hb_script_t
|
||
_raqm_stack_top (_raqm_stack_t *stack)
|
||
{
|
||
if (!stack->size)
|
||
{
|
||
RAQM_DBG ("Stack is Empty\n");
|
||
return HB_SCRIPT_INVALID; /* XXX: check this */
|
||
}
|
||
|
||
return stack->script[stack->size];
|
||
}
|
||
|
||
static bool
|
||
_raqm_stack_push (_raqm_stack_t *stack,
|
||
hb_script_t script,
|
||
int pair_index)
|
||
{
|
||
if (stack->size == stack->capacity)
|
||
{
|
||
RAQM_DBG ("Stack is Full\n");
|
||
return false;
|
||
}
|
||
|
||
stack->size++;
|
||
stack->script[stack->size] = script;
|
||
stack->pair_index[stack->size] = pair_index;
|
||
|
||
return true;
|
||
}
|
||
|
||
static int
|
||
_get_pair_index (const FriBidiChar ch)
|
||
{
|
||
int lower = 0;
|
||
int upper = paired_len - 1;
|
||
|
||
while (lower <= upper)
|
||
{
|
||
int mid = (lower + upper) / 2;
|
||
if (ch < paired_chars[mid])
|
||
upper = mid - 1;
|
||
else if (ch > paired_chars[mid])
|
||
lower = mid + 1;
|
||
else
|
||
return mid;
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
#define STACK_IS_EMPTY(script) ((script)->size <= 0)
|
||
#define IS_OPEN(pair_index) (((pair_index) & 1) == 0)
|
||
|
||
/* Resolve the script for each character in the input string, if the character
|
||
* script is common or inherited it takes the script of the character before it
|
||
* except paired characters which we try to make them use the same script. We
|
||
* then split the BiDi runs, if necessary, on script boundaries.
|
||
*/
|
||
static bool
|
||
_raqm_resolve_scripts (raqm_t *rq)
|
||
{
|
||
int last_script_index = -1;
|
||
int last_set_index = -1;
|
||
hb_script_t last_script = HB_SCRIPT_INVALID;
|
||
_raqm_stack_t *stack = NULL;
|
||
hb_unicode_funcs_t* unicode_funcs = hb_unicode_funcs_get_default ();
|
||
|
||
for (size_t i = 0; i < rq->text_len; ++i)
|
||
rq->text_info[i].script = hb_unicode_script (unicode_funcs, rq->text[i]);
|
||
|
||
#ifdef RAQM_TESTING
|
||
RAQM_TEST ("Before script detection:\n");
|
||
for (size_t i = 0; i < rq->text_len; ++i)
|
||
{
|
||
SCRIPT_TO_STRING (rq->text_info[i].script);
|
||
RAQM_TEST ("script for ch[%zu]\t%s\n", i, buff);
|
||
}
|
||
RAQM_TEST ("\n");
|
||
#endif
|
||
|
||
stack = _raqm_stack_new (rq->text_len);
|
||
if (!stack)
|
||
return false;
|
||
|
||
for (int i = 0; i < (int) rq->text_len; i++)
|
||
{
|
||
if (rq->text_info[i].script == HB_SCRIPT_COMMON && last_script_index != -1)
|
||
{
|
||
int pair_index = _get_pair_index (rq->text[i]);
|
||
if (pair_index >= 0)
|
||
{
|
||
if (IS_OPEN (pair_index))
|
||
{
|
||
/* is a paired character */
|
||
rq->text_info[i].script = last_script;
|
||
last_set_index = i;
|
||
_raqm_stack_push (stack, rq->text_info[i].script, pair_index);
|
||
}
|
||
else
|
||
{
|
||
/* is a close paired character */
|
||
/* find matching opening (by getting the last even index for current
|
||
* odd index) */
|
||
while (!STACK_IS_EMPTY (stack) &&
|
||
stack->pair_index[stack->size] != (pair_index & ~1))
|
||
{
|
||
_raqm_stack_pop (stack);
|
||
}
|
||
if (!STACK_IS_EMPTY (stack))
|
||
{
|
||
rq->text_info[i].script = _raqm_stack_top (stack);
|
||
last_script = rq->text_info[i].script;
|
||
last_set_index = i;
|
||
}
|
||
else
|
||
{
|
||
rq->text_info[i].script = last_script;
|
||
last_set_index = i;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
rq->text_info[i].script = last_script;
|
||
last_set_index = i;
|
||
}
|
||
}
|
||
else if (rq->text_info[i].script == HB_SCRIPT_INHERITED &&
|
||
last_script_index != -1)
|
||
{
|
||
rq->text_info[i].script = last_script;
|
||
last_set_index = i;
|
||
}
|
||
else
|
||
{
|
||
for (int j = last_set_index + 1; j < i; ++j)
|
||
rq->text_info[j].script = rq->text_info[i].script;
|
||
last_script = rq->text_info[i].script;
|
||
last_script_index = i;
|
||
last_set_index = i;
|
||
}
|
||
}
|
||
|
||
/* Loop backwards and change any remaining Common or Inherit characters to
|
||
* take the script if the next character.
|
||
* https://github.com/HOST-Oman/libraqm/issues/95
|
||
*/
|
||
for (int i = rq->text_len - 2; i >= 0; --i)
|
||
{
|
||
if (rq->text_info[i].script == HB_SCRIPT_INHERITED ||
|
||
rq->text_info[i].script == HB_SCRIPT_COMMON)
|
||
rq->text_info[i].script = rq->text_info[i + 1].script;
|
||
}
|
||
|
||
#ifdef RAQM_TESTING
|
||
RAQM_TEST ("After script detection:\n");
|
||
for (size_t i = 0; i < rq->text_len; ++i)
|
||
{
|
||
SCRIPT_TO_STRING (rq->text_info[i].script);
|
||
RAQM_TEST ("script for ch[%zu]\t%s\n", i, buff);
|
||
}
|
||
RAQM_TEST ("\n");
|
||
#endif
|
||
|
||
_raqm_stack_free (stack);
|
||
|
||
return true;
|
||
}
|
||
|
||
static bool
|
||
_raqm_shape (raqm_t *rq)
|
||
{
|
||
hb_buffer_flags_t hb_buffer_flags = HB_BUFFER_FLAG_BOT | HB_BUFFER_FLAG_EOT;
|
||
|
||
#if defined(HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES) && \
|
||
HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES
|
||
if (rq->invisible_glyph < 0)
|
||
hb_buffer_flags |= HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES;
|
||
#endif
|
||
|
||
for (raqm_run_t *run = rq->runs; run != NULL; run = run->next)
|
||
{
|
||
run->buffer = hb_buffer_create ();
|
||
|
||
hb_buffer_add_utf32 (run->buffer, rq->text, rq->text_len,
|
||
run->pos, run->len);
|
||
hb_buffer_set_script (run->buffer, run->script);
|
||
hb_buffer_set_language (run->buffer, rq->text_info[run->pos].lang);
|
||
hb_buffer_set_direction (run->buffer, run->direction);
|
||
hb_buffer_set_flags (run->buffer, hb_buffer_flags);
|
||
|
||
#ifdef HAVE_HB_BUFFER_SET_INVISIBLE_GLYPH
|
||
if (rq->invisible_glyph > 0)
|
||
hb_buffer_set_invisible_glyph (run->buffer, rq->invisible_glyph);
|
||
#endif
|
||
|
||
hb_shape_full (run->font, run->buffer, rq->features, rq->features_len,
|
||
NULL);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/* Convert index from UTF-32 to UTF-8 */
|
||
static uint32_t
|
||
_raqm_u32_to_u8_index (raqm_t *rq,
|
||
uint32_t index)
|
||
{
|
||
FriBidiStrIndex length;
|
||
char *output = malloc ((sizeof (char) * 4 * index) + 1);
|
||
|
||
length = fribidi_unicode_to_charset (FRIBIDI_CHAR_SET_UTF8,
|
||
rq->text,
|
||
index,
|
||
output);
|
||
|
||
free (output);
|
||
return length;
|
||
}
|
||
|
||
/* Convert index from UTF-8 to UTF-32 */
|
||
static uint32_t
|
||
_raqm_u8_to_u32_index (raqm_t *rq,
|
||
uint32_t index)
|
||
{
|
||
FriBidiStrIndex length;
|
||
uint32_t *output = malloc (sizeof (uint32_t) * (index + 1));
|
||
|
||
length = fribidi_charset_to_unicode (FRIBIDI_CHAR_SET_UTF8,
|
||
rq->text_utf8,
|
||
index,
|
||
output);
|
||
|
||
free (output);
|
||
return length;
|
||
}
|
||
|
||
static bool
|
||
_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char,
|
||
hb_codepoint_t r_char);
|
||
|
||
static bool
|
||
_raqm_in_hangul_syllable (hb_codepoint_t ch);
|
||
|
||
/**
|
||
* raqm_index_to_position:
|
||
* @rq: a #raqm_t.
|
||
* @index: (inout): character index.
|
||
* @x: (out): output x position.
|
||
* @y: (out): output y position.
|
||
*
|
||
* Calculates the cursor position after the character at @index. If the character
|
||
* is right-to-left, then the cursor will be at the left of it, whereas if the
|
||
* character is left-to-right, then the cursor will be at the right of it.
|
||
*
|
||
* Return value:
|
||
* %true if the process was successful, %false otherwise.
|
||
*
|
||
* Since: 0.2
|
||
*/
|
||
bool
|
||
raqm_index_to_position (raqm_t *rq,
|
||
size_t *index,
|
||
int *x,
|
||
int *y)
|
||
{
|
||
/* We don't currently support multiline, so y is always 0 */
|
||
*y = 0;
|
||
*x = 0;
|
||
|
||
if (rq == NULL)
|
||
return false;
|
||
|
||
if (rq->flags & RAQM_FLAG_UTF8)
|
||
*index = _raqm_u8_to_u32_index (rq, *index);
|
||
|
||
if (*index >= rq->text_len)
|
||
return false;
|
||
|
||
RAQM_TEST ("\n");
|
||
|
||
while (*index < rq->text_len)
|
||
{
|
||
if (_raqm_allowed_grapheme_boundary (rq->text[*index], rq->text[*index + 1]))
|
||
break;
|
||
|
||
++*index;
|
||
}
|
||
|
||
for (raqm_run_t *run = rq->runs; run != NULL; run = run->next)
|
||
{
|
||
size_t len;
|
||
hb_glyph_info_t *info;
|
||
hb_glyph_position_t *position;
|
||
len = hb_buffer_get_length (run->buffer);
|
||
info = hb_buffer_get_glyph_infos (run->buffer, NULL);
|
||
position = hb_buffer_get_glyph_positions (run->buffer, NULL);
|
||
|
||
for (size_t i = 0; i < len; i++)
|
||
{
|
||
uint32_t curr_cluster = info[i].cluster;
|
||
uint32_t next_cluster = curr_cluster;
|
||
*x += position[i].x_advance;
|
||
|
||
if (run->direction == HB_DIRECTION_LTR)
|
||
{
|
||
for (size_t j = i + 1; j < len && next_cluster == curr_cluster; j++)
|
||
next_cluster = info[j].cluster;
|
||
}
|
||
else
|
||
{
|
||
for (int j = i - 1; i != 0 && j >= 0 && next_cluster == curr_cluster;
|
||
j--)
|
||
next_cluster = info[j].cluster;
|
||
}
|
||
|
||
if (next_cluster == curr_cluster)
|
||
next_cluster = run->pos + run->len;
|
||
|
||
if (*index < next_cluster && *index >= curr_cluster)
|
||
{
|
||
if (run->direction == HB_DIRECTION_RTL)
|
||
*x -= position[i].x_advance;
|
||
*index = curr_cluster;
|
||
goto found;
|
||
}
|
||
}
|
||
}
|
||
|
||
found:
|
||
if (rq->flags & RAQM_FLAG_UTF8)
|
||
*index = _raqm_u32_to_u8_index (rq, *index);
|
||
RAQM_TEST ("The position is %d at index %zu\n",*x ,*index);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* raqm_position_to_index:
|
||
* @rq: a #raqm_t.
|
||
* @x: x position.
|
||
* @y: y position.
|
||
* @index: (out): output character index.
|
||
*
|
||
* Returns the @index of the character at @x and @y position within text.
|
||
* If the position is outside the text, the last character is chosen as
|
||
* @index.
|
||
*
|
||
* Return value:
|
||
* %true if the process was successful, %false in case of error.
|
||
*
|
||
* Since: 0.2
|
||
*/
|
||
bool
|
||
raqm_position_to_index (raqm_t *rq,
|
||
int x,
|
||
int y,
|
||
size_t *index)
|
||
{
|
||
int delta_x = 0, current_x = 0;
|
||
(void)y;
|
||
|
||
if (rq == NULL)
|
||
return false;
|
||
|
||
if (x < 0) /* Get leftmost index */
|
||
{
|
||
if (rq->resolved_dir == RAQM_DIRECTION_RTL)
|
||
*index = rq->text_len;
|
||
else
|
||
*index = 0;
|
||
return true;
|
||
}
|
||
|
||
RAQM_TEST ("\n");
|
||
|
||
for (raqm_run_t *run = rq->runs; run != NULL; run = run->next)
|
||
{
|
||
size_t len;
|
||
hb_glyph_info_t *info;
|
||
hb_glyph_position_t *position;
|
||
len = hb_buffer_get_length (run->buffer);
|
||
info = hb_buffer_get_glyph_infos (run->buffer, NULL);
|
||
position = hb_buffer_get_glyph_positions (run->buffer, NULL);
|
||
|
||
for (size_t i = 0; i < len; i++)
|
||
{
|
||
delta_x = position[i].x_advance;
|
||
if (x < (current_x + delta_x))
|
||
{
|
||
bool before = false;
|
||
if (run->direction == HB_DIRECTION_LTR)
|
||
before = (x < current_x + (delta_x / 2));
|
||
else
|
||
before = (x > current_x + (delta_x / 2));
|
||
|
||
if (before)
|
||
*index = info[i].cluster;
|
||
else
|
||
{
|
||
uint32_t curr_cluster = info[i].cluster;
|
||
uint32_t next_cluster = curr_cluster;
|
||
if (run->direction == HB_DIRECTION_LTR)
|
||
for (size_t j = i + 1; j < len && next_cluster == curr_cluster; j++)
|
||
next_cluster = info[j].cluster;
|
||
else
|
||
for (int j = i - 1; i != 0 && j >= 0 && next_cluster == curr_cluster;
|
||
j--)
|
||
next_cluster = info[j].cluster;
|
||
|
||
if (next_cluster == curr_cluster)
|
||
next_cluster = run->pos + run->len;
|
||
|
||
*index = next_cluster;
|
||
}
|
||
if (_raqm_allowed_grapheme_boundary (rq->text[*index],rq->text[*index + 1]))
|
||
{
|
||
RAQM_TEST ("The start-index is %zu at position %d \n", *index, x);
|
||
return true;
|
||
}
|
||
|
||
while (*index < (unsigned)run->pos + run->len)
|
||
{
|
||
if (_raqm_allowed_grapheme_boundary (rq->text[*index],
|
||
rq->text[*index + 1]))
|
||
{
|
||
*index += 1;
|
||
break;
|
||
}
|
||
*index += 1;
|
||
}
|
||
RAQM_TEST ("The start-index is %zu at position %d \n", *index, x);
|
||
return true;
|
||
}
|
||
else
|
||
current_x += delta_x;
|
||
}
|
||
}
|
||
|
||
/* Get rightmost index*/
|
||
if (rq->resolved_dir == RAQM_DIRECTION_RTL)
|
||
*index = 0;
|
||
else
|
||
*index = rq->text_len;
|
||
|
||
RAQM_TEST ("The start-index is %zu at position %d \n", *index, x);
|
||
|
||
return true;
|
||
}
|
||
|
||
typedef enum
|
||
{
|
||
RAQM_GRAPHEM_CR,
|
||
RAQM_GRAPHEM_LF,
|
||
RAQM_GRAPHEM_CONTROL,
|
||
RAQM_GRAPHEM_EXTEND,
|
||
RAQM_GRAPHEM_REGIONAL_INDICATOR,
|
||
RAQM_GRAPHEM_PREPEND,
|
||
RAQM_GRAPHEM_SPACING_MARK,
|
||
RAQM_GRAPHEM_HANGUL_SYLLABLE,
|
||
RAQM_GRAPHEM_OTHER
|
||
} _raqm_grapheme_t;
|
||
|
||
static _raqm_grapheme_t
|
||
_raqm_get_grapheme_break (hb_codepoint_t ch,
|
||
hb_unicode_general_category_t category);
|
||
|
||
static bool
|
||
_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char,
|
||
hb_codepoint_t r_char)
|
||
{
|
||
hb_unicode_general_category_t l_category;
|
||
hb_unicode_general_category_t r_category;
|
||
_raqm_grapheme_t l_grapheme, r_grapheme;
|
||
hb_unicode_funcs_t* unicode_funcs = hb_unicode_funcs_get_default ();
|
||
|
||
l_category = hb_unicode_general_category (unicode_funcs, l_char);
|
||
r_category = hb_unicode_general_category (unicode_funcs, r_char);
|
||
l_grapheme = _raqm_get_grapheme_break (l_char, l_category);
|
||
r_grapheme = _raqm_get_grapheme_break (r_char, r_category);
|
||
|
||
if (l_grapheme == RAQM_GRAPHEM_CR && r_grapheme == RAQM_GRAPHEM_LF)
|
||
return false; /*Do not break between a CR and LF GB3*/
|
||
if (l_grapheme == RAQM_GRAPHEM_CONTROL || l_grapheme == RAQM_GRAPHEM_CR ||
|
||
l_grapheme == RAQM_GRAPHEM_LF || r_grapheme == RAQM_GRAPHEM_CONTROL ||
|
||
r_grapheme == RAQM_GRAPHEM_CR || r_grapheme == RAQM_GRAPHEM_LF)
|
||
return true; /*Break before and after CONTROL GB4, GB5*/
|
||
if (r_grapheme == RAQM_GRAPHEM_HANGUL_SYLLABLE)
|
||
return false; /*Do not break Hangul syllable sequences. GB6, GB7, GB8*/
|
||
if (l_grapheme == RAQM_GRAPHEM_REGIONAL_INDICATOR &&
|
||
r_grapheme == RAQM_GRAPHEM_REGIONAL_INDICATOR)
|
||
return false; /*Do not break between regional indicator symbols. GB8a*/
|
||
if (r_grapheme == RAQM_GRAPHEM_EXTEND)
|
||
return false; /*Do not break before extending characters. GB9*/
|
||
/*Do not break before SpacingMarks, or after Prepend characters.GB9a, GB9b*/
|
||
if (l_grapheme == RAQM_GRAPHEM_PREPEND)
|
||
return false;
|
||
if (r_grapheme == RAQM_GRAPHEM_SPACING_MARK)
|
||
return false;
|
||
return true; /*Otherwise, break everywhere. GB1, GB2, GB10*/
|
||
}
|
||
|
||
static _raqm_grapheme_t
|
||
_raqm_get_grapheme_break (hb_codepoint_t ch,
|
||
hb_unicode_general_category_t category)
|
||
{
|
||
_raqm_grapheme_t gb_type;
|
||
|
||
gb_type = RAQM_GRAPHEM_OTHER;
|
||
switch ((int)category)
|
||
{
|
||
case HB_UNICODE_GENERAL_CATEGORY_FORMAT:
|
||
if (ch == 0x200C || ch == 0x200D)
|
||
gb_type = RAQM_GRAPHEM_EXTEND;
|
||
else
|
||
gb_type = RAQM_GRAPHEM_CONTROL;
|
||
break;
|
||
|
||
case HB_UNICODE_GENERAL_CATEGORY_CONTROL:
|
||
if (ch == 0x000D)
|
||
gb_type = RAQM_GRAPHEM_CR;
|
||
else if (ch == 0x000A)
|
||
gb_type = RAQM_GRAPHEM_LF;
|
||
else
|
||
gb_type = RAQM_GRAPHEM_CONTROL;
|
||
break;
|
||
|
||
case HB_UNICODE_GENERAL_CATEGORY_SURROGATE:
|
||
case HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR:
|
||
case HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR:
|
||
case HB_UNICODE_GENERAL_CATEGORY_UNASSIGNED:
|
||
if ((ch >= 0xFFF0 && ch <= 0xFFF8) ||
|
||
(ch >= 0xE0000 && ch <= 0xE0FFF))
|
||
gb_type = RAQM_GRAPHEM_CONTROL;
|
||
break;
|
||
|
||
case HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK:
|
||
case HB_UNICODE_GENERAL_CATEGORY_ENCLOSING_MARK:
|
||
case HB_UNICODE_GENERAL_CATEGORY_SPACING_MARK:
|
||
if (ch != 0x102B || ch != 0x102C || ch != 0x1038 ||
|
||
(ch <= 0x1062 && ch >= 0x1064) || (ch <= 0x1067 && ch >= 0x106D) ||
|
||
ch != 0x1083 || (ch <= 0x1087 && ch >= 0x108C) || ch != 0x108F ||
|
||
(ch <= 0x109A && ch >= 0x109C) || ch != 0x1A61 || ch != 0x1A63 ||
|
||
ch != 0x1A64 || ch != 0xAA7B || ch != 0xAA70 || ch != 0x11720 ||
|
||
ch != 0x11721) /**/
|
||
gb_type = RAQM_GRAPHEM_SPACING_MARK;
|
||
|
||
else if (ch == 0x09BE || ch == 0x09D7 ||
|
||
ch == 0x0B3E || ch == 0x0B57 || ch == 0x0BBE || ch == 0x0BD7 ||
|
||
ch == 0x0CC2 || ch == 0x0CD5 || ch == 0x0CD6 ||
|
||
ch == 0x0D3E || ch == 0x0D57 || ch == 0x0DCF || ch == 0x0DDF ||
|
||
ch == 0x1D165 || (ch >= 0x1D16E && ch <= 0x1D172))
|
||
gb_type = RAQM_GRAPHEM_EXTEND;
|
||
break;
|
||
|
||
case HB_UNICODE_GENERAL_CATEGORY_OTHER_LETTER:
|
||
if (ch == 0x0E33 || ch == 0x0EB3)
|
||
gb_type = RAQM_GRAPHEM_SPACING_MARK;
|
||
break;
|
||
|
||
case HB_UNICODE_GENERAL_CATEGORY_OTHER_SYMBOL:
|
||
if (ch >= 0x1F1E6 && ch <= 0x1F1FF)
|
||
gb_type = RAQM_GRAPHEM_REGIONAL_INDICATOR;
|
||
break;
|
||
|
||
default:
|
||
gb_type = RAQM_GRAPHEM_OTHER;
|
||
break;
|
||
}
|
||
|
||
if (_raqm_in_hangul_syllable (ch))
|
||
gb_type = RAQM_GRAPHEM_HANGUL_SYLLABLE;
|
||
|
||
return gb_type;
|
||
}
|
||
|
||
static bool
|
||
_raqm_in_hangul_syllable (hb_codepoint_t ch)
|
||
{
|
||
(void)ch;
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* raqm_version:
|
||
* @major: (out): Library major version component.
|
||
* @minor: (out): Library minor version component.
|
||
* @micro: (out): Library micro version component.
|
||
*
|
||
* Returns library version as three integer components.
|
||
*
|
||
* Since: 0.7
|
||
**/
|
||
void
|
||
raqm_version (unsigned int *major,
|
||
unsigned int *minor,
|
||
unsigned int *micro)
|
||
{
|
||
*major = RAQM_VERSION_MAJOR;
|
||
*minor = RAQM_VERSION_MINOR;
|
||
*micro = RAQM_VERSION_MICRO;
|
||
}
|
||
|
||
/**
|
||
* raqm_version_string:
|
||
*
|
||
* Returns library version as a string with three components.
|
||
*
|
||
* Return value: library version string.
|
||
*
|
||
* Since: 0.7
|
||
**/
|
||
const char *
|
||
raqm_version_string (void)
|
||
{
|
||
return RAQM_VERSION_STRING;
|
||
}
|
||
|
||
/**
|
||
* raqm_version_atleast:
|
||
* @major: Library major version component.
|
||
* @minor: Library minor version component.
|
||
* @micro: Library micro version component.
|
||
*
|
||
* Checks if library version is less than or equal the specified version.
|
||
*
|
||
* Return value:
|
||
* %true if library version is less than or equal the specfied version, %false
|
||
* otherwise.
|
||
*
|
||
* Since: 0.7
|
||
**/
|
||
bool
|
||
raqm_version_atleast (unsigned int major,
|
||
unsigned int minor,
|
||
unsigned int micro)
|
||
{
|
||
return RAQM_VERSION_ATLEAST (major, minor, micro);
|
||
}
|
||
|
||
/**
|
||
* RAQM_VERSION_ATLEAST:
|
||
* @major: Library major version component.
|
||
* @minor: Library minor version component.
|
||
* @micro: Library micro version component.
|
||
*
|
||
* Checks if library version is less than or equal the specified version.
|
||
*
|
||
* Return value:
|
||
* %true if library version is less than or equal the specfied version, %false
|
||
* otherwise.
|
||
*
|
||
* Since: 0.7
|
||
**/
|
||
|
||
/**
|
||
* RAQM_VERSION_STRING:
|
||
*
|
||
* Library version as a string with three components.
|
||
*
|
||
* Since: 0.7
|
||
**/
|
||
|
||
/**
|
||
* RAQM_VERSION_MAJOR:
|
||
*
|
||
* Library major version component.
|
||
*
|
||
* Since: 0.7
|
||
**/
|
||
|
||
/**
|
||
* RAQM_VERSION_MINOR:
|
||
*
|
||
* Library minor version component.
|
||
*
|
||
* Since: 0.7
|
||
**/
|
||
|
||
/**
|
||
* RAQM_VERSION_MICRO:
|
||
*
|
||
* Library micro version component.
|
||
*
|
||
* Since: 0.7
|
||
**/
|