Mercurial > MadButterfly
view src/shape_path.c @ 446:2437047b8bb8
Fix bug of propertional shifting of sh_stext_t.
Original
- sh_stext_t does not apply shifting of text himself to scaled font face, but
- apply aggreagated matrix to font face.
- shifting in parent parent coord_t would applied to scaled font.
- amount of shifting is also multiplied by aggreagated matrix.
- It means shiftings of parent coord_ts are applied two times.
Now,
- remove x, y shifting from aggreagated matrices before applying
to scaled fonts.
author | Thinker K.F. Li <thinker@branda.to> |
---|---|
date | Sun, 02 Aug 2009 10:59:59 +0800 |
parents | cd6af7da32c9 |
children | 16116d84bc5e |
line wrap: on
line source
#include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <string.h> #include <cairo.h> #include "mb_types.h" #include "mb_redraw_man.h" /*! \brief Implement respective objects for SVG path tag. * * In user_data or dev_data, 0x00 bytes are padding after commands. * No commands other than 0x00 can resident after 0x00 itself. * It means command processing code can skip commands after a 0x00. * * Shapes should check if shape_t::geo is assigned. Once transformation * matrics are changed, shape objects should update shape_t::geo if * it is assigned. */ typedef struct _sh_path { shape_t shape; int cmd_len; int arg_len; int fix_arg_len; char *user_data; char *dev_data; /* device space data */ } sh_path_t; #define RESERVED_AIXS sizeof(co_aix[2]) #define ASSERT(x) #define SKIP_SPACE(x) while(*(x) && (isspace(*(x)) || *(x) == ',')) { (x)++; } #define SKIP_NUM(x) \ while(*(x) && \ (isdigit(*(x)) || \ *(x) == 'e' || \ *(x) == 'E' || \ *(x) == '-' || \ *(x) == '+' || \ *(x) == '.')) { \ (x)++; \ } #define OK 0 #define ERR -1 #define PI 3.1415926 #ifdef UNITTEST #undef rdman_shape_man #define rdman_shape_man(x, y) #endif /* ============================================================ * Implement arc in path. */ #include <math.h> /*! \brief Calculate center of the ellipse of an arc. * * Origin of our coordination is left-top corner, and y-axis are grown * to down-side. * * Space of the arc is transformed to space that correspondent * ellipse containing the arc is mapped into an unit circle. * - ux0 = x0 / rx * - uy0 = y0 / ry * - ux = x / rx * - uy = y / ry * ux0, uy0, ux, uy are got by transforming (x0, y0) and (x, y) into points * on the unit circle. The center of unit circle are (ucx, ucy): * - umx = (ux0 + ux) / 2 * - umy = (uy0 + uy) / 2 * - udcx = ucx - umx * - udcy = ucy - umy * - udx = ux - umx * - udy = uy - umy * * - udx * udcx + udy * udcy = 0 * * - udl2 = udx ** 2 + udy ** 2 * * For drawing small arc in clockwise * - udx * udcy - udy * udcx = sqrt((1 - udl2) * udl2) * * - udcy = -udcx * udx / udy * - -udcx * (udx ** 2) / udy - udy * udcx = sqrt((1 - udl2) * udl2) * - -udcx * ((udx ** 2) / udy + udy) = sqrt((1 - udl2) * udl2) * - udcx = -sqrt((1 - udl2) * udl2) / ((udx ** 2) / udy + udy) * or * - udcx = -udcy * udy / udx * - udx * udcy + udcy * (udy ** 2) / udx = sqrt((1 - udl2) * udl2) * - udcy * (udx + (udy ** 2) / udx) = sqrt((1 - udl2) * udl2) * - udcy = sqrt((1 - udl2) * udl2) / (udx + (udy ** 2) / udx) * * - cx = rx * ucx * - cx = rx * (udcx + umx) * - cy = ry * ucy * - cy = ry * (udcy + umy) */ static int calc_center_and_x_aix(co_aix x0, co_aix y0, co_aix x, co_aix y, co_aix rx, co_aix ry, co_aix x_rotate, int large, int sweep, co_aix *cx, co_aix *cy, co_aix *xx, co_aix *xy) { co_aix nrx, nry, nrx0, nry0; co_aix udx, udy, udx2, udy2; co_aix umx, umy; co_aix udcx, udcy; co_aix nrcx, nrcy; co_aix udl2; float _sin = sinf(x_rotate); float _cos = cosf(x_rotate); int reflect; nrx = x * _cos + y * _sin; nry = x * -_sin + y * _cos; nrx0 = x0 * _cos + y0 * _sin; nry0 = x0 * -_sin + y0 * _cos; udx = (nrx - nrx0) / 2 / rx; /* ux - umx */ udy = (nry - nry0) / 2 / ry; /* uy - umy */ umx = (nrx + nrx0) / 2 / rx; umy = (nry + nry0) / 2 / ry; udx2 = udx * udx; udy2 = udy * udy; udl2 = udx2 + udy2; if(udy != 0) { /* center is at left-side of arc */ udcx = -sqrtf((1 - udl2) * udl2) / (udy + udx2 / udy); udcy = -udcx * udx / udy; } else { /* center is at down-side of arc */ udcx = 0; udcy = sqrtf((1 - udl2) * udl2) / udx; } reflect = 0; if(large) reflect ^= 1; if(sweep != 1) reflect ^= 1; if(reflect) { udcx = -udcx; udcy = -udcy; } nrcx = rx * (udcx + umx); nrcy = ry * (udcy + umy); *cx = nrcx * _cos - nrcy * _sin; *cy = nrcx * _sin + nrcy * _cos; *xx = rx * _cos + *cx; *xy = rx * _sin + *cy; return OK; } #define TAKE_NUM(r) do { \ SKIP_SPACE(p); \ old = p; \ SKIP_NUM(p); \ if(p == old) \ return ERR; \ r = atof(old); \ } while(0); static int sh_path_arc_cmd_arg_fill(char cmd, char **cmds_p, const char **data_p, co_aix **args_p, int **fix_args_p) { co_aix rx, ry; co_aix x_rotate; int large, sweep; co_aix x, y, x0, y0, cx, cy, xx, xy; co_aix *args = *args_p; const char *old; const char *p; char *cmds; int *fix_args; p = *data_p; cmds = *cmds_p; fix_args = *fix_args_p; while(*p) { SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) break; rx = atof(old); TAKE_NUM(ry); TAKE_NUM(x_rotate); TAKE_NUM(large); TAKE_NUM(sweep); TAKE_NUM(x); TAKE_NUM(y) x0 = *(args - 2); y0 = *(args - 1); if(islower(cmd)) { x += x0; y += y0; } calc_center_and_x_aix(x0, y0, x, y, rx, ry, x_rotate, large, sweep, &cx, &cy, &xx, &xy); *(args++) = cx; *(args++) = cy; *(args++) = xx; *(args++) = xy; *(args++) = x; *(args++) = y; *cmds++ = toupper(cmd); *fix_args++ = sweep; } *data_p = p; *args_p = args; *cmds_p = cmds; *fix_args_p = fix_args; return OK; } #define INNER(x1, y1, x2, y2) ((x1) * (x2) + (y1) * (y2)) #define CROSS(x1, y1, x2, y2) ((x1) * (y2) - (y1) * (x2)) /*! \brief Make path for arcs in a path. */ void sh_path_arc_path(cairo_t *cr, const co_aix **args_p, const int **fix_args_p) { co_aix cx, cy, x0, y0, x, y, xx, xy; co_aix dx, dy, dx0, dy0, dxx, dxy; co_aix xyratio; co_aix rx; co_aix rx2; co_aix inner0, cross0; co_aix circle_h0; co_aix inner, cross; co_aix angle, angle0; co_aix rotate; const co_aix *args = *args_p; const int *fix_args = *fix_args_p; int sweep; x0 = *(args - 2); y0 = *(args - 1); cx = *args++; cy = *args++; xx = *args++; xy = *args++; x = *args++; y = *args++; sweep = *fix_args++; dx = x - cx; dy = y - cy; dx0 = x0 - cx; dy0 = y0 - cy; dxx = xx - cx; dxy = xy - cy; rx2 = dxx * dxx + dxy * dxy; rx = sqrtf(rx2); /*! \note Why we calculate these numbers there? * If we compute it when filling arguments, sh_path_arc_cmd_arg_fill(), * we can avoid to recompute it for every drawing. But, transforming of * coordinate can effect value of the numbers. */ inner0 = INNER(dxx, dxy, dx0, dy0); cross0 = CROSS(dxx, dxy, dx0, dy0); circle_h0 = sqrtf(rx2 - inner0 * inner0 / rx2); xyratio = cross0 / rx / circle_h0; if(xyratio < 0) xyratio = -xyratio; angle0 = acos(inner0 / rx2); if(cross0 < 0) angle0 = PI * 2 - angle0; /* 3rd, 4th Quadrant */ inner = INNER(dxx, dxy, dx, dy); cross = CROSS(dxx, dxy, dx, dy); angle = acos(inner / rx2); if(cross < 0) angle = PI * 2 - angle; /* 3rd, 4th Quadrant */ /* Make a path for arc */ rotate = acos(dxx / rx); cairo_save(cr); cairo_translate(cr, cx, cy); cairo_rotate(cr, rotate); cairo_scale(cr, 1.0, xyratio); if(sweep) cairo_arc(cr, 0, 0, rx, angle0, angle); else cairo_arc_negative(cr, 0, 0, rx, angle0, angle); cairo_restore(cr); *args_p = args; *fix_args_p = fix_args; } /* ============================================================ */ static void sh_path_free(shape_t *shape) { sh_path_t *path = (sh_path_t *)shape; if(path->user_data) free(path->user_data); free(path); } /*! \brief Count number of arguments. * * \todo Notify programmers that syntax or value error of path data. */ static int sh_path_cmd_arg_cnt(char *data, int *cmd_cntp, int *arg_cntp, int *fix_arg_cntp) { char *p, *old; int cmd_cnt, arg_cnt, fix_arg_cnt; int i; cmd_cnt = arg_cnt = fix_arg_cnt = 0; p = data; SKIP_SPACE(p); while(*p) { switch(*p++) { case 'c': case 'C': while(*p) { old = p; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) break; arg_cnt++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; arg_cnt++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; arg_cnt++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; arg_cnt++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; arg_cnt++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; arg_cnt++; cmd_cnt++; } break; case 's': case 'S': case 'q': case 'Q': while(*p) { old = p; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) break; arg_cnt++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; arg_cnt++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; arg_cnt++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; arg_cnt++; cmd_cnt++; } break; case 'm': case 'M': case 'l': case 'L': case 't': case 'T': while(*p) { old = p; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) break; arg_cnt++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; arg_cnt++; cmd_cnt++; } break; case 'h': case 'H': case 'v': case 'V': while(*p) { SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) break; arg_cnt += 2; cmd_cnt++; } break; case 'A': case 'a': while(*p) { SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) break; for(i = 0; i < 6; i++) { SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; } arg_cnt += 6; fix_arg_cnt++; cmd_cnt++; } break; case 'z': case 'Z': cmd_cnt++; break; default: return ERR; } /*! \todo cmd_cnt should be increased for each implicit repeating. */ SKIP_SPACE(p); } *cmd_cntp = cmd_cnt; *arg_cntp = arg_cnt; *fix_arg_cntp = fix_arg_cnt; return OK; } #define TO_ABSX islower(cmd)? x + atof(old): atof(old) #define TO_ABSY islower(cmd)? y + atof(old): atof(old) static int sh_path_cmd_arg_fill(char *data, sh_path_t *path) { char *p, *old; char *cmds; char cmd; co_aix *args; int *fix_args; co_aix x, y; int r; cmds = path->user_data; args = (co_aix *)(cmds + path->cmd_len); fix_args = (int *)(cmds + path->cmd_len + path->arg_len * sizeof(co_aix)); p = data; SKIP_SPACE(p); while(*p) { /* Transform all relative to absolute, */ x = *(args - 2); y = *(args - 1); switch((cmd = *p++)) { case 'c': case 'C': while(*p) { old = p; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) break; *args = TO_ABSX; args++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; *args = TO_ABSY; args++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; *args = TO_ABSX; args++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; *args = TO_ABSY; args++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; *args = TO_ABSX; args++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; *args = TO_ABSY; args++; *cmds++ = toupper(cmd); } break; case 's': case 'S': case 'q': case 'Q': while(*p) { old = p; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) break; *args = TO_ABSX; args++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; *args = TO_ABSY; args++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; *args = TO_ABSX; args++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; *args = TO_ABSY; args++; *cmds++ = toupper(cmd); } break; case 'm': case 'M': case 'l': case 'L': case 't': case 'T': while(*p) { old = p; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) break; *args = TO_ABSX; args++; SKIP_SPACE(p); old = p; SKIP_NUM(p); if(p == old) return ERR; *args = TO_ABSY; args++; *cmds++ = toupper(cmd); } break; case 'h': case 'H': case 'v': case 'V': /*! \todo implement h, H, v, V comamnds for path. */ return ERR; case 'A': case 'a': r = sh_path_arc_cmd_arg_fill(cmd, &cmds, (const char **)&p, &args, &fix_args); if(r != OK) return ERR; break; case 'z': case 'Z': *cmds++ = toupper(cmd); break; default: return ERR; } SKIP_SPACE(p); } return OK; } /*! \brief Create a path from value of 'data' of SVG path. */ shape_t *rdman_shape_path_new(redraw_man_t *rdman, char *data) { sh_path_t *path; int cmd_cnt, arg_cnt, fix_arg_cnt; int msz; int r; r = sh_path_cmd_arg_cnt(data, &cmd_cnt, &arg_cnt, &fix_arg_cnt); if(r == ERR) return NULL; /* Align at 4's boundary and keep 2 unused co_aix space * to make logic of transformation from relative to absolute * simple. */ cmd_cnt += RESERVED_AIXS; cmd_cnt = (cmd_cnt + 3) & ~0x3; /*! \todo Use elmpool to manage sh_path_t objects. */ path = (sh_path_t *)malloc(sizeof(sh_path_t)); /*! \todo Remove this memset()? */ memset(&path->shape, 0, sizeof(shape_t)); mb_obj_init(path, MBO_PATH); path->cmd_len = cmd_cnt; path->arg_len = arg_cnt; path->fix_arg_len = fix_arg_cnt; msz = cmd_cnt + sizeof(co_aix) * arg_cnt + sizeof(int) * fix_arg_cnt; path->user_data = (char *)malloc(msz * 2); if(path->user_data == NULL) { free(path); return NULL; } path->dev_data = path->user_data + msz; r = sh_path_cmd_arg_fill(data, path); if(r == ERR) { free(path->user_data); free(path); return NULL; } memcpy(path->dev_data, path->user_data, msz); path->shape.free = sh_path_free; rdman_shape_man(rdman, (shape_t *)path); return (shape_t *)path; } shape_t *rdman_shape_path_new_from_binary(redraw_man_t *rdman, char *commands, co_aix *arg,int arg_cnt,int *fix_arg,int fix_arg_cnt) { sh_path_t *path; int msz; int cmd_cnt = strlen(commands); /*! \todo Use elmpool to manage sh_path_t objects. */ path = (sh_path_t *)malloc(sizeof(sh_path_t)); /*! \todo Remove this memset()? */ memset(&path->shape, 0, sizeof(shape_t)); mb_obj_init(path, MBO_PATH); path->cmd_len = strlen(commands); path->arg_len = arg_cnt; path->fix_arg_len = fix_arg_cnt; msz = cmd_cnt + sizeof(co_aix) * arg_cnt + sizeof(int) * fix_arg_cnt; path->user_data = (char *)malloc(msz * 2); if(path->user_data == NULL) { free(path); return NULL; } path->dev_data = path->user_data + msz; memcpy(path->user_data,commands,cmd_cnt); memcpy(path->user_data+cmd_cnt,arg, sizeof(co_aix)*arg_cnt); memcpy(path->user_data+cmd_cnt+arg_cnt*sizeof(co_aix),fix_arg, sizeof(int)*fix_arg_cnt); memcpy(path->dev_data, path->user_data, msz); path->shape.free = sh_path_free; rdman_shape_man(rdman, (shape_t *)path); return (shape_t *)path; } /*! \brief Transform a path from user space to device space. * */ void sh_path_transform(shape_t *shape) { sh_path_t *path; co_aix *user_args, *dev_args; co_aix (*poses)[2]; area_t *area; int arg_len; int i; ASSERT(shape->type == SHT_PATH); ASSERT((shape->arg_len & 0x1) == 0); path = (sh_path_t *)shape; user_args = (co_aix *)(path->user_data + path->cmd_len); dev_args = (co_aix *)(path->dev_data + path->cmd_len); arg_len = path->arg_len; for(i = 0; i < arg_len; i += 2) { dev_args[0] = *user_args++; dev_args[1] = *user_args++; coord_trans_pos(shape->coord, dev_args, dev_args + 1); dev_args += 2; } if(path->shape.geo) { poses = (co_aix (*)[2])(path->dev_data + path->cmd_len); geo_from_positions(path->shape.geo, arg_len / 2, poses); area = shape->geo->cur_area; area->x -= shape->stroke_width / 2 + 0.5; area->y -= shape->stroke_width / 2 + 0.5; area->w += shape->stroke_width + 1; area->h += shape->stroke_width + 1; } } static void sh_path_path(shape_t *shape, cairo_t *cr) { sh_path_t *path; int cmd_len; char *cmds, cmd; const co_aix *args; const int *fix_args; co_aix x, y, x1, y1, x2, y2; int i; ASSERT(shape->type == SHT_PATH); path = (sh_path_t *)shape; cmd_len = path->cmd_len; cmds = path->dev_data; args = (co_aix *)(cmds + cmd_len); fix_args = (int *)(cmds + cmd_len + path->arg_len * sizeof(co_aix)); x = y = x1 = y1 = x2 = y2 = 0; for(i = 0; i < cmd_len; i++) { /* All path commands and arguments are transformed * to absoluted form. */ cmd = *cmds++; switch(cmd) { case 'M': x = *args++; y = *args++; cairo_move_to(cr, x, y); break; case 'L': x = *args++; y = *args++; cairo_line_to(cr, x, y); break; case 'C': x1 = *args++; y1 = *args++; x2 = *args++; y2 = *args++; x = *args++; y = *args++; cairo_curve_to(cr, x1, y1, x2, y2, x, y); break; case 'S': x1 = x + x - x2; y1 = y + y - y2; x2 = *args++; y2 = *args++; x = *args++; y = *args++; cairo_curve_to(cr, x1, y1, x2, y2, x, y); break; case 'Q': x1 = *args++; y1 = *args++; x2 = x1; y2 = y1; x = *args++; y = *args++; cairo_curve_to(cr, x1, y1, x2, y2, x, y); break; case 'T': x1 = x + x - x2; y1 = y + y - y2; x2 = x1; y2 = y1; x = *args++; y = *args++; cairo_curve_to(cr, x1, y1, x2, y2, x, y); break; case 'A': sh_path_arc_path(cr, &args, &fix_args); break; case 'Z': cairo_close_path(cr); break; case '\x0': i = cmd_len; /* padding! Skip remain ones. */ break; } } } void sh_path_draw(shape_t *shape, cairo_t *cr) { sh_path_path(shape, cr); } #ifdef UNITTEST #include <CUnit/Basic.h> void test_rdman_shape_path_new(void) { sh_path_t *path; co_aix *args; path = (sh_path_t *)rdman_shape_path_new(NULL, "M 33 25l33 55c 33 87 44 22 55 99L33 77z"); CU_ASSERT(path != NULL); CU_ASSERT(path->cmd_len == ((5 + RESERVED_AIXS + 3) & ~0x3)); CU_ASSERT(path->arg_len == 12); CU_ASSERT(strncmp(path->user_data, "MLCLZ", 5) == 0); CU_ASSERT(strncmp(path->dev_data, "MLCLZ", 5) == 0); args = (co_aix *)(path->user_data + path->cmd_len); CU_ASSERT(args[0] == 33); CU_ASSERT(args[1] == 25); CU_ASSERT(args[2] == 66); CU_ASSERT(args[3] == 80); CU_ASSERT(args[4] == 99); CU_ASSERT(args[5] == 167); CU_ASSERT(args[6] == 110); CU_ASSERT(args[7] == 102); CU_ASSERT(args[8] == 121); CU_ASSERT(args[9] == 179); CU_ASSERT(args[10] == 33); CU_ASSERT(args[11] == 77); sh_path_free((shape_t *)path); } void test_path_transform(void) { sh_path_t *path; co_aix *args; coord_t coord; geo_t geo; path = (sh_path_t *)rdman_shape_path_new(NULL, "M 33 25l33 55C 33 87 44 22 55 99L33 77z"); CU_ASSERT(path != NULL); CU_ASSERT(path->cmd_len == ((5 + RESERVED_AIXS + 3) & ~0x3)); CU_ASSERT(path->arg_len == 12); CU_ASSERT(strncmp(path->user_data, "MLCLZ", 5) == 0); CU_ASSERT(strncmp(path->dev_data, "MLCLZ", 5) == 0); geo_init(&geo); path->shape.geo = &geo; geo.shape = (shape_t *)path; coord.aggr_matrix[0] = 1; coord.aggr_matrix[1] = 0; coord.aggr_matrix[2] = 1; coord.aggr_matrix[3] = 0; coord.aggr_matrix[4] = 2; coord.aggr_matrix[5] = 0; path->shape.coord = &coord; sh_path_transform((shape_t *)path); args = (co_aix *)(path->dev_data + path->cmd_len); CU_ASSERT(args[0] == 34); CU_ASSERT(args[1] == 50); CU_ASSERT(args[2] == 67); CU_ASSERT(args[3] == 160); CU_ASSERT(args[4] == 34); CU_ASSERT(args[5] == 174); CU_ASSERT(args[6] == 45); CU_ASSERT(args[7] == 44); CU_ASSERT(args[8] == 56); CU_ASSERT(args[9] == 198); CU_ASSERT(args[10] == 34); CU_ASSERT(args[11] == 154); sh_path_free((shape_t *)path); } void test_spaces_head_tail(void) { sh_path_t *path; path = (sh_path_t *) rdman_shape_path_new(NULL, " M 33 25l33 55C 33 87 44 22 55 99L33 77z "); CU_ASSERT(path != NULL); sh_path_free((shape_t *)path); } CU_pSuite get_shape_path_suite(void) { CU_pSuite suite; suite = CU_add_suite("Suite_shape_path", NULL, NULL); CU_ADD_TEST(suite, test_rdman_shape_path_new); CU_ADD_TEST(suite, test_path_transform); return suite; } #endif /* UNITTEST */