Text along a path (GNU Emacs)

 

SVG 2 specifications allows flowing text along a curve via textPath element. This opens up possibilities for cool text effects e.g. Formula Editor in GNU Emacs.

GNU Emacs uses librsvg for SVG rendering on GNU/Linux. This fix for librsvg was  available in 2014 [4]. But this wasn't applied for some reason. Librsvg has moved to Rust since then. The code below is pre-Rust version of librsvg.


sample.svg

<?xml version="1.0" encoding="UTF-8"?>
<svg height="300" width="800" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
    <path id="my_path1" d="M 50 100 Q 25 10 180 100 T 350 100 T 520 100 T 690 100" fill="transparent" />
    <text>
        <textPath xlink:href ="#my_path1" font-size="34"> Text along a path looks awesome!!
        </textPath>
    </text>
</svg>

 

Text wrapping

Text wrapping using inline-size attribute (new implementation). This allows for pixel-precise fonts. In other words, instead of restrictions based on points (=1/72 of an inch) for font-size, one can specify pixel-size. How do you verify? In this case (monospace font), it's simple: 200px / 25px = 8 chars !


sample.svg

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
     width="100" height="300">

  <path d="M 0,25 H 120 M 0,225 H 120 M 62.5,25 V 2" stroke="red"/>
  <text x="62.5" y="25" inline-size="200"
    style="font: 25px IPAMincho; inline-size: 200px; writing-mode: vertical-rl;">
    テキストは10文字後に折り返されます。</text>

</svg>

  

Text inside a shape

Text layout inside a shape using shape-inside attribute (new implementation). The algorithm ensures that even with a superscript the padding is maintained.

The text is justified, i.e., the whitespace due to wrapping is distributed across the line. This example probably looks better with a center aligned text. However, we are simply reusing the SVG example from the specifications.

Justified text
Center aligned text

sample.svg

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
     width="300" height="300" viewBox="0 0 300 300">
 
  <circle id="circle" cx="150" cy="150" r="125" fill="none" stroke="black"/>
  <text style="shape-inside: url(#circle);
           shape-padding: 25px;
           font: 18px DejaVu;
           text-align: justified;
           line-height: 110%;">This is a
  sample of wrapped text in SVG 2! There should
  be 25 pixel padding around the text. The text is
  justified on both sides. It looks good!</text>

</svg>  

The algorithm works by finding the intersection of current line with the curve while tracing the curve from the start point to the end point. An up-crossing followed by a down-crossing constitutes a segment. Looking at the image, you'll see that this reverses in the lower half. Hence it's important that the curve definition is continuous.

It's important to note that librsvg only uses point, line and curve primitives from Cairo for drawing shapes. Hence a rectangle is a combination of four line segments while a circle is a combination of four arcs. For the above algorithm to work, it is imperative that the segments follow one another.

For padding a regular shape, simple scaling works well. i.e. scale the shape to a smaller size which accommodates the padding and then fill that shape with text. However, this approach doesn't work for irregular shapes.

Note: Remember to clear the path using cairo_new_path (). Otherwise, paths will continue to accumulate.


Text in a shape - Broken lines

Extending the algorithm in the previous section, text can also be fitted into shapes where it splits the line into separate sections (upto 4 for an M shape). The only requirement is that the shape be defined as a continuous curve.

 

sample.svg

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
     width="400" height="300" viewBox="0 0 400 300">

  <path id="pw" fill="none" stroke="black"
    d="M50,50 h100 L200,150 L250,50 h100
       L250,250 h-100 Z"/>

  <text style="shape-inside: url(#pw);
           shape-padding: 20px;
           font: 12px DejaVu;
           text-align: justified;
           line-height: 100%;">This is a
  sample of wrapped text in SVG 2! There should
  be 25 pixel padding around the text. The text is
  justified on both sides. It looks good!</text>

</svg>
 

Font Features

Opentype fonts allow a user to select from a range of features like kerning, ligatures, character variants, stylistic sets, etc. to suit one's taste. You can use otf-test command (use any dependent functions from the file) to test and preview these features.

The demo below uses the Fira code font (https://github.com/tonsky/FiraCode) with the text "a g fi". The website has a good documentation about the various features. You can refer to it for a quick reference and comparison.



Developer note: This uses Pango font feature API. The attribute value "normal" doesn't work. Just don't set the attribute in this case.

 PangoAttribute *attribute = pango_attr_font_features_new ("'cv01', 'ss01'");
 attribute->start_index = 0;
 attribute->end_index = -1;
 pango_attr_list_insert (attr_list, attribute);
 


Pango provides high-level API for Harfbuzz backend. In case you want to use Harfbuzz API directly, following code might be useful.

  hb_feature_t userfeatures[1];
  userfeatures[0].tag = HB_TAG ('l','i','g', 'a');
  userfeatures[0].value = 1;
  userfeatures[0].start = HB_FEATURE_GLOBAL_START;
  userfeatures[0].end = HB_FEATURE_GLOBAL_END;

  hb_bool_t success = hb_shape_full (hb_font, hb_buffer, userfeatures, 1, NULL);

 

CSS Parsing

libcroco is the CSS parsing engine behind the C version of librsvg. Some useful code snippets for the same.

    char *str = "body { font: 20px \"Noto Naskh Arabic\", serif; inline-size: 200px; direction: rtl;}";

    CRStatement *stmt = cr_statement_parse_from_buf (str, CR_UTF_8);
    // CRSelector *selectors;
    CRDeclaration *declarations;
    cr_statement_ruleset_get_declarations (stmt, &declarations);
    printf("\n%d %s", cr_declaration_nr_props (declarations),
           cr_declaration_to_string (declarations, 2));
    for (int i = 0; i < cr_declaration_nr_props (declarations); i += 1) {
        CRDeclaration *decl = cr_declaration_get_from_list (declarations, i);
        const char *property = cr_string_peek_raw_str (decl->property);
        const char *value = cr_term_to_string (decl->value);
        printf ("%s: %s important: %d\n", property, value, decl->important);
    }

 

Code

 

References

 
 
 
 

Comments

Popular posts from this blog

GNU Emacs as a Comic Book Reader

HTML Renderer (Emacs)

Mozilla Readability in GNU Emacs