/*
 * Program to render a single glyph from a TTF font and output it as
 * an encapsulated PostScript (EPS) image.  Useful for examining a symbol
 * up close, and provides a simple example of using the Freetype library.
 *
 * Usage:
 *   eps-unicode-character -f FONT-FILE CHAR-CODE
 *
 * Use the -d option to show control points for curves.
 *
 * CHAR-CODE is a decimal or hexadecimal (with 0x prefix) Unicode character
 * code for the symbol to render.  FONT-FILE is the full path and filename
 * of a TrueType font containing the appropriate symbol.
 *
 * (c) Copyright 2004 Geoff Richards.
 * You may do what you want with this program.  There is no warranty.
 * If you like it, buy me beer.
 *
 * http://ungwe.org/blog/2004/02/22/15:50/
 */

#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_GLYPH_H
#include FT_OUTLINE_H
#include FT_BBOX_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>


/* Scraps of PostScript used to style the debugging information. */
#define PS_ON_POINT_STYLE "gsave\n" \
                          "0 0 1 setrgbcolor 2 setlinewidth 1 setlinecap\n"
#define PS_OFF_POINT_STYLE "gsave\n" \
                           "0.5 0 0.7 setrgbcolor 2 setlinewidth 1 setlinecap\n"
#define PS_ON_LINE_STYLE "gsave 0 0 0 setrgbcolor 1 setlinewidth\n"
#define PS_CONIC_LINE_STYLE "gsave 1 0 0 setrgbcolor 0.5 setlinewidth\n"
#define PS_CUBIC_LINE_STYLE "gsave 0 1 0 setrgbcolor 0.5 setlinewidth\n"


/* Stuff we get from the arguments. */
static const char *progname;
static const char *font_filename = 0;
static FT_F26Dot6 point_size = 72;
static FT_UInt res_dpi = 600;
int option_debug = 0;

/* For keeping track of current state while processing the outline. */
struct State {
    int mode;
    double current_x, current_y;
};

void die_freetype (FT_Error err, const char *func);
void draw_outline (FT_OutlineGlyph glyph);
double floatify (FT_Pos x);
int handle_move_to (FT_Vector *to, void *data);
int handle_line_to (FT_Vector *to, void *data);
int handle_conic_to (FT_Vector *control, FT_Vector *to, void *data);
int handle_cubic_to (FT_Vector *control1, FT_Vector *control2, FT_Vector *to,
                     void *data);
int safe_atoi (const char *s, const char *what_for);
int decode_arguments (int argc, char **argv);


int
main (int argc, char **argv)
{
    int i, char_code;
    FT_UInt glyph_index;
    FT_Error err;
    FT_Library library;
    FT_Face face;
    FT_Glyph glyph;

    i = decode_arguments(argc, argv);
    char_code = safe_atoi(argv[i], "character code");

    err = FT_Init_FreeType(&library);
    if (err) die_freetype(err, "FT_Init_FreeType");

    err = FT_New_Face(library, font_filename, 0, &face);
    if (err == FT_Err_Unknown_File_Format) {
        fprintf(stderr, "%s: font file '%s' can't be read (might be"
                " unsupported).\n", progname, font_filename);
        return 2;
    }
    else if (err)
        die_freetype(err, "FT_New_Face");

    err = FT_Set_Char_Size(face, 0, point_size * 64, res_dpi, res_dpi);
    if (err) die_freetype(err, "FT_Set_Char_Size");

    glyph_index = FT_Get_Char_Index(face, char_code);
    if (!glyph_index) {
        fprintf(stderr, "%s: character %d not found in font.\n",
                progname, char_code);
        return 3;
    }

    err = FT_Load_Glyph(face, glyph_index,
                        FT_LOAD_NO_BITMAP | FT_LOAD_NO_HINTING);
    if (err) die_freetype(err, "FT_Load_Glyph");

    err = FT_Get_Glyph(face->glyph, &glyph);
    if (err) die_freetype(err, "FT_Get_Glyph");

    if (glyph->format != FT_GLYPH_FORMAT_OUTLINE) {
        fprintf(stderr, "%s: no outline found for glyph.\n", progname);
        return(2);
    }

    draw_outline((FT_OutlineGlyph) glyph);

    FT_Done_Glyph(glyph);
    err = FT_Done_FreeType(library);
    if (err) die_freetype(err, "FT_Done_FreeType");

    return 0;
}


void
die_freetype (FT_Error err, const char *func)
{
    fprintf(stderr, "%s: libfreetype2 error %d from %s.\n",
            progname, (int) err, func);
    exit(2);
}


/* Render an outline to PostScript.  We set up callback functions to handle
 * each segment of the outline and print the appropriate stuff to draw it.
 * The 'user data' passed to each one is a pointer to a State struct, which
 * stores the current position (the last destination of a moveto, lineto, or
 * curveto operator) and the 'mode'.  The callbacks know about the following
 * modes:
 *   0 - draw part of a path, for the actual output.
 *   1 - draw blobs at the destination point and any control points.
 *   2 - draw a thin stroke for the segment and colored lines to show how
 *       the control points relate to the start and end points.
 * Modes 1 and 2 are only used with the -d option.
 */
void
draw_outline (FT_OutlineGlyph glyph)
{
    FT_Outline_Funcs handlers;
    FT_Error err;
    FT_BBox bbox;
    struct State s;

    err = FT_Outline_Get_BBox(&glyph->outline, &bbox);
    if (err) die_freetype(err, "FT_Outline_Get_BBox");

    /* I have a feeling that the bounding box in an EPS is meant to only
     * use integer values, so I round them down and up as appropriate.  */
    printf("%%!PS-Adobe-3.0 EPSF-3.0\n"
           "%%%%Creator: %s\n"
           "%%%%BoundingBox: %d %d %d %d\n"
           "%%%%Pages: 1\n"
           "%%%%EndComments\n\n"
           "%%%%Page: 1 1\n"
           "gsave newpath\n",
           progname,
           (int) floor(floatify(bbox.xMin)), (int) floor(floatify(bbox.yMin)),
           (int) ceil(floatify(bbox.xMax)), (int) ceil(floatify(bbox.yMax)));

    handlers.move_to = handle_move_to;
    handlers.line_to = handle_line_to;
    handlers.conic_to = handle_conic_to;
    handlers.cubic_to = handle_cubic_to;
    handlers.shift = 0;
    handlers.delta = 0;

    if (option_debug)
        printf("0.77 0.77 0.77 setrgbcolor\n");

    s.mode = 0;
    err = FT_Outline_Decompose(&glyph->outline, &handlers, &s);
    if (err) die_freetype(err, "FT_Outline_Decompose");
    printf("closepath fill grestore\n");

    if (option_debug) {
        s.mode = 1;
        err = FT_Outline_Decompose(&glyph->outline, &handlers, &s);
        if (err) die_freetype(err, "FT_Outline_Decompose");
        s.mode = 2;
        err = FT_Outline_Decompose(&glyph->outline, &handlers, &s);
        if (err) die_freetype(err, "FT_Outline_Decompose");
    }

    printf("%%%%EOF\n");
}

double
floatify (FT_Pos x)
{
    return (double) x / 64.0;
}

int
handle_move_to (FT_Vector *to, void *data)
{
    struct State *s = (struct State *) data;
    s->current_x = floatify(to->x);
    s->current_y = floatify(to->y);
    if (s->mode == 0)
        printf("%g %g moveto\n", s->current_x, s->current_y);
    else if (s->mode == 1)
        printf(PS_ON_POINT_STYLE
               "newpath %g %g moveto 0 0 rlineto stroke grestore\n",
               s->current_x, s->current_y);
    return 0;
}

int
handle_line_to (FT_Vector *to, void *data)
{
    struct State *s = (struct State *) data;
    double x = floatify(to->x), y = floatify(to->y);
    if (s->mode == 0)
        printf("%g %g lineto\n", x, y);
    else if (s->mode == 1)
        printf(PS_ON_POINT_STYLE
               "newpath %g %g moveto 0 0 rlineto stroke grestore\n",
               x, y);
    else if (s->mode == 2)
        printf(PS_ON_LINE_STYLE
               "newpath %g %g moveto\n"
               "%g %g lineto stroke grestore\n",
               s->current_x, s->current_y, x, y);
    s->current_x = x;
    s->current_y = y;
    return 0;
}

int
handle_conic_to (FT_Vector *control, FT_Vector *to, void *data)
{
    /* These conic curves have only one control point, which we use
     * (together with the start and end points) to generate the two
     * control points needed for the PostScript curveto operator.
     * I don't know how this stuff works, but stole the formulae
     * from SFontTTF.cpp in Yudit.  The results look right.  */
    struct State *s = (struct State *) data;
    double x = floatify(to->x), y = floatify(to->y);
    double cx = floatify(control->x), cy = floatify(control->y);
    if (s->mode == 0)
        printf("%g %g %g %g %g %g curveto\n",
               (s->current_x + 2 * cx) / 3, (s->current_y + 2 * cy) / 3,
               (2 * cx + x) / 3, (2 * cy + y) / 3,
               x, y);
    else if (s->mode == 1) {
        /* On point. */
        printf(PS_ON_POINT_STYLE
               "newpath %g %g moveto 0 0 rlineto stroke grestore\n",
               x, y);

        /* Off point */
        printf(PS_OFF_POINT_STYLE
               "newpath %g %g moveto 0 0 rlineto stroke grestore\n",
               cx, cy);
    }
    else if (s->mode == 2) {
        /* Black line showing actual curve. */
        printf(PS_ON_LINE_STYLE
               "newpath %g %g moveto\n"
               "%g %g %g %g %g %g curveto stroke grestore\n",
               s->current_x, s->current_y,
               (s->current_x + 2 * cx) / 3, (s->current_y + 2 * cy) / 3,
               (2 * cx + x) / 3, (2 * cy + y) / 3,
               x, y);

        /* Red line showing connections to control point. */
        printf(PS_CONIC_LINE_STYLE
               "newpath %g %g moveto\n"
               "%g %g lineto %g %g lineto stroke grestore\n",
               s->current_x, s->current_y, cx, cy, x, y);

    }
    s->current_x = x;
    s->current_y = y;
    return 0;
}

int
handle_cubic_to (FT_Vector *control1, FT_Vector *control2, FT_Vector *to,
                 void *data)
{
    /* This should work, but I haven't seen it used. */
    struct State *s = (struct State *) data;
    double x = floatify(to->x), y = floatify(to->y);
    double c1x = floatify(control1->x), c1y = floatify(control1->y);
    double c2x = floatify(control2->x), c2y = floatify(control2->y);
    if (s->mode == 0)
        printf("%g %g %g %g %g %g curveto\n",
               c1x, c1y, c2x, c2y, x, y);
    else if (s->mode == 1) {
        /* On point */
        printf(PS_ON_POINT_STYLE
               "newpath %g %g moveto 0 0 rlineto stroke grestore\n",
               x, y);
        /* Off points */
        printf(PS_OFF_POINT_STYLE
               "newpath %g %g moveto 0 0 rlineto stroke\n"
               "newpath %g %g moveto 0 0 rlineto stroke grestore\n",
               c1x, c1y, c2x, c2y);
    }
    else if (s->mode == 2) {
        /* Black line showing actual curve. */
        printf(PS_ON_LINE_STYLE
               "newpath %g %g moveto\n"
               "%g %g %g %g %g %g curveto stroke grestore\n",
               s->current_x, s->current_y,
               c1x, c1y, c2x, c2y, x, y);


        /* Green line showing connections to control point. */
        printf(PS_CUBIC_LINE_STYLE
               "newpath %g %g moveto\n"
               "%g %g lineto %g %g lineto %g %g lineto stroke grestore\n",
               s->current_x, s->current_y, c1x, c1y, c2x, c2y, x, y);
    }
    s->current_x = x;
    s->current_y = y;
    return 0;
}


int
safe_atoi (const char *s, const char *what_for)
{
    char *endptr;
    int i = strtol(s, &endptr, 0);

    if (endptr == s || *endptr) {
        fprintf(stderr, "%s: bad number '%s' for %s.\n",
                progname, s, what_for);
        exit(1);
    }
    else if (i < 0) {
        fprintf(stderr, "%s: negative numbers for %s are not allowed.\n",
                progname, what_for);
        exit(1);
    }

    return i;
}


int
decode_arguments (int argc, char **argv)
{
    int i = 1;
    char c;
    progname = argv[0];

    while (i < argc && argv[i][0] == '-') {
        if (!argv[i][1] || argv[i][2]) {
            fprintf(stderr, "%s: bad option '%s'.\n", progname, argv[i]);
            exit(1);
        }
        c = argv[i++][1];

        if (c != 'd' && i == argc - 1) {
            fprintf(stderr, "%s: missing value for option '%s'.\n",
                    progname, argv[i]);
            exit(1);
        }

        if (c == 'f')
            font_filename = argv[i++];
        else if (c == 'd')
            option_debug = 1;
        else if (c == 's')
            point_size = safe_atoi(argv[i++], "point size");
        else if (c == 'r')
            res_dpi = safe_atoi(argv[i++], "resolution");
        else {
            fprintf(stderr, "%s: unknown option '-%c'.\n", progname, c);
            exit(1);
        }
    }

    if (i >= argc) {
        fprintf(stderr, "%s: character code argument required\n", progname);
        exit(1);
    }

    if (!font_filename) {
        fprintf(stderr, "%s: font filename required (-f option)\n", progname);
        exit(1);
    }

    return i;
}

/* vi:set ts=4 sw=4 expandtab: */
