#include #include #include #include #include #include "pastry.h" #include #include #include #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; }