λ

pastry

truetype font renderer

settings

demo.c

8030 bytes raw

#include <stdio.h>
#include <string.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xrender.h>

#include <stdint.h>
#include "pastry.h"

#include <hb.h>
#include <hb-ot.h>

#include <fribidi.h>

#define MAX_CHARS 256

static int add_glyph(Display *dpy, GlyphSet glyphset, PASTRY *pastry, PASTRY_Glyph glyph, int *advance)
{
#define ABORT(glyph, m) do { fprintf(stderr, "glyph %lu %s\n", (unsigned long)(glyph), m); return -1; } while (0)

    PASTRY_GMetrics mtx;
    if (pastry_gmetrics(pastry, glyph, &mtx) < 0)
        ABORT(glyph, "bad glyph metrics");

    PASTRY_Image img = {
        .width  = (mtx.minWidth + 3) & ~3,
        .height = mtx.minHeight,
    };
    char pixels[img.width * img.height];
    img.pixels = pixels;
    if (pastry_render(pastry, glyph, img) < 0)
        ABORT(glyph, "not rendered");

    XGlyphInfo info = {
        .x      = (short) -mtx.leftSideBearing,
        .y      = (short) -mtx.yOffset,
        .width  = (unsigned short) img.width,
        .height = (unsigned short) img.height,
        .xOff   = (short) mtx.advanceWidth,
        .yOff   = 0
    };
    Glyph g = (Glyph) glyph;
    XRenderAddGlyphs(dpy, glyphset, &g, &info, 1,
                     img.pixels, (int) (img.width * img.height));

    *advance += info.xOff;
    return 0;
}

typedef struct {
    FriBidiLevel  level;          // embedding level; odd means right-to-left
    int           advance;        // total advance, used to place the next run
    unsigned int  count;          // number of shaped glyphs
    uint32_t      glyphs[MAX_CHARS];
} LineRun;

static int draw_line(Display *dpy, GlyphSet glyphset, Picture fgpic, Picture pic,
                     PASTRY *pastry, hb_font_t *font, hb_buffer_t *buffer,
                     const char *text, int y)
{
    FriBidiChar  u[MAX_CHARS];
    FriBidiLevel levels[MAX_CHARS];

    FriBidiStrIndex len = fribidi_charset_to_unicode(FRIBIDI_CHAR_SET_UTF8, text,
                                                     (FriBidiStrIndex) strlen(text), u);
    if (len <= 0)
        return 0;
    if (len > MAX_CHARS)
        len = MAX_CHARS;

    for (FriBidiStrIndex i = 0; i < len; i++)
        levels[i] = 0;

    FriBidiParType baseDir = FRIBIDI_PAR_ON; // let fribidi pick the direction
    fribidi_log2vis(u, len, &baseDir, NULL, NULL, NULL, levels);

    // split the line into runs of equal embedding level, in logical order
    LineRun runs[MAX_CHARS];
    unsigned int nruns = 0;

    for (FriBidiStrIndex i = 0; i < len; ) {
        FriBidiStrIndex end = i + 1;
        while (end < len && levels[end] == levels[i])
            end++;

        LineRun *run = &runs[nruns++];
        run->level   = levels[i];
        run->advance = 0;

        hb_buffer_clear_contents(buffer);
        hb_buffer_add_utf32(buffer, (const uint32_t *) &u[i], (int) (end - i), 0, (int) (end - i));
        hb_buffer_guess_segment_properties(buffer);
        hb_buffer_set_direction(buffer, (levels[i] & 1) ? HB_DIRECTION_RTL : HB_DIRECTION_LTR);
        hb_shape(font, buffer, NULL, 0);

        unsigned int count = 0;
        hb_glyph_info_t *info = hb_buffer_get_glyph_infos(buffer, &count);
        if (count > MAX_CHARS)
            count = MAX_CHARS;
        run->count = count;
        for (unsigned int k = 0; k < count; k++) {
            run->glyphs[k] = info[k].codepoint;
            if (add_glyph(dpy, glyphset, pastry, (PASTRY_Glyph) info[k].codepoint, &run->advance) < 0)
                return -1;
        }

        i = end;
    }

    // reorder the runs into visual order (rule L2 of the bidi algorithm)
    FriBidiLevel maxLevel = 0, lowestOdd = 0;
    for (unsigned int i = 0; i < nruns; i++) {
        if (runs[i].level > maxLevel)
            maxLevel = runs[i].level;
        if ((runs[i].level & 1) && (lowestOdd == 0 || runs[i].level < lowestOdd))
            lowestOdd = runs[i].level;
    }
    for (FriBidiLevel level = maxLevel; lowestOdd && level >= lowestOdd; level--) {
        for (unsigned int i = 0; i < nruns; ) {
            if (runs[i].level < level) {
                i++;
                continue;
            }
            unsigned int j = i;
            while (j < nruns && runs[j].level >= level)
                j++;
            for (unsigned int a = i, b = j - 1; a < b; a++, b--) {
                LineRun tmp = runs[a];
                runs[a] = runs[b];
                runs[b] = tmp;
            }
            i = j;
        }
    }

    int x = 20;
    for (unsigned int i = 0; i < nruns; i++) {
        if (runs[i].count == 0)
            continue;
        XRenderCompositeString32(dpy, PictOpOver, fgpic, pic, NULL, glyphset,
                                 0, 0, x, y, runs[i].glyphs, (int) runs[i].count);
        x += runs[i].advance;
    }
    return 0;
}

int main(void)
{
#define END(m) do { fprintf(stderr, "%s\n", m); return 1; } while (0)

    Display *dpy = XOpenDisplay(NULL);
    if (dpy == NULL)
        END("Can't open X display");
    unsigned int s = 2, screen = (unsigned int)DefaultScreen(dpy);

    Window win = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, 600*s, 440*s, 0,
                               DefaultDepth(dpy, screen), InputOutput, CopyFromParent, 0, NULL);
    XSelectInput(dpy, win, ExposureMask);
    Atom wmDeleteWindow = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
    XSetWMProtocols(dpy, win, &wmDeleteWindow, 1);
    XMapRaised(dpy, win);

    XRenderPictFormat *fmt = XRenderFindVisualFormat(dpy, DefaultVisual(dpy, screen));
    Picture pic = XRenderCreatePicture(dpy, win, fmt, 0, NULL);
    XRenderColor fg = { 0, 0, 0, 0xFFFF }, bg = { 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF };

    Pixmap fgpix = XCreatePixmap(dpy, win, 1, 1, 24);
    XRenderPictureAttributes attr = { .repeat = True };
    fmt = XRenderFindStandardFormat(dpy, PictStandardRGB24);
    Picture fgpic = XRenderCreatePicture(dpy, fgpix, fmt, CPRepeat, &attr);
    XRenderFillRectangle(dpy, PictOpSrc, fgpic, &fg, 0, 0, 1, 1);

    fmt = XRenderFindStandardFormat(dpy, PictStandardA8);
    GlyphSet glyphset = XRenderCreateGlyphSet(dpy, fmt);

    PASTRY pastry = {
        .xScale = 16*s,
        .yScale = 16*s,
        .flags  = PASTRY_DOWNWARD_Y,
    };
    pastry.font = pastry_loadfile("firago-extended.ttf");
    if (pastry.font == NULL)
        END("failed to load font");

    // harfbuzz does the shaping; pastry just draws the glyph ids it hands us
    hb_blob_t *blob = hb_blob_create_from_file_or_fail("firago-extended.ttf");
    if (blob == NULL)
        END("harfbuzz failed to load font");
    hb_face_t *face = hb_face_create(blob, 0);
    hb_font_t *font = hb_font_create(face);
    hb_ot_font_set_funcs(font);
    hb_buffer_t *buffer = hb_buffer_create();

    XEvent event;
    while (!XNextEvent(dpy, &event)) {
        if (event.type == Expose) {
            XRenderFillRectangle(dpy, PictOpOver, pic, &bg, 0, 0, (unsigned int)event.xexpose.width, (unsigned int)event.xexpose.height);

            FILE *file = fopen("text.utf8", "r");
            if (file == NULL)
                END("cannot open input text");

            PASTRY_LMetrics lmtx;
            pastry_lmetrics(&pastry, &lmtx);
            int y = 20 + (int)lmtx.ascender + (int)lmtx.lineGap;

            char text[MAX_CHARS];
            while (fgets(text, sizeof(text), file)) {
                size_t len = strlen(text);
                if (len > 0 && text[len - 1] == '\n')
                    text[--len] = 0;

                if (draw_line(dpy, glyphset, fgpic, pic, &pastry, font, buffer, text, y) < 0)
                    END("failed to draw line");

                y += 2 * ((int)lmtx.ascender + (int)lmtx.descender + (int)lmtx.lineGap);
            }

            fclose(file);
        } else if (event.type == ClientMessage) {
            if ((Atom) event.xclient.data.l[0] == wmDeleteWindow)
                break;
        }
    }

    hb_buffer_destroy(buffer);
    hb_font_destroy(font);
    hb_face_destroy(face);
    hb_blob_destroy(blob);

    pastry_freefont(pastry.font);
    XCloseDisplay(dpy);
    return 0;
}