/* Copyright 2022-2026 Kurt Nienhaus
 *
 * This file is part of VgaGames.
 * VgaGames is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 * VgaGames is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with VgaGames.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include "font.h"

void init_font_text(void);

struct VG_Image * vg4_font_loadtext_nolist(const char *, const char *, struct VG_Hash *, struct VG_Hash **);
struct VG_Image * vg4_font_totext_nolist(const char *, const char *, const char *, struct VG_Hash *, struct VG_Hash **);

#define MAX_RECURS  24
#define MAX_CELLS   32
#define TAG_SIZE    32

#define ALIAS_SEP  "&"

static struct VG_Image * font_loadtext(const char *, const char *, struct VG_Hash *, struct VG_Hash **);
static struct VG_Image * font_loadtext_doit(const char *, const char *, struct VG_Hash *, struct VG_Hash **, VG_BOOL);
static struct VG_Image * font_totext(const char *, const char *, const char *, struct VG_Hash *, struct VG_Hash **);
static struct VG_Image * font_totext_doit(const char *, const char *, const char *, struct VG_Hash *, struct VG_Hash **, VG_BOOL);

struct fntimg {  /* image with position of sub-images */
  struct VG_Image *imgp;   /* image */
  struct SML3_hash *hsub;  /* positions of sub-images */
};

struct subpos {  /* key of fntimg.hsub */
  char itag[TAG_SIZE];  /* box-tag or empty */
  struct VG_Rect rect;  /* rectangle of image */
};

struct fntchar {  /* font character or image */
  int flag;                 /* 0 = end, 1 = font-character, 2 = image */
  union {
    struct vgi_font *vgft;  /* pointer to character */
    struct fntimg fimg;     /* image and sub-positions */
  } u;
  const char *txptr;        /* actual text-pointer */
  size_t txsize;            /* actual text-size */
  int linew, lineh;         /* actual width and height for this line */
  int gaptop, gapbottom;    /* gap lines if image-height is height of an included image without gaps */
  char itag[TAG_SIZE];      /* box-tag */
  struct VG_Hash *hutag;    /* sub-tags: <name>=<rectangle> */
};

struct textlines {  /* text-lines */
  int imgw, imgh;            /* image size to create */
  int fntw, fnth;            /* position in fntlist */
  int linew, lineh;          /* line-size in width and height for actual line */
  int lastsep_fntw;          /* last separator: x-position in fntlist[fnth], or -1 = no separator yet */
  struct fntchar **fntlist;  /* characters or images */
};

struct fntparm {  /* font parameters */
  struct VG_Image *bgimg;   /* background image, (default: NULL) */
  int maxwidth;             /* greatest width of image (pixel or percent), (default: width of window) */
  int boxwidth;             /* width of box (pixel or percent), (default: 0) */
  int boxheight;            /* height of box (pixel or percent), (default: 0) */
  int border;               /* border in pixels around (outside) image in background color, (default: 0) */
  int rectcolor;            /* rectangle around image: one of VG_COLORS or from VG_COLOR_RGB(), (default: VG_COLOR_TRANSPARENT) */
  struct VG_Font *fntp;     /* font, (default: sys:low for VG_WINDOW_SIZE_LOW or sys:high for VG_WINDOW_SIZE_HIGH) */
  int fgcolor;              /* text color: one of VG_COLORS or from VG_COLOR_RGB(), (default: VG_COLOR_WHITE) */
  int bgcolor;              /* background color: one of VG_COLORS or from VG_COLOR_RGB(), (default: VG_COLOR_TRANSPARENT) */
  VG_BOOL bold;             /* whether font-characters are bold, (default: VG_FALSE) */
  int line_pitch;           /* line pitch to add (in percent of default pitch), (default: 0) */
  int orientation1;         /* orientation of embedded text: one of VG_ORIENTATIONS, (default: VG_ORI_LEFT) */
  int orientation2;         /* as orientation1, when the textline exceeds its horizontal boundary */
  int v_orientation1;       /* vertical orientation: VG_ORI_LEFT = top, VG_ORI_RIGHT = bottom, VG_ORI_CENTER = centered, (default: VG_ORI_CENTER) */
  int v_orientation2;       /* as v_orientation1, when the text exceeds its vertical boundary */
  VG_BOOL nowrap;           /* don't wrap text-line on separator character, (default: VG_FALSE) */
  struct {                  /* table-parameters */
    int cells[MAX_CELLS];     /* cell sizes in percent for each cell of a row */
    int doraster;             /* raster: 0 = no, 1 = lines, 2 = full */
    int rastercolor;          /* color of raster */
  } table;
  char itag[TAG_SIZE];      /* box-tag, (default: empty) */
  struct VG_Hash *hutag;    /* sub-tags: <name>=<rectangle> */
};

static void free_hsub(void *);
static void transfer_hsub(struct SML3_hash *, struct VG_Hash *, int, int);
static void default_params(struct fntparm *);
static void copy_params(struct fntparm *, const struct fntparm *);
static void get_params(struct fntparm *, struct VG_Hash *, const char *, struct VG_Hash *);
static void free_params(struct fntparm *);
static struct textlines * create_textlines(void);
static void addline_textlines(struct textlines *);
static void addto_textlines(struct textlines *);
static void free_textlines(struct textlines *);
static void line_close(const struct VG_Font *, struct textlines *, int);
static struct fntimg fill_text(struct textlines *, const struct fntparm *, int);
static struct VG_Image * add_ori(struct VG_Image *, int, int, int, int, int);
static struct VG_Image * add_border(struct VG_Image *, int, int);
static void add_raster(struct VG_Image *, struct textlines *, int, int, VG_BOOL);
static void draw_imgrect(struct VG_Image *, int);
static void insert_alias(const char *, struct VG_Hash **);
static void set_file_with_path(char *, size_t, const char *, const char *);
static VG_BOOL ctrlcmd_parse(const char **, size_t *, const char **, const char **, char *, size_t, struct VG_Hash **);
static struct fntimg ctrlcmd_exec(VG_BOOL *, const char *, const char *, const char *, struct fntparm *, int, VG_BOOL *, struct VG_Hash *, const char *, struct VG_Hash *);
static struct fntimg ccmdexec_txtfile(const char *, const char *, const struct fntparm *, int, const char *, struct VG_Hash *);
static struct fntimg ccmdexec_txt(const char *, const char *, const struct fntparm *, int, const char *, struct VG_Hash *);
static struct fntimg ccmdexec_imgfile(const char *, const char *, const struct fntparm *, int, const char *, struct VG_Hash *);
static struct fntimg ccmdexec_img(const char *, const char *, const struct fntparm *, int, const char *, struct VG_Hash *);
static struct fntimg ccmdexec_mlang(const char *, const char *, const struct fntparm *, int, const char *, struct VG_Hash *);
static struct fntimg ccmdexec_var(const char *, const char *, const struct fntparm *, int, const char *, struct VG_Hash *);
static struct fntimg ccmdexec_box(const char *, const char *, const struct fntparm *, int, const char *, struct VG_Hash *);
static struct fntimg ccmdexec_table(const char *, const char *, const struct fntparm *, int, const char *, struct VG_Hash *);


/* set functions */
void
init_font_text(void)
{
  vg4->font->loadtext = font_loadtext;
  vg4->font->totext = font_totext;
} /* Ende init_font_text */


/* vg4_font_loadtext_nolist: like font_loadtext(), but without inserting into list */
struct VG_Image *
vg4_font_loadtext_nolist(const char *filename, const char *dparam, struct VG_Hash *hvar, struct VG_Hash **htags)
{
  return font_loadtext_doit(filename, dparam, hvar, htags, VG_FALSE);
} /* Ende vg4_font_loadtext_nolist */


/* vg4_font_totext_nolist: like font_totext(), but without inserting into list */
struct VG_Image *
vg4_font_totext_nolist(const char *text, const char *dparam, const char *dirname, struct VG_Hash *hvar, struct VG_Hash **htags)
{
  return font_totext_doit(text, dparam, dirname, hvar, htags, VG_FALSE);
} /* Ende vg4_font_totext_nolist */


/* font_loadtext:
 * load text from file
 * @param filename  text file
 * @param dparam    default parameters, or NULL
 * @param hvar      hash with variable-values for text control-commands "var", or NULL
 *                    hash-format:
 *                     - key:   variable name
 *                     - value: variable value
 * @param htags     for returning position of tagged images, or NULL
 *                    hash-format:
 *                     - key:   box-tag value
 *                     - value: position as rectangle (struct VG_Rect)
 * @return  text-image or NULL = error
 *
 * For dparam and format of text, see font_totext()
 */
static struct VG_Image *
font_loadtext(const char *filename, const char *dparam, struct VG_Hash *hvar, struct VG_Hash **htags)
{
  return font_loadtext_doit(filename, dparam, hvar, htags, VG_TRUE);
} /* Ende font_loadtext */


/* font_loadtext_doit: do action */
static struct VG_Image *
font_loadtext_doit(const char *filename, const char *dparam, struct VG_Hash *hvar, struct VG_Hash **htags, VG_BOOL intolist)
{
  char dirname[VGI_PATHSIZE];
  struct fntimg fimg;
  struct fntparm params;
  const char *kptr;

  if (htags != NULL) { *htags = NULL; }
  if (filename == NULL || *filename == '\0') { outerr("loading text: no filename"); return NULL; }

  if ((kptr = strrchr(filename, '/')) != NULL) {
    snprintf(dirname, sizeof(dirname), "%.*s", (int)(size_t)(kptr - filename), filename);
    filename = kptr + 1;
  } else {
    vg4->misc->strcpy(dirname, sizeof(dirname), ".");
  }

  default_params(&params);  /* set to default */

  if (dparam != NULL) {  /* mix it with dparam */
    struct VG_Hash *hsh;
    hsh = vg4_intern_parser_keyval(dparam, strlen(dparam), NULL);
    if (hsh == NULL) { return NULL; }
    get_params(&params, hsh, dirname, hvar);
    vg4->hash->destroy(hsh);
  }

  fimg = ccmdexec_txtfile(filename, filename + strlen(filename), &params, 0, dirname, hvar);
  free_params(&params);

  if (fimg.imgp != NULL) {
    struct VG_Image *imgp;
    if (htags != NULL) {
      if (intolist) {
        *htags = vg4->hash->create();
      } else {
        *htags = vg4_hash_create_nolist();
      }
      transfer_hsub(fimg.hsub, *htags, 0, 0);
    }
    SML3_hashfree(&fimg.hsub);
    if (intolist) {
      imgp = vg4->image->clone(fimg.imgp, NULL, NULL);
      vg4->image->destroy(fimg.imgp);
      fimg.imgp = imgp;
    }
  } else {
    pwarn("\n");
  }

  return fimg.imgp;
} /* Ende font_loadtext_doit */


/* font_totext:
 * create text-image
 * @param text     text
 * @param dparam   default parameters (header), or NULL
 * @param dirname  a start-path from where to load subsequent files, or NULL = actual path
 * @param hvar     hash with variable-values for text control-commands "var", or NULL
 *                   hash-format:
 *                    - key:   variable name
 *                    - value: variable value
 * @param htags    for returning position of tagged images and sub-tags, or NULL
 *                   hash-format:
 *                    - key:   box-tag or sub-tag value
 *                    - value: position as rectangle (struct VG_Rect)
 * @return  text-image or NULL = error
 *
 * The text may contain control-commands:
 *   "%{" <command> [<header>] ":" <data> "%}"
 *   e.g.: %{txt: My text%}
 *         %{txt[border=4]:
 *           My other text
 *           with two lines
 *         %}
 *
 * A missing start control-command assumes command = "txt"
 *
 * If a filename begins with a '/', it is searched from the current directory.
 *
 * Control-commands:
 *  - txtfile:   load another text file
 *                 data = path to text file to load
 *                   (data may contain variables in the format: "$(<varname>)",
 *                    - a specific automatic variable "LANG" contains the (fallback) locale,
 *                      e.g.: %{txtfile: myfile_$(LANG).txt%}
 *                   )
 *  - txt:       text data
 *                 data = embedded text
 *  - imgfile:   load an image from file
 *                 data = path to image file to load
 *                   (data may contain variables in the format: "$(<varname>)",
 *                    - a specific automatic variable "LANG" contains the (fallback) locale,
 *                      e.g.: %{imgfile: myfile_$(LANG).bmp%}
 *                   )
 *  - img:       load image from embedded data
 *                 data = embedded base64-coded image
 *  - mlang:     multilanguage scope:key, will be replaced by appropriate text
 *                 data = <scope> ":" <key>
 *                   (data may contain variables in the format: "$(<varname>)")
 *  - var:       variable: will be replaced by the variable value (from hvar)
 *                 data = variable name
 *  - box:       a box containing control-commands
 *                 data = control-commands
 *  - table:     a table
 *                 header needs "cells"
 *                 data = "cell" control-commands
 *  - cell:      a cell of a table (only as sub-command to "table")
 *                 data = embedded text
 *  - nl:        a newline (only in txt and box, no header and no data)
 *  - sp:        a space (only in txt and box, no header and no data)
 *
 * Header:
 *   A list of key/value pairs
 *     "[" <key> "=" <value> [ <space> <key> "=" <value> [...]] "]"
 *     e.g.: [fgcolor = 0x0000ff font=sys:low]
 *
 *   Header values may contain variables in the format: "$(<varname>)"
 *
 *   Missing header pairs inherit from parent
 *
 *   Following pairs are defined:
 *    - background image
 *      (will not be passed to subsequent control-commands)
 *      - key: "bgimg"
 *      - value: path to image file to load
 *      - default: (empty)
 *    - greatest width of image
 *      (will be passed to subsequent control-commands,
 *      they can overwrite it only with lesser values)
 *      - key: "maxwidth"
 *      - value: width in pixels or percent, or 0 = no limit
 *               e.g. "50" or "50%"
 *      - default: width of window
 *    - width of a box
 *      (will be passed to subsequent control-commands as "maxwidth")
 *      - key: "boxwidth"
 *      - value: width in pixels or percent,
 *               e.g. "50" or "50%"
 *      - default: 0 (no limit)
 *    - height of a box (if no background image)
 *      (will not be passed to subsequent control-commands)
 *      - key: "boxheight"
 *      - value: height in pixels
 *      - default: 0 (no limit)
 *    - border in pixels around (outside) image in background color
 *      (will not be passed to subsequent control-commands)
 *      - key: "border"
 *      - value: border in pixels
 *      - default: 0 (no border)
 *    - rectangle around image
 *      (will be passed to subsequent control-commands)
 *      - key: "rectcolor"
 *      - value: hex number (0xRRGGBB) or "no"
 *      - default: "no"
 *    - font to use
 *      (will be passed to subsequent control-commands)
 *      - key: "font"
 *      - value: font-name
 *      - default: - for VG_WINDOW_SIZE_LOW:  sys:low
 *                 - for VG_WINDOW_SIZE_HIGH: sys:high
 *    - text color
 *      (will be passed to subsequent control-commands)
 *      - key: "fgcolor"
 *      - value: hex number (0xRRGGBB)
 *      - default: 0xffffff
 *    - background color
 *      (will be passed to subsequent control-commands)
 *      - key: "bgcolor"
 *      - value: hex number (0xRRGGBB) or "transparent"
 *      - default: "transparent"
 *    - whether font-characters are bold
 *      (will be passed to subsequent control-commands)
 *      - key: "bold"
 *      - value: "no", "off", "0" or "yes", "on", "1"
 *      - default: "no"
 *    - line pitch to add (in percent of default pitch)
 *      (will be passed to subsequent control-commands)
 *      - key: "linepitch"
 *      - value: line pitch to add,
 *               e.g. "100" will be twice of default pitch
 *      - default: 0
 *    - orientation of embedded text
 *      (will be passed to subsequent control-commands)
 *      - key: "orientation"
 *      - value: "left", "center", "right" or "block"
 *        The value may have an additional orientation appended with '+',
 *        which will be used, if a textline exceeds the width of boxwidth or maxwidth,
 *        e.g.: "center+right"
 *      - default: "left"
 *    - vertical orientation, only if bgimg or boxheight is set or in table-cell
 *      (will not be passed to subsequent control-commands)
 *      - key: "v-orientation"
 *      - value: "top", "center" or "bottom"
 *        The value may have an additional vertical orientation appended with '+',
 *        which will be used, if the text exceeds the boundaries of bgimg or boxheight,
 *        e.g.: "top+bottom"
 *      - default: "center"
 *    - not wrapping lines on separation character
 *      (will be passed to subsequent control-commands)
 *      - key: "nowrap"
 *      - value: "no", "off", "0" or "yes", "on", "1"
 *      - default: "no"
 *    - box-tag
 *      (will not be passed to subsequent control-commands)
 *      - key: "tag"
 *      - value: <arbitrary string>
 *      - default: <empty>
 *    - image-sub-tag (repeated for each sub-tag key)
 *      (will not be passed to subsequent control-commands)
 *      - key: "utag-*" (where "*" is the key of the sub-tag)
 *      - value: <rectangle in the format: "<x>+<w>,<y>+<h>">
 *      - default: <none>
 *    - for a table
 *      - cells of a table
 *        (will not be passed to subsequent control-commands)
 *        - key: "cells"
 *        - value: comma-separated percent-size of each cell,
 *                 e.g. "20,20,50" has three cells in a line,
 *                 the first's size is 20% of the table,
 *                 the seconds's size is 20% of the table,
 *                 the third's size is 50% of the table,
 *                 the missing 10% will be used for cell-distances in background color
 *        - no default, must exist for a table
 *      - raster
 *        (will correct cell-sizes to 100% and clear linepitch)
 *        - line raster
 *          - key: "lineraster"
 *          - value: raster-color: hex number (0xRRGGBB)
 *        - full raster
 *          - key: "fullraster"
 *          - value: raster-color: hex number (0xRRGGBB)
 *
 * Aliases:
 *   Aliases are appended to the control-command after a <ALIAS_SEP>,
 *     e.g. %{txt<ALIAS_SEP>center: Text is centered%}
 *
 *   Following aliases are defined, internally setting header values
 *   - left orientation
 *     - alias: "left"
 *     - sets header to: [orientation=left boxwidth=100%]
 *   - right orientation
 *     - alias: "right"
 *     - sets header to: [orientation=right boxwidth=100%]
 *   - centered orientation
 *     - alias: "center"
 *     - sets header to: [orientation=center boxwidth=100%]
 *   - block orientation
 *     - alias: "block"
 *     - sets header to: [orientation=block boxwidth=100%]
 *
 * Examples:
 *  - file1:
 *    This text is contained in the
 *    implicit control-command "txt".
 *
 *  - file2:
 *    %{txt:
 *    This text has no parameter line(s),
 *    the actual parent values will be used.
 *    %}
 *
 *  - file3:
 *    %{box:
 *    %{txt: Text is left%}
 *    %{nl:%}
 *    %{txt&center[font=sys:low fgcolor=0xffff00 linepitch=100]:
 *    Text is centered,
 *    Line distance is twice of default
 *    %}
 *    %{nl:%}
 *    %{txt: Text is left again%}
 *    %}
 *
 *  - file4 (just an embedded image centered with a darkred background):
 *    %{img&center[bgcolor=0x880000 border=3]:
 *    Qk3KAgAAAAAAAIoAAAB8AAAADAAAAAwAAAABACAAAwAAAEACAAATCwAAEwsAAAAAAAAAAAAAAAD/
 *    AAD/AAD/AAAAAAAA/0JHUnMAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAA
 *    AAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAP//AAD//wAA//8SAP//gwD//9oA////AP///wD//9oA
 *    //+DAP//EgD//wAA//8AAP//AAD//ykA//+4CO/3+SC/3/8/gL//Qnq8/y2k0f8P4O/5Af3+uAD/
 *    /ykA//8AAP//EgD//7ccxuP/XEej/2U1mv9RXK3/T2Cv/11Eof9lM5n/N5HI/wP4/LcA//8SAP//
 *    hBLb7fpfQaD/MpvN/wzn8/8B/f7/AP///wfx+P8dxeL/UVut/zGdzvoA//+EAP//2DqMxf9WVan/
 *    Avv9/wD///8byuT/NZbK/xDf7/8A////J7LY/11Gof8H8PjYAP///0J8vP9RX67/AP///wL8/f8n
 *    sdj/LKfT/x7D4f8A////JrTZ/2U2mf8I7/f/AP///xnM5f8ewuD/DOfz/yO23P8bx+P/BfT6/wvo
 *    8/8hut7/KavV/yiv1/8D+fz/AP//2AD///8G8vn/WVGl/agAV+N+HYD1H8Dg/0tqtP6jAFzliBN2
 *    8SWz2v8A///YAP//hAD///oF9Pr/S2y0/ooZde5nQZf5FNbr/zmNxv+FHXnvdSyK9x7C4foA//+E
 *    AP//EgD//7cD+Pz/JrLY/zeNx/8U1er/AP///wP4/P8rptP/MJ7P/wnt9rcA//8SAP//AAD//ykA
 *    //+4Avr9+QP4/P8B/f7/AP///wD///8D+vz5A/j8uAD+/ykA/v8AAP//AAD//wAA//8SAP//gwD/
 *    /9oA////AP///wD//9oA//+DAP//EgD//wAA//8A
 *    %}
 *
 *  - file5:
 *    # A table with 3 columns and a raster
 *    %{table[cells=25,25,50 boxwidth=80% fullraster=0x888888]:
 *    %{cell[bgcolor=0x440000]:
 *    Cell 1 in Line 1
 *    %}
 *    %{cell[bgcolor=0x444400]:
 *    Cell 2 in Line 1
 *    %}
 *    %{cell[bgcolor=0x000044]:
 *    Cell 3 in Line 1
 *    %}
 *    %{cell[bgcolor=0x440000]:
 *    Cell 1 in Line 2
 *    %}
 *    %{cell[bgcolor=0x444400]:
 *    Cell 2 in Line 2
 *    %}
 *    %{cell[bgcolor=0x000044]:
 *    Cell 3 in Line 2
 *    %}
 *    %}
 */
static struct VG_Image *
font_totext(const char *text, const char *dparam, const char *dirname, struct VG_Hash *hvar, struct VG_Hash **htags)
{
  return font_totext_doit(text, dparam, dirname, hvar, htags, VG_TRUE);
} /* Ende font_totext */


/* font_totext_doit: do action */
static struct VG_Image *
font_totext_doit(const char *text, const char *dparam, const char *dirname, struct VG_Hash *hvar, struct VG_Hash **htags, VG_BOOL intolist)
{
  struct fntimg fimg;
  struct fntparm params;
  size_t tlen;

  if (htags != NULL) { *htags = NULL; }
  if (text == NULL || *text == '\0') { outerr("no text passed"); return NULL; }
  if (dirname == NULL || *dirname == '\0') { dirname = "."; }

  default_params(&params);  /* set to default */

  if (dparam != NULL) {  /* mix it with dparam */
    struct VG_Hash *hsh;
    hsh = vg4_intern_parser_keyval(dparam, strlen(dparam), NULL);
    if (hsh == NULL) { return NULL; }
    get_params(&params, hsh, dirname, hvar);
    vg4->hash->destroy(hsh);
  }

  tlen = strlen(text);

  fimg = ccmdexec_txt(text, text + tlen + 1, &params, 0, dirname, hvar);
  free_params(&params);

  if (fimg.imgp != NULL) {
    struct VG_Image *imgp;
    if (htags != NULL) {
      if (intolist) {
        *htags = vg4->hash->create();
      } else {
        *htags = vg4_hash_create_nolist();
      }
      transfer_hsub(fimg.hsub, *htags, 0, 0);
    }
    SML3_hashfree(&fimg.hsub);
    if (intolist) {
      imgp = vg4->image->clone(fimg.imgp, NULL, NULL);
      vg4->image->destroy(fimg.imgp);
      fimg.imgp = imgp;
    }
  } else {
    pwarn("\n");
  }

  return fimg.imgp;
} /* Ende font_totext_doit */


/* free-function for fntimg.hsub */
static void
free_hsub(void *vptr)
{
  struct SML3_hash **val = (struct SML3_hash **)vptr;
  if (val != NULL && *val != NULL) { SML3_hashfree(val); }
} /* Ende free_hsub */


/* transfers from fntimg.hsub to htags */
static void
transfer_hsub(struct SML3_hash *hsub, struct VG_Hash *htags, int xplus, int yplus)
{
  struct SML3_hash **hsval;
  struct SML3_hashelem *he1;
  struct subpos sps;

  if (hsub == NULL || htags == NULL) { return; }

  for (he1 = SML3_hashlist(hsub, NULL, 0); he1 != NULL; he1 = SML3_hashlist(hsub, he1, 0)) {
    sps = *(struct subpos *)SML3_hashelem_keyget(he1, NULL);
    hsval = (struct SML3_hash **)SML3_hashelem_valget(he1, NULL);
    sps.rect.x += xplus;
    sps.rect.y += yplus;
    if (*sps.itag != '\0') {
      vg4->hash->set(htags, sps.itag, &sps.rect, sizeof(sps.rect));
    }
    if (hsval != NULL && *hsval != NULL) {
      transfer_hsub(*hsval, htags, sps.rect.x, sps.rect.y);
    }
  }
} /* Ende transfer_hsub */


/* set parameters to default */
static void
default_params(struct fntparm *params)
{
  int winw;

  if (params == NULL) { return; }

  vg4->window->getsize(&winw, NULL);
  params->bgimg = NULL;
  params->maxwidth = winw;
  params->boxwidth = 0;
  params->boxheight = 0;
  params->border = 0;
  params->rectcolor = VG_COLOR_TRANSPARENT;
  params->fntp = vg4_font_loaddefault();
  params->fgcolor = VG_COLOR_WHITE;
  params->bgcolor = VG_COLOR_TRANSPARENT;
  params->bold = VG_FALSE;
  params->line_pitch = 0;
  params->orientation1 = params->orientation2 = VG_ORI_LEFT;
  params->v_orientation1 = params->v_orientation2 = VG_ORI_CENTER;
  params->nowrap = VG_FALSE;
  memset(&params->table, 0, sizeof(params->table));
  memset(params->itag, 0, sizeof(params->itag));
  params->hutag = NULL;
} /* Ende default_params */


/* copy parameters from params to nparams */
static void
copy_params(struct fntparm *nparams, const struct fntparm *params)
{
  if (nparams == NULL || params == NULL) { return; }

  *nparams = *params;

  /* don't pass following to sub-commands */
  nparams->bgimg = NULL;
  if (nparams->boxwidth > 0) {  /* set maxwidth to boxwidth if greater */
    if (nparams->maxwidth == 0 || nparams->maxwidth > nparams->boxwidth) {
      nparams->maxwidth = nparams->boxwidth;
    }
    nparams->boxwidth = 0;
  }
  nparams->boxheight = 0;
  nparams->border = 0;
  nparams->v_orientation1 = nparams->v_orientation2 = VG_ORI_CENTER;
  memset(&nparams->table, 0, sizeof(nparams->table));
  memset(nparams->itag, 0, sizeof(nparams->itag));
  nparams->hutag = NULL;
} /* Ende copy_params */


/* update params with parameters from hparam */
static void
get_params(struct fntparm *params, struct VG_Hash *hparam, const char *dirname, struct VG_Hash *hvar)
{
  const char *key;
  char *pval;
  int ival, winw;
  struct SML3_gummi gm = SML3_GUM_INITIALIZER;

  if (params == NULL || hparam == NULL) { return; }

  vg4->window->getsize(&winw, NULL);

  /* background image */
  key = "bgimg";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      char imgfile[VGI_PATHSIZE];
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      set_file_with_path(imgfile, sizeof(imgfile), dirname, pval);
      params->bgimg = vg4_image_load_nolist(imgfile);
      if (params->bgimg != NULL) {
        vg4->image->getsize(params->bgimg, NULL, &ival, NULL);
        if (params->maxwidth == 0 || ival < params->maxwidth) {
          params->maxwidth = ival;
        }
      }
    }
  }

  /* height of box, if no background image */
  if (params->bgimg == NULL) {
    key = "boxheight";
    if (vg4->hash->isset(hparam, key)) {
      ival = 0;
      pval = (char *)vg4->hash->get(hparam, key, NULL);
      if (pval != NULL) {
        vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
        ival = atoi(pval);
        if (ival >= 0) { params->boxheight = ival; }
      }
    }
  }

  /* greatest width of image */
  key = "maxwidth";
  if (vg4->hash->isset(hparam, key)) {
    ival = 0;
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      ival = atoi(pval);
      if (ival > 0) {
        if (strchr(pval, '%') != NULL) {
          if (ival > 100) { ival = 100; }
          if (params->maxwidth == 0) { params->maxwidth = winw; }
          ival = params->maxwidth * ival / 100;
        } else {
          if (ival > winw) { ival = winw; }
        }
      } else if (strcmp(pval, "0") == 0) {
        params->maxwidth = 0;
      }
    }
    if (ival > 0) {
      if (params->maxwidth == 0 || ival < params->maxwidth) {
        params->maxwidth = ival;
      }
    }
  }

  /* width of box */
  key = "boxwidth";
  if (vg4->hash->isset(hparam, key)) {
    ival = 0;
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      ival = atoi(pval);
      if (ival > 0) {
        if (strchr(pval, '%') != NULL) {
          if (ival > 100) { ival = 100; }
          params->boxwidth = params->maxwidth;
          if (params->boxwidth == 0) { params->boxwidth = winw; }
          ival = params->boxwidth * ival / 100;
        } else {
          if (params->maxwidth > 0 && ival > params->maxwidth) { ival = params->maxwidth; }
          if (ival > winw) { ival = winw; }
        }
        params->boxwidth = ival;
        /* set also maxwidth */
        params->maxwidth = params->boxwidth;
      }
    }
  }

  /* border */
  key = "border";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      ival = atoi(pval);
      if (ival > 0 && params->maxwidth > 0 && params->maxwidth < ival * 2 + 1) {
        ival = (params->maxwidth - 1) / 2;
      }
      if (ival > 0 && ival > (winw - 1) / 2) { ival = (winw - 1) / 2; }
      if (ival > 0 && params->maxwidth > 0) {
        params->maxwidth -= (ival * 2);
        if (params->boxwidth > 0) { params->boxwidth = params->maxwidth; }
      }
      params->border = ival;
    }
  }

  /* rectangle color */
  key = "rectcolor";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      if (strcmp(pval, "no") == 0 || strcmp(pval, "off") == 0 || strcmp(pval, "transparent") == 0) {
        params->rectcolor = VG_COLOR_TRANSPARENT;
      } else {
        params->rectcolor = (int)strtol(pval, NULL, 16);
      }
    }
  }

  /* font */
  key = "font";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      struct VG_Font *fntp;
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      fntp = vg4->font->load(pval);
      if (fntp != NULL) { params->fntp = fntp; }
    }
  }

  /* text color */
  key = "fgcolor";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      params->fgcolor = (int)strtol(pval, NULL, 16);
    }
  }

  /* background color */
  key = "bgcolor";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      if (strcmp(pval, "no") == 0 || strcmp(pval, "off") == 0 || strcmp(pval, "transparent") == 0) {
        params->bgcolor = VG_COLOR_TRANSPARENT;
      } else {
        params->bgcolor = (int)strtol(pval, NULL, 16);
      }
    }
  }

  /* whether is bold */
  key = "bold";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      if (strcmp(pval, "no") == 0 || strcmp(pval, "off") == 0 || strcmp(pval, "0") == 0) {
        params->bold = VG_FALSE;
      } else if (strcmp(pval, "yes") == 0 || strcmp(pval, "on") == 0 || strcmp(pval, "1") == 0) {
        params->bold = VG_TRUE;
      }
    }
  }

  /* line pitch */
  key = "linepitch";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      params->line_pitch = atoi(pval);
    }
  }

  /* orientation of embedded text */
  key = "orientation";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      char *pval2;
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      if ((pval2 = strchr(pval, '+')) == NULL) { pval2 = pval + strlen(pval); } else { pval2++; }
      ival = 0;
      if (strncmp(pval, "left", 4) == 0) {
        ival = VG_ORI_LEFT;
      } else if (strncmp(pval, "center", 6) == 0) {
        ival = VG_ORI_CENTER;
      } else if (strncmp(pval, "right", 5) == 0) {
        ival = VG_ORI_RIGHT;
      } else if (strncmp(pval, "block", 5) == 0) {
        ival = VG_ORI_BLOCK;
      }
      if (ival > 0) { params->orientation1 = params->orientation2 = ival; }
      ival = 0;
      if (strncmp(pval2, "left", 4) == 0) {
        ival = VG_ORI_LEFT;
      } else if (strncmp(pval2, "center", 6) == 0) {
        ival = VG_ORI_CENTER;
      } else if (strncmp(pval2, "right", 5) == 0) {
        ival = VG_ORI_RIGHT;
      } else if (strncmp(pval2, "block", 5) == 0) {
        ival = VG_ORI_BLOCK;
      }
      if (ival > 0) { params->orientation2 = ival; }
    }
  }

  /* vertical orientation of embedded text, only if bgimg or boxheight is set */
  if (params->bgimg != NULL || params->boxheight > 0) {
    key = "v-orientation";
    if (vg4->hash->isset(hparam, key)) {
      pval = (char *)vg4->hash->get(hparam, key, NULL);
      if (pval != NULL) {
        char *pval2;
        vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
        if ((pval2 = strchr(pval, '+')) == NULL) { pval2 = pval + strlen(pval); } else { pval2++; }
        ival = 0;
        if (strncmp(pval, "top", 3) == 0) {
          ival = VG_ORI_LEFT;
        } else if (strncmp(pval, "center", 6) == 0) {
          ival = VG_ORI_CENTER;
        } else if (strncmp(pval, "bottom", 6) == 0) {
          ival = VG_ORI_RIGHT;
        }
        if (ival > 0) { params->v_orientation1 = params->v_orientation2 = ival; }
        ival = 0;
        if (strncmp(pval2, "top", 3) == 0) {
          ival = VG_ORI_LEFT;
        } else if (strncmp(pval2, "center", 6) == 0) {
          ival = VG_ORI_CENTER;
        } else if (strncmp(pval2, "bottom", 6) == 0) {
          ival = VG_ORI_RIGHT;
        }
        if (ival > 0) { params->v_orientation2 = ival; }
      }
    }
  }

  /* whether not wrapping lines */
  key = "nowrap";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      if (strcmp(pval, "no") == 0 || strcmp(pval, "off") == 0 || strcmp(pval, "0") == 0) {
        params->nowrap = VG_FALSE;
      } else if (strcmp(pval, "yes") == 0 || strcmp(pval, "on") == 0 || strcmp(pval, "1") == 0) {
        params->nowrap = VG_TRUE;
      }
    }
  }

  /* line-raster color */
  key = "lineraster";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      params->table.rastercolor = (int)strtol(pval, NULL, 16);
      params->table.doraster = 1;
    }
  }

  /* full-raster color */
  key = "fullraster";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      params->table.rastercolor = (int)strtol(pval, NULL, 16);
      params->table.doraster = 2;
    }
  }

  /* cells */
  key = "cells";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      struct SML3_gummi gm1 = SML3_GUM_INITIALIZER;
      struct SML3_gummi gm2 = SML3_GUM_INITIALIZER;
      const char *saveptr;
      char *tstring;
      int gespz, maxc, posc, nges;
      memset(params->table.cells, 0, sizeof(params->table.cells));
      vg4_intern_str_with_var(&gm1, pval, hvar, NULL);
      saveptr = SML3_gumgetval(&gm1);
      gespz = maxc = 0;
      while ((tstring = SML3_string_toks(&saveptr, ",", &gm2)) != NULL) {
        params->table.cells[maxc] = atoi(tstring);
        if (params->table.cells[maxc] <= 0) { params->table.cells[maxc] = 1; }
        gespz += params->table.cells[maxc];
        if (++maxc == MAX_CELLS) { break; }
      }
      SML3_gumdest(&gm2);
      SML3_gumdest(&gm1);
      if (gespz > 100) {  /* sum is more than 100% */
        nges = 0;
        for (posc = 0; posc < maxc; posc++) {
          params->table.cells[posc] = params->table.cells[posc] * 100 / gespz;
          if (params->table.cells[posc] == 0) { params->table.cells[posc] = 1; }
          nges += params->table.cells[posc];
        }
        if (nges < 100) { params->table.cells[maxc - 1] += (100 - nges); }
      } else if (params->table.doraster > 0 && gespz < 100) {  /* resize to 100% */
        nges = 0;
        for (posc = 0; posc < maxc; posc++) {
          params->table.cells[posc] = params->table.cells[posc] + (100 - gespz) / maxc;
          nges += params->table.cells[posc];
        }
        if (nges < 100) { params->table.cells[maxc - 1] += (100 - nges); }
      }
      if (params->table.doraster > 0) { params->line_pitch = 0; }
    }
  }

  /* box-tag */
  key = "tag";
  if (vg4->hash->isset(hparam, key)) {
    pval = (char *)vg4->hash->get(hparam, key, NULL);
    if (pval != NULL) {
      vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
      vg4->misc->strcpy(params->itag, sizeof(params->itag), pval);
    }
  }

  /* sub-tags */
  key = "utag-";
  { void *vpos;
    const char *ukey; 
    size_t klen;
    struct VG_Rect urect;
    klen = strlen(key);
    vpos = NULL;
    for (ukey = vg4->hash->list(hparam, &vpos); vpos != NULL; ukey = vg4->hash->list(hparam, &vpos)) {
      if (strncmp(ukey, key, klen) == 0 && ukey[klen] != '\0') {
        pval = (char *)vg4->hash->get(hparam, ukey, NULL);
        if (pval != NULL) {
          vg4_intern_str_with_var(&gm, pval, hvar, NULL); pval = SML3_gumgetval(&gm);
          if (params->hutag == NULL) { params->hutag = vg4_hash_create_nolist(); }
          urect = vg4_intern_read_rect(pval);
          if (urect.w > 0 && urect.h > 0) {  /* not empty */
            vg4->hash->set(params->hutag, ukey + klen, &urect, sizeof(urect));
          }
        }
      }
    }
  }

  SML3_gumdest(&gm);
} /* Ende get_params */


/* free parameters */
static void
free_params(struct fntparm *params)
{
  if (params == NULL) { return; }
  if (params->bgimg != NULL) {
    vg4->image->destroy(params->bgimg);
    params->bgimg = NULL;
  }
  if (params->hutag != NULL) {
    vg4->hash->destroy(params->hutag);
    params->hutag = NULL;
  }
} /* Ende free_params */


/* create text-lines */
static struct textlines *
create_textlines(void)
{
  struct textlines *tlins;

  tlins = SML3_calloc(1, sizeof(*tlins));
  tlins->fntlist = SML3_malloc(sizeof(*tlins->fntlist));
  tlins->fntlist[tlins->fnth] = NULL;
  tlins->lastsep_fntw = -1;

  return tlins;
} /* Ende create_textlines */


/* add new line to textlines */
static void
addline_textlines(struct textlines *tlins)
{
  if (tlins == NULL || tlins->fntlist == NULL) { return; }

  tlins->fnth++;
  tlins->fntlist = SML3_realloc(tlins->fntlist, (tlins->fnth + 1) * sizeof(*tlins->fntlist));
  tlins->fntlist[tlins->fnth] = NULL;
} /* Ende addline_textlines */


/* add new element to the actual line of textlines */
static void
addto_textlines(struct textlines *tlins)
{
  if (tlins == NULL || tlins->fntlist == NULL) { return; }

  if (tlins->fntlist[tlins->fnth] == NULL) {  /* initialize new line */
    tlins->fntw = 0;
    tlins->fntlist[tlins->fnth] = SML3_malloc(sizeof(**tlins->fntlist));
    tlins->linew = tlins->lineh = 0;
    tlins->lastsep_fntw = -1;
    tlins->fntlist[tlins->fnth][tlins->fntw].linew = 0;
    tlins->fntlist[tlins->fnth][tlins->fntw].lineh = 0;
    tlins->fntlist[tlins->fnth][tlins->fntw].gaptop = 0;
    tlins->fntlist[tlins->fnth][tlins->fntw].gapbottom = 0;
  } else {  /* increment line size */
    tlins->fntw++;
    tlins->fntlist[tlins->fnth] = SML3_realloc(tlins->fntlist[tlins->fnth], (tlins->fntw + 1) * sizeof(**tlins->fntlist));
    tlins->fntlist[tlins->fnth][tlins->fntw].linew = tlins->fntlist[tlins->fnth][tlins->fntw - 1].linew;
    tlins->fntlist[tlins->fnth][tlins->fntw].lineh = tlins->fntlist[tlins->fnth][tlins->fntw - 1].lineh;
    tlins->fntlist[tlins->fnth][tlins->fntw].gaptop = tlins->fntlist[tlins->fnth][tlins->fntw - 1].gaptop;
    tlins->fntlist[tlins->fnth][tlins->fntw].gapbottom = tlins->fntlist[tlins->fnth][tlins->fntw - 1].gapbottom;
  }
  tlins->fntlist[tlins->fnth][tlins->fntw].flag = 0;
  memset(&tlins->fntlist[tlins->fnth][tlins->fntw].u, 0, sizeof(tlins->fntlist[tlins->fnth][tlins->fntw].u));
  memset(tlins->fntlist[tlins->fnth][tlins->fntw].itag, 0, sizeof(tlins->fntlist[tlins->fnth][tlins->fntw].itag));
  tlins->fntlist[tlins->fnth][tlins->fntw].hutag = NULL;
} /* Ende addto_textlines */


/* destroy text-lines */
static void
free_textlines(struct textlines *tlins)
{
  int fntw, fnth;

  if (tlins == NULL) { return; }
  if (tlins->fntlist == NULL) { free(tlins); return; }

  for (fnth = 0; fnth <= tlins->fnth; fnth++) {
    if (tlins->fntlist[fnth] == NULL) { break; }  /* end-line */
    for (fntw = 0; tlins->fntlist[fnth][fntw].flag > 0; fntw++) {
      if (tlins->fntlist[fnth][fntw].flag == 2) {  /* image */
        vg4->image->destroy(tlins->fntlist[fnth][fntw].u.fimg.imgp);
        SML3_hashfree(&tlins->fntlist[fnth][fntw].u.fimg.hsub);
      }
      if (tlins->fntlist[fnth][fntw].hutag != NULL) {
        vg4->hash->destroy(tlins->fntlist[fnth][fntw].hutag);
      }
    }
    free(tlins->fntlist[fnth]);
  }
  free(tlins->fntlist);
  free(tlins);
} /* Ende free_textlines */


/* close text-line */
static void
line_close(const struct VG_Font *fntp, struct textlines *tlins, int lpitch_add)
{
  int linew, lineh;
  int gaptop, gapbottom;

  if (fntp == NULL || tlins == NULL || tlins->fntlist == NULL) { return; }

  if (tlins->fntlist[tlins->fnth] == NULL) {  /* no elements in line */
    char key[16];
    struct vgi_font *vgft;
    snprintf(key, sizeof(key), "%d", VGI_FONT_SEPSPACE);
    vgft = (struct vgi_font *)vg4->hash->get(fntp->hfont, key, NULL);
    if (vgft != NULL) { lineh = vgft->h; } else { lineh = 8; }
    linew = 1;
    gaptop = gapbottom = 0;
  } else {  /* elements in line */
    linew = tlins->fntlist[tlins->fnth][tlins->fntw].linew;
    lineh = tlins->fntlist[tlins->fnth][tlins->fntw].lineh;
    gaptop = tlins->fntlist[tlins->fnth][tlins->fntw].gaptop;
    gapbottom = tlins->fntlist[tlins->fnth][tlins->fntw].gapbottom;
  }

  /* end-element */
  addto_textlines(tlins);
  tlins->fntlist[tlins->fnth][tlins->fntw].flag = 0;
  memset(&tlins->fntlist[tlins->fnth][tlins->fntw].u, 0, sizeof(tlins->fntlist[tlins->fnth][tlins->fntw].u));
  tlins->fntlist[tlins->fnth][tlins->fntw].linew = linew;
  tlins->fntlist[tlins->fnth][tlins->fntw].lineh = lineh;
  tlins->fntlist[tlins->fnth][tlins->fntw].gaptop = gaptop;
  tlins->fntlist[tlins->fnth][tlins->fntw].gapbottom = gapbottom;

  /* update imgw and imgh */
  if (tlins->imgw < linew) { tlins->imgw = linew; }
  tlins->imgh += lineh + gaptop + gapbottom + lpitch_add;

  /* end-line */
  addline_textlines(tlins);
} /* Ende line_close */


/* create image with text */
static struct fntimg
fill_text(struct textlines *tlins, const struct fntparm *params, int lpitch_add)
{
  const VG_BOOL sniph = VG_TRUE;
  const int adddiv = 100;
  struct fntimg fimg;
  int imgh;
  int fntw, fnth;
  int linew, lineh;
  int begx, addx;
  int vgx, vgy, vgw, vgh, ptcount;
  struct VG_Point *pts;
  unsigned char *data;
  int imgx, imgy, i_ori;

  memset(&fimg, 0, sizeof(fimg));
  if (tlins == NULL || tlins->fntlist == NULL || params == NULL) { return fimg; }

  /* create image */
  imgh = 0;
  if (params->boxwidth > 0) {
    tlins->imgw = params->boxwidth;
  } else if (params->maxwidth > 0) {
    if (tlins->imgw > params->maxwidth) { tlins->imgw = params->maxwidth; }
  }
  if (params->bgimg != NULL) {
    int iw, ih;
    vg4->image->getsize(params->bgimg, NULL, &iw, &ih);
    if (iw > tlins->imgw) { tlins->imgw = iw; }
    if (ih < tlins->imgh) { i_ori = params->v_orientation2; } else { i_ori = params->v_orientation1; }
    if (ih > tlins->imgh || sniph) {
      if (i_ori == VG_ORI_LEFT) {  /* top */
        imgh = 0;
      } else if (i_ori == VG_ORI_RIGHT) {  /* bottom */
        imgh = ih - tlins->imgh;
      } else {
        imgh = (ih - tlins->imgh) / 2;
      }
      tlins->imgh = ih;
    }
  } else if (params->boxheight > 0) {
    if (params->boxheight > tlins->imgh || sniph) {
      if (params->boxheight < tlins->imgh) { i_ori = params->v_orientation2; } else { i_ori = params->v_orientation1; }
      if (i_ori == VG_ORI_LEFT) {  /* top */
        imgh = 0;
      } else if (i_ori == VG_ORI_RIGHT) {  /* bottom */
        imgh = params->boxheight - tlins->imgh;
      } else {
        imgh = (params->boxheight - tlins->imgh) / 2;
      }
      tlins->imgh = params->boxheight;
    }
  }
  fimg.imgp = vg4_image_create_nolist(tlins->imgw, tlins->imgh);
  vg4->image->fill(fimg.imgp, params->bgcolor);
  if (params->bgimg != NULL) { vg4->image->copy(fimg.imgp, params->bgimg, NULL, NULL); }
  fimg.hsub = SML3_hashnew(free_hsub, 0);

  /* fill image */
  for (fnth = 0; tlins->fntlist[fnth] != NULL; fnth++) {  /* for each text-line */

    /* get max width and height of text-line */
    for (fntw = 0; tlins->fntlist[fnth][fntw].flag > 0; fntw++) {;}
    linew = tlins->fntlist[fnth][fntw].linew;
    lineh = tlins->fntlist[fnth][fntw].lineh;
    imgh += tlins->fntlist[fnth][fntw].gaptop;

    if (sniph && imgh >= tlins->imgh) { break; }

    /* orientation */
    begx = addx = 0;
    if (tlins->imgw < linew) { i_ori = params->orientation2; } else { i_ori = params->orientation1; }
    switch(i_ori) {
      case VG_ORI_RIGHT:
        begx = (tlins->imgw - linew) * adddiv;
        break;
      case VG_ORI_CENTER:
        begx = (tlins->imgw - linew) * adddiv / 2;
        break;
      case VG_ORI_BLOCK:
        addx = (tlins->imgw - linew) * adddiv;
        if (addx > 0 && fntw > 1) {
          addx /= (fntw - 1);
        } else {
          addx = 0;
        }
        break;
    }

    /* draw text-line */
    for (fntw = 0; tlins->fntlist[fnth][fntw].flag > 0; fntw++) {
      if (tlins->fntlist[fnth][fntw].flag == 1) {  /* font-character */
        vgw = tlins->fntlist[fnth][fntw].u.vgft->w;
        vgh = tlins->fntlist[fnth][fntw].u.vgft->h;
        if (vgw <= 0 || vgh <= 0) { continue; }
        if (params->bold) {
          pts = SML3_calloc((vgw * 2) * vgh, sizeof(*pts));
        } else {
          pts = SML3_calloc(vgw * vgh, sizeof(*pts));
        }
        ptcount = 0;
        imgx = tlins->fntlist[fnth][fntw].linew - tlins->fntlist[fnth][fntw].u.vgft->w;
        imgx += (begx / adddiv);
        imgy = (lineh - tlins->fntlist[fnth][fntw].u.vgft->h) / 2;  /* valign centered */
        imgy += imgh;
        for (vgy = 0; vgy < vgh; vgy++) {
          data = &tlins->fntlist[fnth][fntw].u.vgft->data[vgy * vgw];
          for (vgx = 0; vgx < vgw; vgx++) {
            if (data[vgx]) {
              pts[ptcount].x = imgx + vgx;
              pts[ptcount].y = imgy + vgy;
              ptcount++;
              if (params->bold) {
                pts[ptcount].x = imgx + vgx + 1;
                pts[ptcount].y = imgy + vgy;
                ptcount++;
              }
            }
          }
        }
        vg4->image->draw_points(fimg.imgp, pts, ptcount, params->fgcolor);
        free(pts);

      } else if (tlins->fntlist[fnth][fntw].flag == 2) {  /* image */
        int iw, ih;
        struct VG_Position posd;
        vg4->image->getsize(tlins->fntlist[fnth][fntw].u.fimg.imgp, NULL, &iw, &ih);
        if (sniph && ih > lineh) { ih = lineh; }
        imgx = tlins->fntlist[fnth][fntw].linew - iw;
        imgx += (begx / adddiv);
        imgy = (lineh - ih) / 2;  /* valign centered */
        imgy += imgh;
        posd.x = imgx;
        posd.y = imgy;
        posd.pos = VG_POS_UPPER_LEFT;
        vg4->image->copy(fimg.imgp, tlins->fntlist[fnth][fntw].u.fimg.imgp, &posd, NULL);

        /* image or sub-image tagged? */
        if (*tlins->fntlist[fnth][fntw].itag != '\0'
            || tlins->fntlist[fnth][fntw].hutag != NULL
            || tlins->fntlist[fnth][fntw].u.fimg.hsub != NULL) {
          struct subpos sps;
          struct SML3_hashelem *he1;
          vg4->misc->strcpy(sps.itag, sizeof(sps.itag), tlins->fntlist[fnth][fntw].itag);
          sps.rect.x = posd.x + params->border; sps.rect.w = iw;
          sps.rect.y = posd.y + params->border; sps.rect.h = ih;
          he1 = SML3_hashset(fimg.hsub, &sps, sizeof(sps));
          SML3_hashelem_valset(he1, &tlins->fntlist[fnth][fntw].u.fimg.hsub, sizeof(tlins->fntlist[0][0].u.fimg.hsub));
          tlins->fntlist[fnth][fntw].u.fimg.hsub = NULL;
          /* sub-tags? */
          if (tlins->fntlist[fnth][fntw].hutag != NULL) {
            struct SML3_hash **hsval = (struct SML3_hash **)SML3_hashelem_valget(he1, NULL);
            if (hsval != NULL) {
              void *vpos;
              const char *ukey; 
              struct VG_Rect *prect;
              if (*hsval == NULL) { *hsval = SML3_hashnew(free_hsub, 0); }
              vpos = NULL;
              for (ukey = vg4->hash->list(tlins->fntlist[fnth][fntw].hutag, &vpos);
                   vpos != NULL;
                   ukey = vg4->hash->list(tlins->fntlist[fnth][fntw].hutag, &vpos)) {
                prect = (struct VG_Rect *)vg4->hash->get(tlins->fntlist[fnth][fntw].hutag, ukey, NULL);
                if (prect != NULL) {
                  vg4->misc->strcpy(sps.itag, sizeof(sps.itag), ukey);
                  sps.rect = *prect;
                  SML3_hashset(*hsval, &sps, sizeof(sps));
                }
              }
            }
          }
        }
      }
      begx += addx;
    }

    /* position of next text-line */
    imgh += lineh;
    imgh += tlins->fntlist[fnth][fntw].gapbottom;
    imgh += lpitch_add;
    if (sniph && imgh >= tlins->imgh) { break; }
  }
  imgh -= lpitch_add;  /* last line has lpitch_add = 0 */

  /* free hash, if empty */
  if (SML3_hashlist(fimg.hsub, NULL, 0) == NULL) { SML3_hashfree(&fimg.hsub); fimg.hsub = NULL; }

  return fimg;
} /* Ende fill_text */


/* set width and orientation for image not created with fill_text() */
static struct VG_Image *
add_ori(struct VG_Image *imgp, int boxwidth, int maxwidth, int orientation1, int orientation2, int bgcolor)
{
  struct VG_Image *imgpn;
  int imgw, imgh, imgwn, i_ori;
  struct VG_Position posd;

  if (imgp == NULL || maxwidth <= 0) { return imgp; }

  vg4->image->getsize(imgp, NULL, &imgw, &imgh);

  imgwn = imgw;
  if (boxwidth > imgw) {
    imgwn = boxwidth;
  } else if (maxwidth < imgw) {
    imgwn = maxwidth;
  } else if (bgcolor == VG_COLOR_TRANSPARENT) {
    return imgp;
  }

  /* calculate position according to orientation */
  posd.x = posd.y = 0;
  posd.pos = VG_POS_UPPER_LEFT;
  if (imgwn < imgw) { i_ori = orientation2; } else { i_ori = orientation1; }
  switch(i_ori) {
    case VG_ORI_RIGHT:
      posd.x = imgwn - 1;
      posd.pos = VG_POS_UPPER_RIGHT;
      break;
    case VG_ORI_CENTER:
      posd.x = imgwn / 2;
      posd.y = imgh / 2;
      posd.pos = VG_POS_CENTERED;
      break;
  }

  imgpn = vg4_image_create_nolist(imgwn, imgh);
  vg4->image->fill(imgpn, bgcolor);

  vg4->image->copy(imgpn, imgp, &posd, NULL);
  vg4->image->destroy(imgp);

  return imgpn;
} /* Ende add_ori */


/* add border to image returning new image and destroying old one */
static struct VG_Image *
add_border(struct VG_Image *imgp, int border, int bgcolor)
{
  struct VG_Image *imgpn;
  int imgw, imgh;

  if (imgp == NULL || border <= 0) { return imgp; }

  vg4->image->getsize(imgp, NULL, &imgw, &imgh);

  imgpn = vg4_image_create_nolist(imgw + (border * 2), imgh + (border * 2));
  vg4->image->fill(imgpn, bgcolor);

  vg4->image->copy(imgpn, imgp, NULL, NULL);
  vg4->image->destroy(imgp);

  return imgpn;
} /* Ende add_border */


/* draw raster into image */
static void
add_raster(struct VG_Image *imgp, struct textlines *tlins, int rastercolor, int lpitch_add, VG_BOOL fullraster)
{
  const int adddiv = 100;
  int imgh;
  int fntw, fnth;
  int linew, lineh;
  int begx, addx;
  int x1, y1, y2, iw;
  struct VG_Rect rect;

  if (imgp == NULL) { return; }
  if (tlins == NULL || tlins->fntlist == NULL) { return; }
  if (rastercolor < 0 || rastercolor == VG_COLOR_TRANSPARENT) { return; }

  /* draw raster */

  rect.x = rect.y = 0;
  rect.w = tlins->imgw;
  rect.h = tlins->imgh;
  vg4->image->draw_rect(imgp, &rect, rastercolor, VG_FALSE);
  rect.x = rect.y = 1;
  rect.w = tlins->imgw - 2;
  rect.h = tlins->imgh - 2;
  vg4->image->draw_rect(imgp, &rect, rastercolor, VG_FALSE);

  imgh = 0;
  for (fnth = 0; tlins->fntlist[fnth] != NULL; fnth++) {  /* for each text-line */
    /* start y-position */
    y1 = imgh;

    for (fntw = 0; tlins->fntlist[fnth][fntw].flag > 0; fntw++) {;}
    lineh = tlins->fntlist[fnth][fntw].lineh;
    linew = tlins->fntlist[fnth][fntw].linew;
    imgh += tlins->fntlist[fnth][fntw].gaptop;

    begx = 0;
    addx = (tlins->imgw - linew) * adddiv;
    if (addx > 0 && fntw > 1) {
      addx /= (fntw - 1);
    } else {
      addx = 0;
    }

    /* end y-position */
    y2 = imgh + lineh + tlins->fntlist[fnth][fntw].gapbottom + lpitch_add;
    if (y2 >= tlins->imgh) { y2 = tlins->imgh - 1; }

    /* draw vertical lines */
    if (fullraster) {
      for (fntw = 0; tlins->fntlist[fnth][fntw].flag > 0; fntw++) {
        if (tlins->fntlist[fnth][fntw + 1].flag > 0) {
          linew = tlins->fntlist[fnth][fntw].linew;
          if (tlins->fntlist[fnth][fntw + 1].flag == 1) {  /* font-character */
            iw = tlins->fntlist[fnth][fntw + 1].u.vgft->w;
          } else if (tlins->fntlist[fnth][fntw + 1].flag == 2) {  /* image */
            vg4->image->getsize(tlins->fntlist[fnth][fntw + 1].u.fimg.imgp, NULL, &iw, NULL);
          }
          x1 = linew + ((tlins->fntlist[fnth][fntw + 1].linew - iw) - linew) / 2;
          /* draw line */
          x1 = x1 + ((begx + addx / 2) / adddiv);
          vg4->image->draw_line(imgp, x1, y1, x1, y2, rastercolor);
          vg4->image->draw_line(imgp, x1 + 1, y1, x1 + 1, y2, rastercolor);
          begx += addx;
        }
      }
    }

    /* draw horizontal line */
    if (tlins->fntlist[fnth + 1] != NULL) {
      vg4->image->draw_line(imgp, 0, y2 - (lpitch_add / 2), tlins->imgw - 1, y2 - (lpitch_add / 2), rastercolor);
      vg4->image->draw_line(imgp, 0, y2 - (lpitch_add / 2) + 1, tlins->imgw - 1, y2 - (lpitch_add / 2) + 1, rastercolor);
    }

    /* position of next text-line */
    imgh += lineh;
    imgh += tlins->fntlist[fnth][fntw].gapbottom;
    imgh += lpitch_add;
  }
} /* Ende add_raster */


/* draw rectangle around image */
static void
draw_imgrect(struct VG_Image *imgp, int rectcolor)
{
  struct VG_Rect rect;

  if (imgp == NULL) { return; }
  if (rectcolor < 0 || rectcolor == VG_COLOR_TRANSPARENT) { return; }

  rect.x = rect.y = 0;
  vg4->image->getsize(imgp, NULL, &rect.w, &rect.h);
  vg4->image->draw_rect(imgp, &rect, rectcolor, VG_FALSE);
} /* Ende draw_imgrect */


/* insert an alias (<ccmd><ALIAS_SEP><alias>) into parameter-hash */
static void
insert_alias(const char *alias, struct VG_Hash **hparam)
{
  const char *alias_header;

  if (alias == NULL || *alias == '\0') { return; }
  if (hparam == NULL) { return; }

  alias_header = NULL;

  if (strcmp(alias, "left") == 0) {
    alias_header = "[orientation=left boxwidth=100%]";
  } else if (strcmp(alias, "center") == 0) {
    alias_header = "[orientation=center boxwidth=100%]";
  } else if (strcmp(alias, "right") == 0) {
    alias_header = "[orientation=right boxwidth=100%]";
  } else if (strcmp(alias, "block") == 0) {
    alias_header = "[orientation=block boxwidth=100%]";
  }

  if (alias_header != NULL) {
    struct VG_Hash *hsh;
    hsh = vg4_intern_parser_keyval(alias_header, strlen(alias_header) + 1, NULL);
    if (hsh != NULL) {
      if (*hparam != NULL) {
        vg4->hash->insert(*hparam, hsh, VG_TRUE);
        vg4->hash->destroy(hsh);
      } else {
        *hparam = hsh;
      }
    }
  }
} /* Ende insert_alias */


/* set into rfile [dirname/]file according to file being absolute */
static void
set_file_with_path(char *rfile, size_t rsize, const char *dirname, const char *file)
{
  if (rfile == NULL || rsize == 0) { return; }
  *rfile = '\0';
  if (file == NULL || *file == '\0') { return; }
  if (dirname == NULL || *dirname == '\0') { dirname = "."; }

  if (*file == '/') {
    vg4->misc->strcpy(rfile, rsize, file + 1);
  } else {
    snprintf(rfile, rsize, "%s/%s", dirname, file);
  }
} /* Ende set_file_with_path */


/* find start and end of control-command line and return command */
static VG_BOOL
ctrlcmd_parse(const char **txptr, size_t *txsize, const char **text_start, const char **text_end, char *ccmd, size_t csize, struct VG_Hash **hparam)
{
  const char *kp1, *kp2;
  size_t ksize;
  int ebene;
  char ccb[64], *ccp;
  VG_BOOL retw;

  if (txptr == NULL || *txptr == NULL || txsize == NULL || *txsize == 0) { return VG_FALSE; }
  if (text_start == NULL || text_end == NULL) { return VG_FALSE; }
  if (ccmd == NULL || csize == 0) { return VG_FALSE; }
  if (hparam == NULL) { return VG_FALSE; }

  *hparam = NULL;

  kp1 = *txptr;
  ksize = *txsize;
  ebene = 1;

  /* search for end of control-command line: "%}" */
  for (;; kp1 = kp2 + 1, ksize--) {
    kp2 = memchr(kp1, '%', ksize);
    if (kp2 == NULL) { return VG_FALSE; }
    ksize -= (size_t)(kp2 - kp1);
    if (ksize <= 1) { return VG_FALSE; }

    if (kp2[1] == '{') {  /* new nested control-command line */
      ebene++;
    } else if (kp2[1] == '}') {  /* end of (nested) control-command line */
      if (--ebene == 0) {
        kp1 = kp2 + 1; ksize--;
        break;
      }
#if 0
    } else if (kp2[1] == '%') {  /* treat %% as character "%" */
      kp2++; ksize--;
      if (ksize == 1) { return VG_FALSE; }
#endif
    }
  }
  *text_end = kp1;  /* "}" */

  /* search for command */

  retw = VG_FALSE;
  kp1 = *txptr;
  kp2 = *text_end;
  vg4_intern_skip_ws(&kp1, kp2);  /* skip to command */
  kp2 = kp1 + strspn(kp1, "abcdefghijklmnopqrstuvwxyz" ALIAS_SEP);
  if (kp2 == kp1) { return VG_FALSE; }  /* no command */

  /* set possible command */
  snprintf(ccb, sizeof(ccb), "%.*s", (int)(size_t)(kp2 - kp1), kp1);

  kp1 = kp2;  /* after command */
  vg4_intern_skip_ws(&kp1, *text_end);  /* skip to header or ':' */

  if (*kp1 == '[') {  /* command header? */
    /* read header into hparam */
    struct VG_Hash *hsh;
    char *ntext;
    hsh = vg4_intern_parser_keyval(kp1, (size_t)(*text_end - kp1), &ntext);
    if (hsh == NULL) { return VG_FALSE; }
    kp1 = ntext;  /* after header */
    vg4_intern_skip_ws(&kp1, *text_end);  /* skip to ':' */
    if (kp1 == *text_end || *kp1 != ':') {  /* command invalid */
      vg4->hash->destroy(hsh);
      return VG_FALSE;
    }
    *hparam = hsh;
  }

  if (*kp1 == ':') {  /* command valid */
    /* set command and alias */
    if ((ccp = strchr(ccb, ALIAS_SEP[0])) != NULL) {
      *ccp++ = '\0';
      insert_alias(ccp, hparam);
    }
    vg4->misc->strcpy(ccmd, csize, ccb);
    /* search for start of data */
    *text_start = kp1 + 1;
    if (strcmp(ccmd, "txt") == 0 || strcmp(ccmd, "cell") == 0) {  /* skip spaces to after newline */
      vg4_intern_skip_sp(text_start, *text_end);
    } else {  /* skip whitespaces */
      vg4_intern_skip_ws(text_start, *text_end);
    }
    /* update next position in text */
    *txptr = *text_end + 1;
    *txsize = ksize - 1;
    /* search for end of data */
    (*text_end)--;
    while (*text_end > *text_start) {
      (*text_end)--;
      if (**text_end == '\0' || strchr(VGI_WSPACE, (int)**text_end) == NULL) { (*text_end)++; break; }
    }
    retw = VG_TRUE;
  }

  return retw;
} /* Ende ctrlcmd_parse */


/* execute control-command, return result as image */
static struct fntimg
ctrlcmd_exec(VG_BOOL *iserr, const char *ccmd, const char *text_start, const char *text_end, struct fntparm *params, int calldepth, VG_BOOL *usegap, struct VG_Hash *hparam, const char *dirname, struct VG_Hash *hvar)
{
  struct fntimg fimg;

  if (iserr != NULL) { *iserr = VG_FALSE; }
  memset(&fimg, 0, sizeof(fimg));
  if (usegap != NULL) { *usegap = VG_FALSE; }
  if (ccmd == NULL || text_start == NULL || text_end == NULL) { return fimg; }
  if (text_start > text_end) { return fimg; }

  if (calldepth > MAX_RECURS) {
    pwarn("Too many nested control-commands: %d", calldepth);
    goto errexec;
  }

  /* update parameters with header-parameters */
  if (hparam != NULL) { get_params(params, hparam, dirname, hvar); }

  /* process command */

  if (strcmp(ccmd, "txtfile") == 0) {
    /* load text from file */
    fimg = ccmdexec_txtfile(text_start, text_end, params, calldepth, dirname, hvar);

  } else if (strcmp(ccmd, "txt") == 0) {
    /* create image from embedded text */
    fimg = ccmdexec_txt(text_start, text_end, params, calldepth, dirname, hvar);

  } else if (strcmp(ccmd, "imgfile") == 0) {
    /* load image from file */
    fimg = ccmdexec_imgfile(text_start, text_end, params, calldepth, dirname, hvar);
    if (fimg.imgp != NULL && usegap != NULL) { *usegap = VG_TRUE; }

  } else if (strcmp(ccmd, "img") == 0) {
    /* create image from embedded base64 encoded image */
    fimg = ccmdexec_img(text_start, text_end, params, calldepth, dirname, hvar);
    if (fimg.imgp != NULL && usegap != NULL) { *usegap = VG_TRUE; }

  } else if (strcmp(ccmd, "mlang") == 0) {
    /* load text from multilanguage scope:key */
    fimg = ccmdexec_mlang(text_start, text_end, params, calldepth, dirname, hvar);

  } else if (strcmp(ccmd, "var") == 0) {
    /* variable */
    fimg = ccmdexec_var(text_start, text_end, params, calldepth, dirname, hvar);

  } else if (strcmp(ccmd, "box") == 0) {
    /* create image from box-contents */
    fimg = ccmdexec_box(text_start, text_end, params, calldepth, dirname, hvar);

  } else if (strcmp(ccmd, "table") == 0) {
    /* create image from table-contents */
    fimg = ccmdexec_table(text_start, text_end, params, calldepth, dirname, hvar);

  } else {
    pwarn("Unknown control-command: \"%s\"", ccmd);
    goto errexec;
  }

  if (fimg.imgp == NULL) { goto errexec; }

  /* draw rectangle around image */
  draw_imgrect(fimg.imgp, params->rectcolor);

  return fimg;

errexec:
  { size_t dlen = (size_t)(text_end - text_start);
    if (dlen > 32) { dlen = 32; } else if (dlen == 0) { text_start = " "; dlen = 1; }
    pwarn(": error executing control-command: ");
    pwarn("  %s(%.*s%s)\n", ccmd, (int)dlen, text_start, (dlen == 32 ? "..." : ""));
  }
  if (iserr != NULL) { *iserr = VG_TRUE; }
  return fimg;
} /* Ende ctrlcmd_exec */


/* execute control-command "txtfile", return result as image */
static struct fntimg
ccmdexec_txtfile(const char *text_start, const char *text_end, const struct fntparm *params, int calldepth, const char *dirname, struct VG_Hash *hvar)
{
  struct fntimg fimg;
  char filename[256];
  char nfilename[VGI_PATHSIZE], ndirname[VGI_PATHSIZE];
  FILE *ffp;
  struct stat sbuf;
  char *finh;
  size_t fsize;

  memset(&fimg, 0, sizeof(fimg));
  if (params == NULL) { pwarn("no parameters passed"); return fimg; }
  if (text_start == NULL || text_end == NULL) { pwarn("no text-file passed"); return fimg; }
  if (text_start >= text_end) { pwarn("no text-file passed"); return fimg; }

  snprintf(filename, sizeof(filename), "%.*s", (int)(size_t)(text_end - text_start), text_start);

  { struct SML3_gummi gm = SML3_GUM_INITIALIZER;
    const char *cloc;
    FILE *fft = NULL;
    cloc = vg4->mlang->getlocale(VG_FALSE);
    vg4_intern_str_with_var(&gm, filename, hvar, cloc);
    set_file_with_path(nfilename, sizeof(nfilename), dirname, SML3_gumgetval(&gm));
    if (*SML3_gumgetval(&gm) == '\0' || (fft = fopen(nfilename, "r")) == NULL) {
      cloc = vg4->mlang->getlocale(VG_TRUE);
      vg4_intern_str_with_var(&gm, filename, hvar, cloc);
    } else if (fft != NULL) {
      fclose(fft);
    }
    vg4->misc->strcpy(filename, sizeof(filename), SML3_gumgetval(&gm));
    SML3_gumdest(&gm);
    if (*filename == '\0') { pwarn("loading txtfile: no name"); return fimg; }
  }

  set_file_with_path(nfilename, sizeof(nfilename), dirname, filename);

  { const char *kptr;
    kptr = strrchr(filename, '/');
    if (kptr != NULL) {
      if (*filename == '/') {
        snprintf(ndirname, sizeof(ndirname), "%.*s", (int)(kptr - filename), filename);
      } else {
        snprintf(ndirname, sizeof(ndirname), "%s/%.*s", dirname, (int)(kptr - filename), filename);
      }
    } else {
      snprintf(ndirname, sizeof(ndirname), "%s", dirname);
    }
  }

  /* open and read text-file */

  ffp = fopen(nfilename, "r");
  if (ffp == NULL) { pwarn("loading \"%s\": %s", nfilename, strerror(errno)); return fimg; }

  if (fstat(fileno(ffp), &sbuf) < 0) {
    pwarn("loading \"%s\": fstat: %s", nfilename, strerror(errno));
    fclose(ffp);
    return fimg;
  }
  fsize = (size_t)sbuf.st_size;

  finh = SML3_malloc(fsize + 1);
  if (fread(finh, 1, fsize, ffp) != fsize) {
    pwarn("loading \"%s\": file too short", nfilename);
    fclose(ffp);
    return fimg;
  }
  finh[fsize] = '\0';
  fclose(ffp);

  fimg = ccmdexec_txt(finh, finh + fsize, params, calldepth, ndirname, hvar);
  free(finh);

  return fimg;
} /* Ende ccmdexec_txtfile */


/* execute control-command "txt", return result as image */
static struct fntimg
ccmdexec_txt(const char *text_start, const char *text_end, const struct fntparm *params, int calldepth, const char *dirname, struct VG_Hash *hvar)
{
  const char *empty = "";
  struct fntimg fimg, ccfimg;
  const char *txptr;
  size_t txsize;
  struct textlines *tlins;
  int lpitch_add;
  int cindex, anzz;
  VG_BOOL usegap;
  struct vgi_font *vgft;
  char key[16], itag[TAG_SIZE];
  struct VG_Hash *hutag;

  memset(&fimg, 0, sizeof(fimg));
  if (params == NULL) { pwarn("no parameters passed"); return fimg; }
  if (text_start == NULL || text_end == NULL || text_start >= text_end) { text_start = empty; text_end = text_start + strlen(text_start); }

  /* text */
  txptr = text_start;
  txsize = (size_t)(text_end - text_start);

  /* textlines */
  tlins = create_textlines();

  /* pixels to add to default line pitch */
  lpitch_add = params->line_pitch * (params->fntp->gaptop + params->fntp->gapbottom) / 100;

  /* loop for each UTF8-character in the text */
  for (;;) {
    anzz = vg4->misc->utf8_next(txptr, txsize, &cindex);
    if (anzz == 0) {
      if (tlins->fntlist[tlins->fnth] != NULL) { line_close(params->fntp, tlins, 0); }
      break;
    }
    if (anzz < 0) {
      pwarn("invalid UTF8-character at position %d: 0x%x", (int)(size_t)(txptr - text_start), (int)(unsigned char)*txptr);
      line_close(params->fntp, tlins, 0);
      free_textlines(tlins);
      return fimg;
    }

    txptr += anzz;
    txsize -= anzz;

    if (cindex == '\n') {  /* newline-character */
      line_close(params->fntp, tlins, lpitch_add);
      continue;
    }

    /* control-command?  "%{" <cmd> ["[" ... "]"] ":" ... "%}" */
    usegap = VG_FALSE;
    memset(&ccfimg, 0, sizeof(ccfimg));
    *itag = '\0';
    hutag = NULL;
    if (cindex == '%') {
      int i1, i2;

      i1 = vg4->misc->utf8_next(txptr, txsize, &i2);
      if (i1 > 0 && i2 == '{') {
        const char *ntext_start, *ntext_end;
        char ccmd[32];
        struct VG_Hash *hparam = NULL;
        txptr += i1; txsize -= i1;

        if (ctrlcmd_parse(&txptr, &txsize, &ntext_start, &ntext_end, ccmd, sizeof(ccmd), &hparam)) {
          if (strcmp(ccmd, "nl") == 0) {
            /* newline */
            vg4->hash->destroy(hparam);
            line_close(params->fntp, tlins, lpitch_add);
            continue;

          } else if (strcmp(ccmd, "sp") == 0) {
            /* space */
            vg4->hash->destroy(hparam);
            cindex = ' ';
            goto tt_char;

          } else {
            /* execute command returning an image */
            VG_BOOL iserr;
            struct fntparm nparams;
            copy_params(&nparams, params);
            ccfimg = ctrlcmd_exec(&iserr, ccmd, ntext_start, ntext_end, &nparams, calldepth + 1, &usegap, hparam, dirname, hvar);
            vg4->hash->destroy(hparam);
            if (iserr) { free_params(&nparams); line_close(params->fntp, tlins, 0); free_textlines(tlins); return fimg; }
            if (ccfimg.imgp != NULL) {  /* insert returned image */
              vg4->misc->strcpy(itag, sizeof(itag), nparams.itag);
              hutag = nparams.hutag; nparams.hutag = NULL;
              cindex = 0;
              vgft = NULL;
              free_params(&nparams);
              goto tt_goon;
            }
            free_params(&nparams);
            continue;
          }
        }
        txptr -= i1; txsize += i1;
      }
    }

    if (cindex < VGI_FONT_SEPSPACE) { continue; }  /* character lower to space */

tt_char:
    snprintf(key, sizeof(key), "%d", cindex);
    vgft = (struct vgi_font *)vg4->hash->get(params->fntp->hfont, key, NULL);

    if (vgft == NULL) {  /* character not in font */
      cindex = 0;
      snprintf(key, sizeof(key), "unknown");
      vgft = (struct vgi_font *)vg4->hash->get(params->fntp->hfont, key, NULL);
      if (vgft == NULL) {  /* unknown-character also not in font */
        snprintf(key, sizeof(key), "%d", VGI_FONT_SEPSPACE);
        vgft = (struct vgi_font *)vg4->hash->get(params->fntp->hfont, key, NULL);
        if (vgft == NULL) {  /* should not occur */
          pwarn("unknown unreplacable UTF8-character at position %d: 0x%x",
                (int)(size_t)(txptr - anzz - text_start),
                (int)(unsigned char)*(txptr - anzz));
          line_close(params->fntp, tlins, 0);
          free_textlines(tlins);
          return fimg;
        }
      }
    }

tt_goon:
    addto_textlines(tlins);

    if (ccfimg.imgp != NULL) {  /* image */
      int ccimgw, ccimgh;
      vg4->image->getsize(ccfimg.imgp, NULL, &ccimgw, &ccimgh);
      tlins->linew += ccimgw;
      if (ccimgh > tlins->lineh) {
        tlins->lineh = ccimgh;
        if (usegap) {
          tlins->fntlist[tlins->fnth][tlins->fntw].gaptop = params->fntp->gaptop;
          tlins->fntlist[tlins->fnth][tlins->fntw].gapbottom = params->fntp->gapbottom;
        } else {
          tlins->fntlist[tlins->fnth][tlins->fntw].gaptop = 0;
          tlins->fntlist[tlins->fnth][tlins->fntw].gapbottom = 0;
        }
      }
      /* set image */
      tlins->fntlist[tlins->fnth][tlins->fntw].flag = 2;
      tlins->fntlist[tlins->fnth][tlins->fntw].u.fimg = ccfimg;
    } else {  /* font-character */
      tlins->linew += vgft->w;
      if (vgft->h > tlins->lineh) {
        tlins->lineh = vgft->h;
        tlins->fntlist[tlins->fnth][tlins->fntw].gaptop = 0;
        tlins->fntlist[tlins->fnth][tlins->fntw].gapbottom = 0;
      }
      /* set font character */
      tlins->fntlist[tlins->fnth][tlins->fntw].flag = 1;
      tlins->fntlist[tlins->fnth][tlins->fntw].u.vgft = vgft;
    }

    /* set remaining */
    tlins->fntlist[tlins->fnth][tlins->fntw].txptr = txptr;
    tlins->fntlist[tlins->fnth][tlins->fntw].txsize = txsize;
    tlins->fntlist[tlins->fnth][tlins->fntw].linew = tlins->linew;
    tlins->fntlist[tlins->fnth][tlins->fntw].lineh = tlins->lineh;
    vg4->misc->strcpy(tlins->fntlist[tlins->fnth][tlins->fntw].itag,
                      sizeof(tlins->fntlist[tlins->fnth][tlins->fntw].itag),
                      itag);
    tlins->fntlist[tlins->fnth][tlins->fntw].hutag = hutag;

    /* line overflow? */
    if (!params->nowrap && params->maxwidth > 0 && tlins->linew > params->maxwidth) {
      int ifntw, afntw = tlins->fntw;

      if (tlins->lastsep_fntw >= 0) {  /* last separator character */
        tlins->fntw = tlins->lastsep_fntw;
      } else if (tlins->fntw > 0) {  /* previous character */
        tlins->fntw--;
      }

      for (ifntw = tlins->fntw + 1; ifntw <= afntw; ifntw++) {
        if (tlins->fntlist[tlins->fnth][ifntw].flag == 2) {  /* image */
          vg4->image->destroy(tlins->fntlist[tlins->fnth][ifntw].u.fimg.imgp);
          SML3_hashfree(&tlins->fntlist[tlins->fnth][ifntw].u.fimg.hsub);
        }
        if (tlins->fntlist[tlins->fnth][ifntw].hutag != NULL) {
          vg4->hash->destroy(tlins->fntlist[tlins->fnth][ifntw].hutag);
        }
      }

      txptr = tlins->fntlist[tlins->fnth][tlins->fntw].txptr;
      txsize = tlins->fntlist[tlins->fnth][tlins->fntw].txsize;
      line_close(params->fntp, tlins, lpitch_add);
      continue;
    }

    /* check for separator character */
    if (cindex == VGI_FONT_SEPSPACE) {
      tlins->lastsep_fntw = tlins->fntw;
    } else if (cindex > 0) {
      int i1;
      for (i1 = 0; i1 < VGI_FONT_SEPC_MAX; i1++) {
        if (params->fntp->sepc[i1] == 0) { break; }
        if (cindex == params->fntp->sepc[i1]) {
          tlins->lastsep_fntw = tlins->fntw;
          break;
        }
      }
    }
  }

  /* create image and add border */
  fimg = fill_text(tlins, params, lpitch_add);
  fimg.imgp = add_border(fimg.imgp, params->border, params->bgcolor);

  free_textlines(tlins);

  return fimg;
} /* Ende ccmdexec_txt */


/* execute control-command "imgfile", return result as image */
static struct fntimg
ccmdexec_imgfile(const char *text_start, const char *text_end, const struct fntparm *params, int calldepth, const char *dirname, struct VG_Hash *hvar)
{
  struct fntimg fimg;
  char filename[VGI_PATHSIZE];
  char nfilename[VGI_PATHSIZE];

  memset(&fimg, 0, sizeof(fimg));
  if (params == NULL) { pwarn("no parameters passed"); return fimg; }
  if (text_start == NULL || text_end == NULL) { pwarn("no image-file passed"); return fimg; }
  if (text_start >= text_end) { pwarn("no image-file passed"); return fimg; }
  (void)calldepth;
  (void)hvar;

  snprintf(filename, sizeof(filename), "%.*s", (int)(size_t)(text_end - text_start), text_start);

  { struct SML3_gummi gm = SML3_GUM_INITIALIZER;
    const char *cloc;
    FILE *fft = NULL;
    cloc = vg4->mlang->getlocale(VG_FALSE);
    vg4_intern_str_with_var(&gm, filename, hvar, cloc);
    set_file_with_path(nfilename, sizeof(nfilename), dirname, SML3_gumgetval(&gm));
    if (*SML3_gumgetval(&gm) == '\0' || (fft = fopen(nfilename, "r")) == NULL) {
      cloc = vg4->mlang->getlocale(VG_TRUE);
      vg4_intern_str_with_var(&gm, filename, hvar, cloc);
    } else if (fft != NULL) {
      fclose(fft);
    }
    vg4->misc->strcpy(filename, sizeof(filename), SML3_gumgetval(&gm));
    SML3_gumdest(&gm);
    if (*filename == '\0') { pwarn("loading imgfile: no name"); return fimg; }
  }

  set_file_with_path(nfilename, sizeof(nfilename), dirname, filename);

  fimg.imgp = vg4_image_load_nolist(nfilename);
  if (fimg.imgp != NULL) {  /* set width and orientation and add border */
    fimg.imgp = add_ori(fimg.imgp, params->boxwidth, params->maxwidth, params->orientation1, params->orientation2, params->bgcolor);
    fimg.imgp = add_border(fimg.imgp, params->border, params->bgcolor);
  }

  return fimg;
} /* Ende ccmdexec_imgfile */


/* execute control-command "img", return result as image */
static struct fntimg
ccmdexec_img(const char *text_start, const char *text_end, const struct fntparm *params, int calldepth, const char *dirname, struct VG_Hash *hvar)
{
  struct fntimg fimg;
  char *imgdata;
  size_t imgsize;

  memset(&fimg, 0, sizeof(fimg));
  if (params == NULL) { pwarn("no parameters passed"); return fimg; }
  if (text_start == NULL || text_end == NULL) { pwarn("no image-contents passed"); return fimg; }
  if (text_start >= text_end) { pwarn("no image-contents passed"); return fimg; }
  (void)calldepth;
  (void)dirname;
  (void)hvar;

  SML3_base64_decode(&imgdata, &imgsize, text_start, (size_t)(text_end - text_start), NULL);
  fimg.imgp = vg4_image_loadmem(imgdata, imgsize);
  free(imgdata);

  if (fimg.imgp != NULL) {  /* set width and orientation and add border */
    fimg.imgp = add_ori(fimg.imgp, params->boxwidth, params->maxwidth, params->orientation1, params->orientation2, params->bgcolor);
    fimg.imgp = add_border(fimg.imgp, params->border, params->bgcolor);
  }

  return fimg;
} /* Ende ccmdexec_img */


/* execute control-command "mlang", return result as image */
static struct fntimg
ccmdexec_mlang(const char *text_start, const char *text_end, const struct fntparm *params, int calldepth, const char *dirname, struct VG_Hash *hvar)
{
  struct fntimg fimg;
  char *mltxt, *mlscope, *mlkey;
  size_t mlsize;

  memset(&fimg, 0, sizeof(fimg));
  if (params == NULL) { pwarn("no parameters passed"); return fimg; }
  if (text_start == NULL || text_end == NULL) { pwarn("no mlang-contents passed"); return fimg; }
  if (text_start >= text_end) { pwarn("no mlang-contents passed"); return fimg; }

  /* get multilanguage text from data (scope:key) */
  mlsize = (size_t)(text_end - text_start + 1);
  mltxt = SML3_memdup(text_start, mlsize);
  mltxt[mlsize - 1] = '\0';
  mlscope = mltxt;
  if ((mlkey = strchr(mltxt, ':')) != NULL) {
    *mlkey++ = '\0';
  } else {
    pwarn("no scope for mlang passed: \"%s\"", mltxt);
    return fimg;
  }

  { struct SML3_gummi gm0 = SML3_GUM_INITIALIZER;
    struct SML3_gummi gm1 = SML3_GUM_INITIALIZER;
    struct SML3_gummi gm2 = SML3_GUM_INITIALIZER;
    const char *cloc;
    SML3_gumprintf(&gm0, 0, "%s:%s", mlscope, mlkey);
    cloc = vg4->mlang->getlocale(VG_FALSE);
    vg4_intern_str_with_var(&gm1, mlscope, hvar, cloc);
    vg4_intern_str_with_var(&gm2, mlkey, hvar, cloc);
    text_start = vg4->mlang->get(SML3_gumgetval(&gm1), SML3_gumgetval(&gm2));
    if (*text_start == '\0' || strcmp(text_start, SML3_gumgetval(&gm0)) == 0) {  /* not found */
      cloc = vg4->mlang->getlocale(VG_TRUE);
      vg4_intern_str_with_var(&gm1, mlscope, hvar, cloc);
      vg4_intern_str_with_var(&gm2, mlkey, hvar, cloc);
      text_start = vg4->mlang->get(SML3_gumgetval(&gm1), SML3_gumgetval(&gm2));
    }
    text_end = text_start + strlen(text_start) + 1;
    SML3_gumdest(&gm0);
    SML3_gumdest(&gm1);
    SML3_gumdest(&gm2);
  }
  free(mltxt);

  return ccmdexec_txt(text_start, text_end, params, calldepth, dirname, hvar);
} /* Ende ccmdexec_mlang */


/* execute control-command "var", return result as image */
static struct fntimg
ccmdexec_var(const char *text_start, const char *text_end, const struct fntparm *params, int calldepth, const char *dirname, struct VG_Hash *hvar)
{
  struct fntimg fimg;
  char *mlvar;
  size_t mlsize;

  memset(&fimg, 0, sizeof(fimg));
  if (params == NULL) { pwarn("no parameters passed"); return fimg; }
  if (text_start == NULL || text_end == NULL) { pwarn("no var-contents passed"); return fimg; }
  if (text_start >= text_end) { pwarn("no var-contents passed"); return fimg; }

  /* replace variable with its value */
  mlsize = (size_t)(text_end - text_start + 1);
  mlvar = SML3_memdup(text_start, mlsize);
  mlvar[mlsize - 1] = '\0';
  text_start = (const char *)vg4->hash->get(hvar, mlvar, NULL);
  if (text_start == NULL) { pwarn("variable \"%s\" not found", mlvar); free(mlvar); return fimg; }
  text_end = text_start + strlen(text_start) + 1;
  free(mlvar);

  return ccmdexec_txt(text_start, text_end, params, calldepth, dirname, hvar);
} /* Ende ccmdexec_var */


/* execute control-command "box", return result as image */
static struct fntimg
ccmdexec_box(const char *text_start, const char *text_end, const struct fntparm *params, int calldepth, const char *dirname, struct VG_Hash *hvar)
{
  const char *empty = "";
  struct fntimg fimg, ccfimg;
  const char *txptr, *ntext_start, *ntext_end;
  size_t txsize;
  struct textlines *tlins;
  int lpitch_add, cindex, anzz, ccpos, ccimgw, ccimgh;
  VG_BOOL usegap;
  char ccmd[32], itag[TAG_SIZE];
  struct VG_Hash *hparam, *hutag;

  memset(&fimg, 0, sizeof(fimg));
  if (params == NULL) { pwarn("no parameters passed"); return fimg; }
  if (text_start == NULL || text_end == NULL || text_start >= text_end) { text_start = empty; text_end = text_start + strlen(text_start); }

  /* box-contents */
  txptr = text_start;
  txsize = (size_t)(text_end - text_start);

  /* textlines */
  tlins = create_textlines();

  /* pixels to add to default line pitch */
  lpitch_add = params->line_pitch * (params->fntp->gaptop + params->fntp->gapbottom) / 100;

  /* loop for each UTF8-character in the box-contents */
  ccpos = 0;
  for (;;) {
    anzz = vg4->misc->utf8_next(txptr, txsize, &cindex);
    if (anzz == 0) {
      if (tlins->fntlist[tlins->fnth] != NULL) { line_close(params->fntp, tlins, 0); }
      break;
    }
    if (anzz < 0) {
      pwarn("invalid UTF8-character at position %d: 0x%x", (int)(size_t)(txptr - text_start), (int)(unsigned char)*txptr);
      line_close(params->fntp, tlins, 0);
      free_textlines(tlins);
      return fimg;
    }

    txptr += anzz;
    txsize -= anzz;

    /* control-command?  "%{" <cmd> ["[" ... "]"] ":" ... "%}" */
    switch(ccpos) {
      case 0:
        if (cindex == '%') { ccpos++; }
        break;
      case 1:
        if (cindex == '{') { ccpos++; } else { ccpos = 0; }
        break;
    }
    if (ccpos < 2) { continue; }

    ccpos = 0;
    if (!ctrlcmd_parse(&txptr, &txsize, &ntext_start, &ntext_end, ccmd, sizeof(ccmd), &hparam)) { continue; }

    /* execute control-command */

    if (strcmp(ccmd, "nl") == 0) {
      /* newline */
      vg4->hash->destroy(hparam);
      line_close(params->fntp, tlins, lpitch_add);
      continue;

    } else if (strcmp(ccmd, "sp") == 0) {
      /* space */
      int w1, h1;
      vg4->hash->destroy(hparam);
      vg4->font->maxsize(params->fntp, &w1, &h1);
      ccfimg.imgp = vg4_image_create_nolist(w1, h1);
      ccfimg.hsub = NULL;
      vg4->image->fill(ccfimg.imgp, params->bgcolor);
      usegap = VG_FALSE;
      *itag = '\0';
      hutag = NULL;

    } else {
      /* execute command returning an image */
      VG_BOOL iserr;
      struct fntparm nparams;
      copy_params(&nparams, params);
      ccfimg = ctrlcmd_exec(&iserr, ccmd, ntext_start, ntext_end, &nparams, calldepth + 1, &usegap, hparam, dirname, hvar);
      vg4->hash->destroy(hparam);
      if (iserr) { free_params(&nparams); line_close(params->fntp, tlins, 0); free_textlines(tlins); return fimg; }
      if (ccfimg.imgp == NULL) { free_params(&nparams); continue; }
      vg4->misc->strcpy(itag, sizeof(itag), nparams.itag);
      hutag = nparams.hutag; nparams.hutag = NULL;
      free_params(&nparams);
    }

    /* insert image into line */

    addto_textlines(tlins);

    vg4->image->getsize(ccfimg.imgp, NULL, &ccimgw, &ccimgh);
    tlins->linew += ccimgw;
    if (ccimgh > tlins->lineh) {
      tlins->lineh = ccimgh;
      if (usegap) {
        tlins->fntlist[tlins->fnth][tlins->fntw].gaptop = params->fntp->gaptop;
        tlins->fntlist[tlins->fnth][tlins->fntw].gapbottom = params->fntp->gapbottom;
      } else {
        tlins->fntlist[tlins->fnth][tlins->fntw].gaptop = 0;
        tlins->fntlist[tlins->fnth][tlins->fntw].gapbottom = 0;
      }
    }
    /* set image */
    tlins->fntlist[tlins->fnth][tlins->fntw].flag = 2;
    tlins->fntlist[tlins->fnth][tlins->fntw].u.fimg = ccfimg;
    tlins->fntlist[tlins->fnth][tlins->fntw].txptr = txptr;
    tlins->fntlist[tlins->fnth][tlins->fntw].txsize = txsize;
    tlins->fntlist[tlins->fnth][tlins->fntw].linew = tlins->linew;
    tlins->fntlist[tlins->fnth][tlins->fntw].lineh = tlins->lineh;
    vg4->misc->strcpy(tlins->fntlist[tlins->fnth][tlins->fntw].itag,
               sizeof(tlins->fntlist[tlins->fnth][tlins->fntw].itag),
               itag);
    tlins->fntlist[tlins->fnth][tlins->fntw].hutag = hutag;

    /* line overflow? */
    if (params->maxwidth > 0 && tlins->linew > params->maxwidth) {
      int afntw = tlins->fntw;

      if (tlins->fntw > 0) {  /* previous image */
        tlins->fntw--;
        if (tlins->fntlist[tlins->fnth][afntw].flag == 2) {  /* image */
          vg4->image->destroy(tlins->fntlist[tlins->fnth][afntw].u.fimg.imgp);
          SML3_hashfree(&tlins->fntlist[tlins->fnth][afntw].u.fimg.hsub);
        }
        if (tlins->fntlist[tlins->fnth][afntw].hutag != NULL) {
          vg4->hash->destroy(tlins->fntlist[tlins->fnth][afntw].hutag);
        }
      }

      txptr = tlins->fntlist[tlins->fnth][tlins->fntw].txptr;
      txsize = tlins->fntlist[tlins->fnth][tlins->fntw].txsize;
      line_close(params->fntp, tlins, lpitch_add);
      continue;
    }
  }

  /* create image and add border */
  fimg = fill_text(tlins, params, lpitch_add);
  fimg.imgp = add_border(fimg.imgp, params->border, params->bgcolor);

  free_textlines(tlins);

  return fimg;
} /* Ende ccmdexec_box */


/* execute control-command "table", return result as image */
static struct fntimg
ccmdexec_table(const char *text_start, const char *text_end, const struct fntparm *params, int calldepth, const char *dirname, struct VG_Hash *hvar)
{
  struct fntimg fimg, ccfimg;
  const char *txptr, *ntext_start, *ntext_end;
  size_t txsize;
  struct textlines *tlins;
  int lpitch_add, cindex, anzz, ccpos, ccimgw, ccimgh;
  char ccmd[32], itag[TAG_SIZE];
  struct VG_Hash *hparam, *hutag;
  int winw, maxcells, momcell, p_orientation1, p_orientation2;
  struct fntparm cparams;
  struct {
    char size[16];
    int rectcolor;
    int bgcolor;
    int v_ori;
  } cells[MAX_CELLS];

  memset(&fimg, 0, sizeof(fimg));
  if (params == NULL) { pwarn("no parameters passed"); return fimg; }
  if (text_start == NULL || text_end == NULL) { pwarn("no table-contents passed"); return fimg; }
  if (text_start >= text_end) { pwarn("no table-contents passed"); return fimg; }

  cparams = *params;

  /* calculate cell-sizes */
  vg4->window->getsize(&winw, NULL);
  if (cparams.maxwidth == 0) { cparams.maxwidth = winw; }
  cparams.boxwidth = cparams.maxwidth;
  anzz = 0;
  for (maxcells = 0; maxcells < MAX_CELLS; maxcells++) {
    if (cparams.table.cells[maxcells] == 0) { break; }
    snprintf(cells[maxcells].size, sizeof(cells[0].size), "%d", cparams.maxwidth * cparams.table.cells[maxcells] / 100);
    anzz += cparams.table.cells[maxcells];
  }
  if (maxcells == 0) { pwarn("header \"cells\" missing"); return fimg; }
  if (anzz >= 100) {  /* 100 percent: no vertical spaces */
    anzz = 0;
    for (momcell = 0; momcell < maxcells; momcell++) {
      anzz += atoi(cells[momcell].size);
    }
    if (anzz < cparams.maxwidth) {  /* add missing pixels to last cell */
      anzz = atoi(cells[maxcells - 1].size) + cparams.maxwidth - anzz;
      snprintf(cells[maxcells - 1].size, sizeof(cells[0].size), "%d", anzz);
    }
  }
  momcell = 0;

  /* table-contents */
  txptr = text_start;
  txsize = (size_t)(text_end - text_start);

  /* textlines */
  tlins = create_textlines();

  /* pixels to add to default line pitch */
  lpitch_add = cparams.line_pitch * (cparams.fntp->gaptop + cparams.fntp->gapbottom) / 100;

  /* loop for each UTF8-character in the table-contents */
  ccpos = 0;
  for (;;) {
    anzz = vg4->misc->utf8_next(txptr, txsize, &cindex);
    if (anzz == 0) {
      if (tlins->fntlist[tlins->fnth] != NULL) { line_close(cparams.fntp, tlins, 0); }
      break;
    }
    if (anzz < 0) {
      pwarn("invalid UTF8-character at position %d: 0x%x", (int)(size_t)(txptr - text_start), (int)(unsigned char)*txptr);
      line_close(cparams.fntp, tlins, 0);
      free_textlines(tlins);
      return fimg;
    }

    txptr += anzz;
    txsize -= anzz;

    /* control-command?  "%{" <cmd> ["[" ... "]"] ":" ... "%}" */
    switch(ccpos) {
      case 0:
        if (cindex == '%') { ccpos++; }
        break;
      case 1:
        if (cindex == '{') { ccpos++; } else { ccpos = 0; }
        break;
    }
    if (ccpos < 2) { continue; }

    ccpos = 0;
    if (!ctrlcmd_parse(&txptr, &txsize, &ntext_start, &ntext_end, ccmd, sizeof(ccmd), &hparam)) { continue; }

    /* execute control-command */

    memset(&ccfimg, 0, sizeof(ccfimg));
    if (strcmp(ccmd, "cell") == 0) {
      struct fntparm nparams;
      if (momcell == maxcells) {  /* close line */
        line_close(cparams.fntp, tlins, lpitch_add);
        momcell = 0;
      }
      copy_params(&nparams, &cparams);
      if (hparam == NULL) { hparam = vg4_hash_create_nolist(); }
      vg4->hash->setstr(hparam, "boxwidth", cells[momcell].size);
      vg4->hash->setstr(hparam, "maxwidth", cells[momcell].size);
      if (cparams.table.doraster > 0) {  /* add to border because of raster-lines */
        char borderbuf[32];
        char *borderval = vg4->hash->get(hparam, "border", NULL);
        if (borderval == NULL) { borderval = "0"; }
        snprintf(borderbuf, sizeof(borderbuf), "%d", atoi(borderval) + 2);
        vg4->hash->setstr(hparam, "border", borderbuf);
      }
      get_params(&nparams, hparam, dirname, hvar);
      ccfimg = ccmdexec_txt(ntext_start, ntext_end, &nparams, calldepth, dirname, hvar);
      if (ccfimg.imgp == NULL) {
        size_t dlen = (size_t)(text_end - text_start);
        if (dlen > 32) { dlen = 32; } else if (dlen == 0) { text_start = " "; dlen = 1; }
        pwarn(": error executing control-command: ");
        pwarn("  %s(%.*s%s)\n", ccmd, (int)dlen, text_start, (dlen == 32 ? "..." : ""));
      }
      vg4->misc->strcpy(itag, sizeof(itag), nparams.itag);
      hutag = nparams.hutag; nparams.hutag = NULL;
      cells[momcell].rectcolor = nparams.rectcolor;
      cells[momcell].bgcolor = nparams.bgcolor;
      free_params(&nparams);
      /* redo to get v-orientation */
      vg4->hash->setstr(hparam, "boxheight", "10");  /* dummy to get v-orientation */
      get_params(&nparams, hparam, dirname, hvar);
      cells[momcell].v_ori = nparams.v_orientation1;
      free_params(&nparams);
    }
    if (hparam != NULL) { vg4->hash->destroy(hparam); }
    if (ccfimg.imgp == NULL) { continue; }

    /* insert image into line */

    addto_textlines(tlins);

    vg4->image->getsize(ccfimg.imgp, NULL, &ccimgw, &ccimgh);
    tlins->linew += ccimgw;
    if (ccimgh > tlins->lineh) {
      tlins->lineh = ccimgh;
      tlins->fntlist[tlins->fnth][tlins->fntw].gaptop = 0;
      tlins->fntlist[tlins->fnth][tlins->fntw].gapbottom = 0;
    }
    /* set image */
    tlins->fntlist[tlins->fnth][tlins->fntw].flag = 2;
    tlins->fntlist[tlins->fnth][tlins->fntw].u.fimg = ccfimg;
    tlins->fntlist[tlins->fnth][tlins->fntw].txptr = txptr;
    tlins->fntlist[tlins->fnth][tlins->fntw].txsize = txsize;
    tlins->fntlist[tlins->fnth][tlins->fntw].linew = tlins->linew;
    tlins->fntlist[tlins->fnth][tlins->fntw].lineh = tlins->lineh;
    vg4->misc->strcpy(tlins->fntlist[tlins->fnth][tlins->fntw].itag,
               sizeof(tlins->fntlist[tlins->fnth][tlins->fntw].itag),
               itag);
    tlins->fntlist[tlins->fnth][tlins->fntw].hutag = hutag;

    /* line full? */
    if (++momcell == maxcells) {
      int fntw, imgw, imgh;
      /* correct image height */
      for (fntw = 0; fntw <= tlins->fntw; fntw++) {
        vg4->image->getsize(tlins->fntlist[tlins->fnth][fntw].u.fimg.imgp, NULL, &imgw, &imgh);
        if (imgh < tlins->lineh) {
          struct VG_Position posi;
          struct VG_Image *imgpn = vg4_image_create_nolist(imgw, tlins->lineh);
          posi.pos = VG_POS_UPPER_LEFT;
          posi.x = 0;
          if (cells[fntw].v_ori == VG_ORI_LEFT) {  /* top */
            posi.y = 0;
          } else if (cells[fntw].v_ori == VG_ORI_CENTER) {  /* center */
            posi.y = (tlins->lineh - imgh) / 2;
          } else if (cells[fntw].v_ori == VG_ORI_RIGHT) {  /* bottom */
            posi.y = tlins->lineh - imgh;
          }
          vg4->image->fill(imgpn, cells[fntw].bgcolor);
          vg4->image->copy(imgpn, tlins->fntlist[tlins->fnth][fntw].u.fimg.imgp, &posi, NULL);
          vg4->image->destroy(tlins->fntlist[tlins->fnth][fntw].u.fimg.imgp);
          tlins->fntlist[tlins->fnth][fntw].u.fimg.imgp = imgpn;
          if (tlins->fntlist[tlins->fnth][fntw].u.fimg.hsub != NULL) {
            /* correct y-positions of all first-level sub-hashes */
            struct SML3_hash *hs1, *hs2, **hsval;
            struct SML3_hashelem *he1, *he2;
            size_t hsvalsize;
            struct subpos sps;
            hs1 = tlins->fntlist[tlins->fnth][fntw].u.fimg.hsub;
            hs2 = SML3_hashnew(free_hsub, 0);
            for (he1 = SML3_hashlist(hs1, NULL, 0); he1 != NULL; he1 = SML3_hashlist(hs1, he1, 0)) {
              sps = *(struct subpos *)SML3_hashelem_keyget(he1, NULL);
              hsval = (struct SML3_hash **)SML3_hashelem_valget(he1, &hsvalsize);
              sps.rect.y += posi.y;
              he2 = SML3_hashset(hs2, &sps, sizeof(sps));
              SML3_hashelem_valset(he2, hsval, hsvalsize);
            }
            SML3_hash_reload_destfk(tlins->fntlist[tlins->fnth][fntw].u.fimg.hsub, NULL);
            SML3_hashfree(&tlins->fntlist[tlins->fnth][fntw].u.fimg.hsub);
            tlins->fntlist[tlins->fnth][fntw].u.fimg.hsub = hs2;
          }
        }
        /* draw rectangle around image */
        draw_imgrect(tlins->fntlist[tlins->fnth][fntw].u.fimg.imgp, cells[fntw].rectcolor);
      }
    }
  }

  /* create image */
  p_orientation1 = cparams.orientation1;
  p_orientation2 = cparams.orientation2;
  cparams.orientation1 = cparams.orientation2 = VG_ORI_BLOCK;
  fimg = fill_text(tlins, &cparams, lpitch_add);
  cparams.orientation1 = p_orientation1;
  cparams.orientation2 = p_orientation2;
  /* add raster */
  if (cparams.table.doraster > 0) {
    add_raster(fimg.imgp, tlins, cparams.table.rastercolor, lpitch_add, (cparams.table.doraster == 1 ? VG_FALSE : VG_TRUE));
  }
  /* add border */
  fimg.imgp = add_border(fimg.imgp, cparams.border, cparams.bgcolor);

  free_textlines(tlins);

  return fimg;
} /* Ende ccmdexec_table */
