/* $Header: /home/cvsroot/dvipdfmx/src/spc_html.c,v 1.6 2007/01/19 11:00:48 chofchof Exp $ This is dvipdfmx, an eXtended version of dvipdfm by Mark A. Wicks. Copyright (C) 2002 by Jin-Hwan Cho and Shunsaku Hirata, the dvipdfmx project team Copyright (C) 1998, 1999 by Mark A. Wicks This program 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. This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "system.h" #include "mem.h" #include "error.h" #include "dpxutil.h" #include "pdfdraw.h" #include "pdfdev.h" #include "pdfximage.h" #include "pdfdoc.h" #include "specials.h" #include "spc_util.h" #include "spc_html.h" #define ENABLE_HTML_IMG_SUPPORT 1 #define ENABLE_HTML_SVG_TRANSFORM 1 #define ENABLE_HTML_SVG_OPACITY 1 /* _FIXME_ * Please rewrite this or remove html special support */ #define ANCHOR_TYPE_HREF 0 #define ANCHOR_TYPE_NAME 1 struct spc_html_ { struct { long extensions; } opts; pdf_obj *link_dict; char *baseurl; int pending_type; }; static struct spc_html_ _html_state = { { 0 }, NULL, NULL, -1 }; #ifdef ENABLE_HTML_SVG_TRANSFORM static int cvt_a_to_tmatrix (pdf_tmatrix *M, const char *ptr, char **nextptr); #endif /* ENABLE_HTML_SVG_TRANSFORM */ #define \ downcasify(s) \ if ((s)) { \ char *_p = (char *) (s); \ while (*(_p) != 0) { \ if (*(_p) >= 'A' && *(_p) <= 'Z') { \ *(_p) = (*(_p) - 'A') + 'a'; \ } \ _p++; \ } \ } static int parse_key_val (char **pp, char *endptr, char **kp, char **vp) { char *q, *p, *k, *v; int n, error = 0; for (p = *pp ; p < endptr && isspace(*p); p++); #if 0 while (!error && p < endptr && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')) ) { #endif k = v = NULL; for (q = p, n = 0; p < endptr && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '-' || *p == ':' ); n++, p++); if (n == 0) { #if 0 break; #else *kp = *vp = NULL; return -1; #endif } k = NEW(n + 1, char); memcpy(k, q, n); k[n] = '\0'; if (p + 2 >= endptr || p[0] != '=' || (p[1] != '\"' && p[1] != '\'')) { RELEASE(k); k = NULL; *pp = p; error = -1; } else { char qchr = p[1]; p += 2; /* skip '="' */ for (q = p, n = 0; p < endptr && *p != qchr; p++, n++); if (p == endptr || *p != qchr) error = -1; else { v = NEW(n + 1, char); memcpy(v, q, n); v[n] = '\0'; #if 0 pdf_add_dict(t->attr, pdf_new_name(k), pdf_new_string(v, n)); RELEASE(v); #endif p++; } } #if 0 RELEASE(k); if (!error) for ( ; p < endptr && isspace(*p); p++); } #endif *kp = k; *vp = v; *pp = p; return error; } #define HTML_TAG_NAME_MAX 127 #define HTML_TAG_TYPE_EMPTY 1 #define HTML_TAG_TYPE_OPEN 1 #define HTML_TAG_TYPE_CLOSE 2 static int read_html_tag (char *name, pdf_obj *attr, int *type, char **pp, char *endptr) { char *p = *pp; int n = 0, error = 0; for ( ; p < endptr && isspace(*p); p++); if (p >= endptr || *p != '<') return -1; *type = HTML_TAG_TYPE_OPEN; for (++p; p < endptr && isspace(*p); p++); if (p < endptr && *p == '/') { *type = HTML_TAG_TYPE_CLOSE; for (++p; p < endptr && isspace(*p); p++); } #define ISDELIM(c) ((c) == '>' || (c) == '/' || isspace(c)) for (n = 0; p < endptr && n < HTML_TAG_NAME_MAX && !ISDELIM(*p); n++, p++) { name[n] = *p; } name[n] = '\0'; if (n == 0 || p == endptr || !ISDELIM(*p)) { *pp = p; return -1; } for ( ; p < endptr && isspace(*p); p++); while (p < endptr && !error && *p != '/' && *p != '>') { char *kp = NULL, *vp = NULL; error = parse_key_val(&p, endptr, &kp, &vp); if (!error) { downcasify(kp); pdf_add_dict(attr, pdf_new_name(kp), pdf_new_string(vp, strlen(vp) + 1)); /* include trailing NULL here!!! */ RELEASE(kp); RELEASE(vp); } for ( ; p < endptr && isspace(*p); p++); } if (error) { *pp = p; return error; } if (p < endptr && *p == '/') { *type = HTML_TAG_TYPE_EMPTY; for (++p; p < endptr && isspace(*p); p++); } if (p == endptr || *p != '>') { *pp = p; return -1; } p++; downcasify(name); *pp = p; return 0; } static int spc_handler_html__init (struct spc_env *spe, struct spc_arg *ap, void *dp) { struct spc_html_ *sd = dp; sd->link_dict = NULL; sd->baseurl = NULL; sd->pending_type = -1; return 0; } static int spc_handler_html__clean (struct spc_env *spe, struct spc_arg *ap, void *dp) { struct spc_html_ *sd = dp; if (sd->baseurl) RELEASE(sd->baseurl); if (sd->pending_type >= 0 || sd->link_dict) spc_warn(spe, "Unclosed html anchor found."); if (sd->link_dict) pdf_release_obj(sd->link_dict); sd->pending_type = -1; sd->baseurl = NULL; sd->link_dict = NULL; return 0; } static int spc_handler_html__bophook (struct spc_env *spe, struct spc_arg *ap, void *dp) { struct spc_html_ *sd = dp; if (sd->pending_type >= 0) { spc_warn(spe, "...html anchor continues from previous page processed..."); } return 0; } static int spc_handler_html__eophook (struct spc_env *spe, struct spc_arg *ap, void *dp) { struct spc_html_ *sd = dp; if (sd->pending_type >= 0) { spc_warn(spe, "Unclosed html anchor at end-of-page!"); } return 0; } static char * fqurl (const char *baseurl, const char *name) { char *q; int len = 0; len = strlen(name); if (baseurl) len += strlen(baseurl) + 1; /* we may want to add '/' */ q = NEW(len + 1, char); *q = '\0'; if (baseurl && baseurl[0]) { char *p; strcpy(q, baseurl); p = q + strlen(q) - 1; if (*p == '/') *p = '\0'; if (name[0] && name[0] != '/') strcat(q, "/"); } strcat(q, name); return q; } static int html_open_link (struct spc_env *spe, const char *name, struct spc_html_ *sd) { pdf_obj *color; char *url; ASSERT( name ); ASSERT( sd->link_dict == NULL ); /* Should be checked somewhere else */ sd->link_dict = pdf_new_dict(); pdf_add_dict(sd->link_dict, pdf_new_name("Type"), pdf_new_name ("Annot")); pdf_add_dict(sd->link_dict, pdf_new_name("Subtype"), pdf_new_name ("Link")); color = pdf_new_array (); pdf_add_array(color, pdf_new_number(0.0)); pdf_add_array(color, pdf_new_number(0.0)); pdf_add_array(color, pdf_new_number(1.0)); pdf_add_dict(sd->link_dict, pdf_new_name("C"), color); url = fqurl(sd->baseurl, name); if (url[0] == '#') { /* url++; causes memory leak in RELEASE(url) */ pdf_add_dict(sd->link_dict, pdf_new_name("Dest"), pdf_new_string(url+1, strlen(url+1))); } else { /* Assume this is URL */ pdf_obj *action = pdf_new_dict(); pdf_add_dict(action, pdf_new_name("Type"), pdf_new_name("Action")); pdf_add_dict(action, pdf_new_name("S"), pdf_new_name("URI")); pdf_add_dict(action, pdf_new_name("URI"), pdf_new_string(url, strlen(url))); pdf_add_dict(sd->link_dict, pdf_new_name("A"), pdf_link_obj(action)); pdf_release_obj(action); } RELEASE(url); spc_begin_annot(spe, sd->link_dict); sd->pending_type = ANCHOR_TYPE_HREF; return 0; } static int html_open_dest (struct spc_env *spe, const char *name, struct spc_html_ *sd) { int error; pdf_obj *array, *page_ref; pdf_coord cp; cp.x = spe->x_user; cp.y = spe->y_user; pdf_dev_transform(&cp, NULL); page_ref = pdf_doc_this_page_ref(); ASSERT( page_ref ); /* Otherwise must be bug */ array = pdf_new_array(); pdf_add_array(array, page_ref); pdf_add_array(array, pdf_new_name("XYZ")); pdf_add_array(array, pdf_new_null()); pdf_add_array(array, pdf_new_number(cp.y + 24.0)); pdf_add_array(array, pdf_new_null()); error = pdf_doc_add_names("Dests", name, strlen(name), array); if (error) spc_warn(spe, "Failed to add named destination: %s", name); sd->pending_type = ANCHOR_TYPE_NAME; return error; } #define ANCHOR_STARTED(s) ((s)->pending_type >= 0 || (s)->link_dict) static int spc_html__anchor_open (struct spc_env *spe, pdf_obj *attr, struct spc_html_ *sd) { pdf_obj *href, *name; int error = 0; if (ANCHOR_STARTED(sd)) { spc_warn(spe, "Nested html anchors found!"); return -1; } href = pdf_lookup_dict(attr, "href"); name = pdf_lookup_dict(attr, "name"); if (href && name) { spc_warn(spe, "Sorry, you can't have both \"href\" and \"name\" in anchor tag..."); error = -1; } else if (href) { error = html_open_link(spe, pdf_string_value(href), sd); } else if (name) { /* name */ error = html_open_dest(spe, pdf_string_value(name), sd); } else { spc_warn(spe, "You should have \"href\" or \"name\" in anchor tag!"); error = -1; } return error; } static int spc_html__anchor_close (struct spc_env *spe, pdf_obj *attr, struct spc_html_ *sd) { int error = 0; switch (sd->pending_type) { case ANCHOR_TYPE_HREF: if (sd->link_dict) { spc_end_annot(spe); pdf_release_obj(sd->link_dict); sd->link_dict = NULL; sd->pending_type = -1; } else { spc_warn(spe, "Closing html anchor (link) without starting!"); error = -1; } break; case ANCHOR_TYPE_NAME: sd->pending_type = -1; break; default: spc_warn(spe, "No corresponding opening tag for html anchor."); error = -1; break; } return error; } static int spc_html__base_empty (struct spc_env *spe, pdf_obj *attr, struct spc_html_ *sd) { pdf_obj *href; char *vp; href = pdf_lookup_dict(attr, "href"); if (!href) { spc_warn(spe, "\"href\" not found for \"base\" tag!"); return -1; } vp = (char *) pdf_string_value(href); if (sd->baseurl) { spc_warn(spe, "\"baseurl\" changed: \"%s\" --> \"%s\"", sd->baseurl, vp); RELEASE(sd->baseurl); } sd->baseurl = NEW(strlen(vp) + 1, char); strcpy(sd->baseurl, vp); return 0; } #ifdef ENABLE_HTML_IMG_SUPPORT /* This isn't completed. * Please think about placement of images. */ static double atopt (const char *a) { char *q, *p = (char *) a; double v, u = 1.0; const char *_ukeys[] = { #define K_UNIT__PT 0 #define K_UNIT__IN 1 #define K_UNIT__CM 2 #define K_UNIT__MM 3 #define K_UNIT__BP 4 "pt", "in", "cm", "mm", "bp", #define K_UNIT__PX 5 "px", NULL }; int k; q = parse_float_decimal(&p, p + strlen(p)); if (!q) { WARN("Invalid length value: %s (%c)", a, *p); return 0.0; } v = atof(q); RELEASE(q); q = parse_c_ident(&p, p + strlen(p)); if (q) { for (k = 0; _ukeys[k] && strcmp(_ukeys[k], q); k++); switch (k) { case K_UNIT__PT: u *= 72.0 / 72.27; break; case K_UNIT__IN: u *= 72.0; break; case K_UNIT__CM: u *= 72.0 / 2.54 ; break; case K_UNIT__MM: u *= 72.0 / 25.4 ; break; case K_UNIT__BP: u *= 1.0 ; break; case K_UNIT__PX: u *= 1.0 ; break; /* 72dpi */ default: WARN("Unknown unit of measure: %s", q); break; } RELEASE(q); } return v * u; } #ifdef ENABLE_HTML_SVG_OPACITY /* Replicated from spc_tpic */ static pdf_obj * create_xgstate (double a /* alpha */, int f_ais /* alpha is shape */) { pdf_obj *dict; dict = pdf_new_dict(); pdf_add_dict(dict, pdf_new_name("Type"), pdf_new_name("ExtGState")); if (f_ais) { pdf_add_dict(dict, pdf_new_name("AIS"), pdf_new_boolean(1)); } pdf_add_dict(dict, pdf_new_name("ca"), pdf_new_number(a)); return dict; } static int check_resourcestatus (const char *category, const char *resname) { pdf_obj *dict1, *dict2; dict1 = pdf_doc_current_page_resources(); if (!dict1) return 0; dict2 = pdf_lookup_dict(dict1, category); if (dict2 && pdf_obj_typeof(dict2) == PDF_DICT) { if (pdf_lookup_dict(dict2, resname)) return 1; } return 0; } #endif /* ENABLE_HTML_SVG_OPACITY */ static int spc_html__img_empty (struct spc_env *spe, pdf_obj *attr, struct spc_html_ *sd) { pdf_obj *src, *obj; transform_info ti; int id, error = 0; #ifdef ENABLE_HTML_SVG_OPACITY double alpha = 1.0; /* meaning fully opaque */ #endif /* ENABLE_HTML_SVG_OPACITY */ #ifdef ENABLE_HTML_SVG_TRANSFORM pdf_tmatrix M; pdf_setmatrix(&M, 1.0, 0.0, 0.0, 1.0, spe->x_user, spe->y_user); #endif /* ENABLE_HTML_SVG_TRANSFORM */ spc_warn(spe, "html \"img\" tag found (not completed, plese don't use!)."); src = pdf_lookup_dict(attr, "src"); if (!src) { spc_warn(spe, "\"src\" attribute not found for \"img\" tag!"); return -1; } transform_info_clear(&ti); obj = pdf_lookup_dict(attr, "width"); if (obj) { ti.width = atopt(pdf_string_value(obj)); ti.flags |= INFO_HAS_WIDTH; } obj = pdf_lookup_dict(attr, "height"); if (obj) { ti.height = atopt(pdf_string_value(obj)); ti.flags |= INFO_HAS_HEIGHT; } #ifdef ENABLE_HTML_SVG_OPACITY obj = pdf_lookup_dict(attr, "svg:opacity"); if (obj) { alpha = atof(pdf_string_value(obj)); if (alpha < 0.0 || alpha > 1.0) { spc_warn(spe, "Invalid opacity value: %s", pdf_string_value(obj)); alpha = 1.0; } } #endif /* ENABLE_HTML_SVG_OPCAITY */ #ifdef ENABLE_HTML_SVG_TRANSFORM obj = pdf_lookup_dict(attr, "svg:transform"); if (obj) { char *p = (char *) pdf_string_value(obj); pdf_tmatrix N; for ( ; *p && isspace(*p); p++); while (*p && !error) { pdf_setmatrix(&N, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0); error = cvt_a_to_tmatrix(&N, p, &p); if (!error) { N.f = -N.f; pdf_concatmatrix(&M, &N); for ( ; *p && isspace(*p); p++); if (*p == ',') for (++p; *p && isspace(*p); p++); } } } #endif /* ENABLE_HTML_SVG_TRANSFORM */ if (error) { spc_warn(spe, "Error in html \"img\" tag attribute."); return error; } id = pdf_ximage_findresource(pdf_string_value(src), 0/*, 0*/); if (id < 0) { spc_warn(spe, "Could not find/load image: %s", pdf_string_value(src)); error = -1; } else { #if defined(ENABLE_HTML_SVG_TRANSFORM) || defined(ENABLE_HTML_SVG_OPACITY) { char *res_name; pdf_rect r; graphics_mode(); pdf_dev_gsave(); #ifdef ENABLE_HTML_SVG_OPACITY { pdf_obj *dict; int a = round(100.0 * alpha); if (a != 0) { res_name = NEW(strlen("_Tps_a100_") + 1, char); sprintf(res_name, "_Tps_a%03d_", a); /* Not Tps prefix but... */ if (!check_resourcestatus("ExtGState", res_name)) { dict = create_xgstate(round_at(0.01 * a, 0.01), 0); pdf_doc_add_page_resource("ExtGState", res_name, pdf_ref_obj(dict)); pdf_release_obj(dict); } pdf_doc_add_page_content(" /", 2); pdf_doc_add_page_content(res_name, strlen(res_name)); pdf_doc_add_page_content(" gs", 3); RELEASE(res_name); } } #endif /* ENABLE_HTML_SVG_OPACITY */ pdf_dev_concat(&M); pdf_ximage_scale_image(id, &M, &r, &ti); pdf_dev_concat(&M); pdf_dev_rectclip(r.llx, r.lly, r.urx - r.llx, r.ury - r.lly); res_name = pdf_ximage_get_resname(id); pdf_doc_add_page_content(" /", 2); pdf_doc_add_page_content(res_name, strlen(res_name)); pdf_doc_add_page_content(" Do", 3); pdf_dev_grestore(); pdf_doc_add_page_resource("XObject", res_name, pdf_ximage_get_reference(id)); } #else pdf_dev_put_image(id, &ti, spe->x_user, spe->y_user); #endif /* ENABLE_HTML_SVG_XXX */ } return error; } #else static int spc_html__img_empty (struct spc_env *spe, pdf_obj *attr, struct spc_html_ *sd) { spc_warn(spe, "IMG tag not yet supported yet..."); return -1; } #endif /* ENABLE_HTML_IMG_SUPPORT */ static int spc_handler_html_default (struct spc_env *spe, struct spc_arg *ap) { struct spc_html_ *sd = &_html_state; char name[HTML_TAG_NAME_MAX + 1]; pdf_obj *attr; int error = 0, type = HTML_TAG_TYPE_OPEN; if (ap->curptr >= ap->endptr) return 0; attr = pdf_new_dict(); error = read_html_tag(name, attr, &type, &ap->curptr, ap->endptr); if (error) { pdf_release_obj(attr); return error; } if (!strcmp(name, "a")) { switch (type) { case HTML_TAG_TYPE_OPEN: error = spc_html__anchor_open (spe, attr, sd); break; case HTML_TAG_TYPE_CLOSE: error = spc_html__anchor_close(spe, attr, sd); break; default: spc_warn(spe, "Empty html anchor tag???"); error = -1; break; } } else if (!strcmp(name, "base")) { if (type == HTML_TAG_TYPE_CLOSE) { spc_warn(spe, "Close tag for \"base\"???"); error = -1; } else { /* treat "open" same as "empty" */ error = spc_html__base_empty(spe, attr, sd); } } else if (!strcmp(name, "img")) { if (type == HTML_TAG_TYPE_CLOSE) { spc_warn(spe, "Close tag for \"img\"???"); error = -1; } else { /* treat "open" same as "empty" */ error = spc_html__img_empty(spe, attr, sd); } } pdf_release_obj(attr); for ( ; ap->curptr < ap->endptr && isspace(ap->curptr[0]); ap->curptr++); return error; } #ifdef ENABLE_HTML_SVG_TRANSFORM /* translate wsp* '(' wsp* number (comma-wsp number)? wsp* ')' */ static int cvt_a_to_tmatrix (pdf_tmatrix *M, const char *ptr, char **nextptr) { char *q, *p = (char *) ptr; int n; double v[6]; static const char *_tkeys[] = { #define K_TRNS__MATRIX 0 "matrix", /* a b c d e f */ #define K_TRNS__TRANSLATE 1 "translate", /* tx [ty] : dflt. tf = 0 */ #define K_TRNS__SCALE 2 "scale", /* sx [sy] : dflt. sy = sx */ #define K_TRNS__ROTATE 3 "rotate", /* ang [cx cy] : dflt. cx, cy = 0 */ #define K_TRNS__SKEWX 4 #define K_TRNS__SKEWY 5 "skewX", /* ang */ "skewY", /* ang */ NULL }; int k; for ( ; *p && isspace(*p); p++); q = parse_c_ident(&p, p + strlen(p)); if (!q) return -1; /* parsed transformation key */ for (k = 0; _tkeys[k] && strcmp(q, _tkeys[k]); k++); RELEASE(q); /* handle args */ for ( ; *p && isspace(*p); p++); if (*p != '(' || *(p + 1) == 0) return -1; for (++p; *p && isspace(*p); p++); for (n = 0; n < 6 && *p && *p != ')'; n++) { q = parse_float_decimal(&p, p + strlen(p)); if (!q) break; else { v[n] = atof(q); if (*p == ',') p++; for ( ; *p && isspace(*p); p++); if (*p == ',') for (++p; *p && isspace(*p); p++); RELEASE(q); } } if (*p != ')') return -1; p++; switch (k) { case K_TRNS__MATRIX: if (n != 6) return -1; M->a = v[0]; M->c = v[1]; M->b = v[2]; M->d = v[3]; M->e = v[4]; M->f = v[5]; break; case K_TRNS__TRANSLATE: if (n != 1 && n != 2) return -1; M->a = M->d = 1.0; M->c = M->b = 0.0; M->e = v[0]; M->f = (n == 2) ? v[1] : 0.0; break; case K_TRNS__SCALE: if (n != 1 && n != 2) return -1; M->a = v[0]; M->d = (n == 2) ? v[1] : v[0]; M->c = M->b = 0.0; M->e = M->f = 0.0; break; case K_TRNS__ROTATE: if (n != 1 && n != 3) return -1; M->a = cos(v[0] * M_PI / 180.0); M->c = sin(v[0] * M_PI / 180.0); M->b = -M->c; M->d = M->a; M->e = (n == 3) ? v[1] : 0.0; M->f = (n == 3) ? v[2] : 0.0; break; case K_TRNS__SKEWX: if (n != 1) return -1; M->a = M->d = 1.0; M->c = 0.0; M->b = tan(v[0] * M_PI / 180.0); break; case K_TRNS__SKEWY: if (n != 1) return -1; M->a = M->d = 1.0; M->c = tan(v[0] * M_PI / 180.0); M->b = 0.0; break; } if (nextptr) *nextptr = p; return 0; } #endif /* ENABLE_HTML_SVG_TRANSFORM */ int spc_html_at_begin_document (void) { struct spc_html_ *sd = &_html_state; return spc_handler_html__init(NULL, NULL, sd); } int spc_html_at_begin_page (void) { struct spc_html_ *sd = &_html_state; return spc_handler_html__bophook(NULL, NULL, sd); } int spc_html_at_end_page (void) { struct spc_html_ *sd = &_html_state; return spc_handler_html__eophook(NULL, NULL, sd); } int spc_html_at_end_document (void) { struct spc_html_ *sd = &_html_state; return spc_handler_html__clean(NULL, NULL, sd); } int spc_html_check_special (const char *buffer, long size) { char *p, *endptr; p = (char *) buffer; endptr = p + size; for ( ; p < endptr && isspace(*p); p++); size = (long) (endptr - p); if (size >= strlen("html:") && !memcmp(p, "html:", strlen("html:"))) { return 1; } return 0; } int spc_html_setup_handler (struct spc_handler *sph, struct spc_env *spe, struct spc_arg *ap) { ASSERT(sph && spe && ap); for ( ; ap->curptr < ap->endptr && isspace(ap->curptr[0]); ap->curptr++); if (ap->curptr + strlen("html:") > ap->endptr || memcmp(ap->curptr, "html:", strlen("html:"))) { return -1; } ap->command = (char *) ""; sph->key = (char *) "html:"; sph->exec = &spc_handler_html_default; ap->curptr += strlen("html:"); for ( ; ap->curptr < ap->endptr && isspace(ap->curptr[0]); ap->curptr++); return 0; }