From 19c59f209876d5e57323a88783e26898730028e9 Mon Sep 17 00:00:00 2001 From: giraffedata Date: Sun, 12 Jun 2016 17:32:46 +0000 Subject: Add -leftmargin, etc., -ascent, -descent, -pad, -crop, -dump-ps; use asciihex encoding of text to avoid corrupting Postscript program; validate font name to avoid corrupting Postscript program; fail if no input text rather than generate single space; more verbose stuff git-svn-id: http://svn.code.sf.net/p/netpbm/code/trunk@2784 9d0c8265-081b-0410-96cb-a4ca84ce46f8 --- generator/pbmtextps.c | 554 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 432 insertions(+), 122 deletions(-) (limited to 'generator/pbmtextps.c') diff --git a/generator/pbmtextps.c b/generator/pbmtextps.c index 06f7ee3e..fb55fa0a 100644 --- a/generator/pbmtextps.c +++ b/generator/pbmtextps.c @@ -1,4 +1,4 @@ -/* + /* * pbmtextps.c - render text into a bitmap using a postscript interpreter * * Copyright (C) 2002 by James McCann. @@ -18,6 +18,9 @@ * http://www.adobe.com/products/postscript/pdfs/PLRM.pdf * ISBN 0-201-37922-8 * + * Postscript Font Naming Issues: + * https://partners.adobe.com/public/developer/en/font/5088.FontNames.pdf + * * Other resources: * http://partners.adobe.com/public/developer/ps/index_specs.html */ @@ -38,26 +41,74 @@ #include "pm_system.h" #include "pbm.h" -struct CmdlineInfo { - /* All the information the user supplied in the command line, - in a form easy for the program to use. - */ - unsigned int res; /* resolution, DPI */ - unsigned int fontsize; /* Size of font in points */ - const char * font; /* Name of postscript font */ - float stroke; - /* Width of stroke in points (only for outline font) */ - unsigned int verbose; - const char * text; -}; +static void +validateFontName(const char * const name) { +/*----------------------------------------------------------------------------- + Validate font name string. + + Abort with error message if it contains anything other than the printable + characters in the ASCII 7-bit range, or any character with a special meaning + in PostScript. +-----------------------------------------------------------------------------*/ + unsigned int idx; + + for (idx = 0; name[idx] != '\0'; ++idx) { + char const c = name[idx]; + + if (c < 32 || c > 125) + pm_error("Invalid character in font name"); + else + switch (c) { + case '[': case ']': case '(': case ')': + case '{': case '}': case '/': case '\\': + case '<': case '>': case '%': case ' ': + case '@': + pm_error("Invalid character in font name"); + } + } +} + + + +static void +asciiHexEncode(char * const inbuff, + char * const outbuff) { +/*----------------------------------------------------------------------------- + Convert the input text string to ASCII-Hex encoding. + + Examples: "ABC abc 123" -> <4142432061626320313233> + "FOO(BAR)FOO" -> <464f4f2842415229464f4f> +-----------------------------------------------------------------------------*/ + char const hexits[16] = "0123456789abcdef"; + + unsigned int idx; + + for (idx = 0; inbuff[idx] != '\0'; ++idx) { + unsigned int const item = (unsigned char) inbuff[idx]; + + outbuff[idx*2] = hexits[item >> 4]; + outbuff[idx*2+1] = hexits[item & 0xF]; + } + + outbuff[idx * 2] = '\0'; +} static void buildTextFromArgs(int const argc, const char ** const argv, - const char ** const textP) { - + const char ** const asciiHexTextP) { +/*---------------------------------------------------------------------------- + Build the array of text to be included in the Postscript program to + be rendered, from the arguments of this program. + + We encode it in ASCII-Hex notation as opposed to using the plain text from + the command line because 1) the command line might have Postscript control + characters in it; and 2) the command line might have text in 8-bit or + multibyte code, but a Postscript program is supposed to be entirely + printable ASCII characters. +-----------------------------------------------------------------------------*/ char * text; unsigned int totalTextSize; unsigned int i; @@ -65,6 +116,9 @@ buildTextFromArgs(int const argc, text = strdup(""); totalTextSize = 1; + if (argc-1 < 1) + pm_error("No text"); + for (i = 1; i < argc; ++i) { if (i > 1) { totalTextSize += 1; @@ -79,11 +133,46 @@ buildTextFromArgs(int const argc, pm_error("out of memory"); strcat(text, argv[i]); } - *textP = text; + + { + char * asciiHexText; + + MALLOCARRAY(asciiHexText, totalTextSize * 2); + + if (!asciiHexText) + pm_error("Unable to allocate memory for hex encoding of %u " + "characters of text", totalTextSize); + + asciiHexEncode(text, asciiHexText); + *asciiHexTextP = asciiHexText; + } + pm_strfree(text); } +struct CmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + unsigned int res; + float fontsize; + const char * font; + float stroke; + float ascent; + float descent; + float leftmargin; + float rightmargin; + float topmargin; + float bottommargin; + unsigned int pad; + unsigned int verbose; + unsigned int dump; + const char * text; +}; + + + static void parseCommandLine(int argc, const char ** argv, struct CmdlineInfo * const cmdlineP) { @@ -97,21 +186,55 @@ parseCommandLine(int argc, const char ** argv, optStruct3 opt; unsigned int option_def_index; + unsigned int cropSpec, ascentSpec, descentSpec; + unsigned int leftmarginSpec, rightmarginSpec; + unsigned int topmarginSpec, bottommarginSpec; MALLOCARRAY(option_def, 100); option_def_index = 0; /* incremented by OPTENTRY */ - OPTENT3(0, "resolution", OPT_UINT, &cmdlineP->res, NULL, 0); - OPTENT3(0, "font", OPT_STRING, &cmdlineP->font, NULL, 0); - OPTENT3(0, "fontsize", OPT_UINT, &cmdlineP->fontsize, NULL, 0); - OPTENT3(0, "stroke", OPT_FLOAT, &cmdlineP->stroke, NULL, 0); - OPTENT3(0, "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0); + OPTENT3(0, "resolution", OPT_UINT, + &cmdlineP->res, NULL, 0); + OPTENT3(0, "font", OPT_STRING, + &cmdlineP->font, NULL, 0); + OPTENT3(0, "fontsize", OPT_FLOAT, + &cmdlineP->fontsize, NULL, 0); + OPTENT3(0, "stroke", OPT_FLOAT, + &cmdlineP->stroke, NULL, 0); + OPTENT3(0, "ascent", OPT_FLOAT, + &cmdlineP->ascent, &ascentSpec, 0); + OPTENT3(0, "descent", OPT_FLOAT, + &cmdlineP->descent, &descentSpec, 0); + OPTENT3(0, "leftmargin", OPT_FLOAT, + &cmdlineP->leftmargin, &leftmarginSpec, 0); + OPTENT3(0, "rightmargin", OPT_FLOAT, + &cmdlineP->rightmargin, &rightmarginSpec, 0); + OPTENT3(0, "topmargin", OPT_FLOAT, + &cmdlineP->topmargin, &topmarginSpec, 0); + OPTENT3(0, "bottommargin", OPT_FLOAT, + &cmdlineP->bottommargin, &bottommarginSpec, 0); + OPTENT3(0, "crop", OPT_FLAG, + NULL, &cropSpec, 0); + OPTENT3(0, "pad", OPT_FLAG, + NULL, &cmdlineP->pad, 0); + OPTENT3(0, "verbose", OPT_FLAG, + NULL, &cmdlineP->verbose, 0); + OPTENT3(0, "dump-ps", OPT_FLAG, + NULL, &cmdlineP->dump, 0); /* Set the defaults */ cmdlineP->res = 150; cmdlineP->fontsize = 24; cmdlineP->font = "Times-Roman"; - cmdlineP->stroke = -1; + cmdlineP->stroke = -1; + cmdlineP->ascent = 0; + cmdlineP->descent = 0; + cmdlineP->rightmargin = 0; + cmdlineP->leftmargin = 0; + cmdlineP->topmargin = 0; + cmdlineP->bottommargin = 0; + cropSpec = FALSE; + cmdlineP->pad = FALSE; opt.opt_table = option_def; opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ @@ -119,6 +242,39 @@ parseCommandLine(int argc, const char ** argv, pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0); + validateFontName(cmdlineP->font); + + if (cmdlineP->fontsize <= 0) + pm_error("-fontsize must be positive"); + if (cmdlineP->ascent < 0) + pm_error("-ascent must not be negative"); + if (cmdlineP->descent < 0) + pm_error("-descent must not be negative"); + if (cmdlineP->leftmargin <0) + pm_error("-leftmargin must not be negative"); + if (cmdlineP->rightmargin <0) + pm_error("-rightmargin must not be negative"); + if (cmdlineP->topmargin <0) + pm_error("-topmargin must not be negative"); + if (cmdlineP->bottommargin <0) + pm_error("-bottommargin must not be negative"); + + if (cropSpec == TRUE) { + if (ascentSpec || descentSpec || + leftmarginSpec || rightmarginSpec || + topmarginSpec || bottommarginSpec || + cmdlineP->pad) + pm_error("-crop cannot be specified with -ascent, -descent, " + "-leftmargin, -rightmargin, " + "-topmargin, -bottommargin or -pad"); + } else { + if (!descentSpec && !bottommarginSpec && !cmdlineP->pad) + cmdlineP->descent = cmdlineP->fontsize * 1.5; + + if (!leftmarginSpec) + cmdlineP->leftmargin = cmdlineP->fontsize / 2; + } + buildTextFromArgs(argc, argv, &cmdlineP->text); free(option_def); @@ -126,82 +282,130 @@ parseCommandLine(int argc, const char ** argv, +static void +termCmdline(struct CmdlineInfo const cmdline) { + + pm_strfree(cmdline.text); +} + + + static const char * -constructPostscript(struct CmdlineInfo const cmdline) { +postscriptProgram(struct CmdlineInfo const cmdline) { /*----------------------------------------------------------------------------- - In Postscript, the bottom of the page is row zero. Negative values are - allowed but negative regions are clipped from the output image. We make - adjustments to ensure that nothing is lost. + In Postscript, the bottom of the page is row zero. Postscript allows + negative values but negative regions are clipped from the output image. + We make adjustments to ensure that nothing is lost. - Postscript fonts also allow negative values in the bounding box coordinates. - The bottom edge of "L" is row zero. The feet of "g" "p" "y" extend into - negative region. In a similar manner the left edge of the bounding box may - be negative. We add margins on the left and the bottom with "xinit" and - "yinit" to provide for such characters. + Postscript also allow fonts to have negative values in the bounding box + coordinates. The bottom edge of "L" is row zero: this row is called the + "baseline". The feet of "g" "p" "y" extend into negative region. In a + similar manner the left edge of the bounding box may be negative. We add + margins on the left and the bottom with "xorigin" and "yorigin" to + provide for such characters. The sequence "textstring false charpath flattenpath pathbbox" determines - the bounding box of the entire text when rendered. We make adjustments - with "xdelta", "ydelta" whenever "xinit", "yinit" is insufficient. - This may sound unlikely, but is likely to happen with right-to-left or - vertical writing, which we haven't tested. + the bounding box of the entire text when rendered. -----------------------------------------------------------------------------*/ /* C89 limits the size of a string constant, so we have to build the Postscript command in pieces. + + psVariable, psTemplate: Set variables. + psFixed1: Scale font. Calculate pad metrics. + psFixed2: Determine width, height, xorigin, yorigin. + psFixed3: Render. + psFixed4: Verbose mode: Report font name, metrics. + + We could add code to psFixed2 for handling right-to-left writing + (Hebrew, Arabic) and vertical writing (Chinese, Korean, Japanese). */ const char * const psTemplate = - "/%s findfont\n" - "/fontsize %u def\n" - "/stroke %f def\n" - "/textstring (%s) def\n"; + "/FindFont {/%s findfont} def\n" + "/fontsize %f def\n" + "/pensize %f def\n" + "/textstring <%s> def\n" + "/ascent %f def\n" + "/descent %f def\n" + "/leftmargin %f def\n" + "/rightmargin %f def\n" + "/topmargin %f def\n" + "/bottommargin %f def\n" + "/pad %s def\n" + "/verbose %s def\n"; const char * const psFixed1 = - "fontsize scalefont\n" - "setfont\n" - "/xinit fontsize 2 div def\n" - "/yinit fontsize 1.5 mul def\n" - "xinit yinit moveto\n" - "textstring false charpath flattenpath pathbbox\n" - "/top exch def\n" - "/right exch def\n" - "/bottom exch def\n" - "/left exch def\n" - "/xdelta left 0 lt {left neg} {0} ifelse def\n" - "/ydelta bottom 0 lt {bottom neg} {0} ifelse def\n" - "/width right xdelta add def\n" - "/height top ydelta add def\n"; - + "FindFont fontsize scalefont\n" + "pad { dup dup\n" + " /FontMatrix get 3 get /yscale exch def\n" + " /FontBBox get dup\n" + " 1 get yscale mul neg /padbottom exch def\n" + " 3 get yscale mul /padtop exch def}\n" + " {/padbottom 0 def /padtop 0 def}\n" + " ifelse\n" + "setfont\n"; + const char * const psFixed2 = + "0 0 moveto\n" + "textstring false charpath flattenpath pathbbox\n" + "/BBtop exch def\n" + "/BBright exch def\n" + "/BBbottom exch neg def\n" + "/BBleft exch neg def\n" + "/max { 2 copy lt { exch } if pop } bind def\n" + "/yorigin descent padbottom max BBbottom max bottommargin add def\n" + "/xorigin leftmargin BBleft max def\n" + "/width xorigin BBright add rightmargin add def\n" + "/height ascent BBtop max padtop max topmargin add yorigin add def\n"; + + const char * const psFixed3 = "<> setpagedevice\n" - "xinit yinit moveto\n" - "xdelta ydelta rmoveto\n" - "stroke 0 lt\n" + "xorigin yorigin moveto\n" + "pensize 0 lt\n" " {textstring show}\n" - " {stroke setlinewidth 0 setgray\n" + " {pensize setlinewidth 0 setgray\n" " textstring true charpath stroke}\n" " ifelse\n" - "textstring false charpath flattenpath pathbbox\n" - "showpage"; - + "showpage\n"; + + const char * const psFixed4 = + "verbose\n" + " {xorigin yorigin moveto\n" + " [(width height) width height] ==\n" + " [(ascent descent) height yorigin sub yorigin] ==\n" + " [(bounding box) \n" + " textstring false charpath flattenpath pathbbox] ==\n" + " [(Fontname) FindFont dup /FontName\n" + " known\n" + " {/FontName get}\n" + " {pop (anonymous)}\n" + " ifelse] ==}\n" + " if"; + const char * retval; const char * psVariable; - pm_asprintf(&psVariable, psTemplate, cmdline.font, cmdline.fontsize, - cmdline.stroke, cmdline.text); + pm_asprintf(&psVariable, psTemplate, cmdline.font, + cmdline.fontsize, cmdline.stroke, cmdline.text, + cmdline.ascent, cmdline.descent, + cmdline.leftmargin, cmdline.rightmargin, + cmdline.topmargin, cmdline.bottommargin, + cmdline.pad ? "true" : "false", + cmdline.verbose ? "true" : "false" ); - pm_asprintf(&retval, "%s%s%s", psVariable, psFixed1, psFixed2); + pm_asprintf(&retval, "%s%s%s%s%s", psVariable, + psFixed1, psFixed2, psFixed3, psFixed4); pm_strfree(psVariable); - + return retval; } static const char ** -gsArgList(const char * const psFname, - const char * const outputFilename, +gsArgList(const char * const outputFilename, struct CmdlineInfo const cmdline) { unsigned int const maxArgCt = 50; @@ -220,7 +424,6 @@ gsArgList(const char * const psFname, argCt = 0; /* initial value */ pm_asprintf(&retval[argCt++], "ghostscript"); - pm_asprintf(&retval[argCt++], "-r%d", cmdline.res); pm_asprintf(&retval[argCt++], "-sDEVICE=pbmraw"); pm_asprintf(&retval[argCt++], "-sOutputFile=%s", outputFilename); @@ -228,7 +431,7 @@ gsArgList(const char * const psFname, pm_asprintf(&retval[argCt++], "-dBATCH"); pm_asprintf(&retval[argCt++], "-dSAFER"); pm_asprintf(&retval[argCt++], "-dNOPAUSE"); - pm_asprintf(&retval[argCt++], "%s", psFname); + pm_asprintf(&retval[argCt++], "-"); retval[argCt++] = NULL; @@ -239,32 +442,6 @@ gsArgList(const char * const psFname, -static void -writeProgram(const char * const psFname, - struct CmdlineInfo const cmdline) { - - const char * ps; - FILE * psfile; - - psfile = fopen(psFname, "w"); - if (psfile == NULL) - pm_error("Can't open temp file '%s'. Errno=%d (%s)", - psFname, errno, strerror(errno)); - - ps = constructPostscript(cmdline); - - if (cmdline.verbose) - pm_message("Postscript program = '%s'", ps); - - if (fwrite(ps, 1, strlen(ps), psfile) != strlen(ps)) - pm_error("Can't write postscript to temp file"); - - fclose(psfile); - pm_strfree(ps); -} - - - static void reportGhostScript(const char * const executableNm, const char ** const argList) { @@ -295,22 +472,138 @@ freeArgList(const char ** const argList) { static void -executeProgram(const char * const psFname, +reportFontName(const char * const fontname) { + + pm_message("Font: '%s'", fontname); + +} + + + +static void +reportMetrics(float const width, + float const height, + float const ascent, + float const descent, + float const BBoxleft, + float const BBoxbottom, + float const BBoxright, + float const BBoxtop) { + + pm_message("-- Metrics in points. Bottom left is (0,0) --"); + pm_message("Width: %f", width); + pm_message("Height: %f", height); + pm_message("Ascent: %f", ascent); + pm_message("Descent: %f", descent); + pm_message("BoundingBox_Left: %f", BBoxleft); + pm_message("BoundingBox_Right: %f", BBoxright); + pm_message("BoundingBox_Top: %f", BBoxtop); + pm_message("BoundingBox_Bottom: %f", BBoxbottom); + +} + + + +static void +acceptGSoutput(int const pipetosuckFd, + void * const nullParams ) { +/*----------------------------------------------------------------------------- + Accept text written to stdout by the PostScript program. + + There are two kinds of output: + (1) Metrics and fontname reported, when verbose is on. + (2) Error messages from ghostscript. + + We read one line at a time. + + We cannot predict how long one line can be in case (2). In practice + the "execute stack" report gets long. We provide by setting lineBuffSize + to a large number. +-----------------------------------------------------------------------------*/ + unsigned int const lineBuffSize = 1024*32; + FILE * const inFileP = fdopen(pipetosuckFd, "r"); + + float width, height, ascent, descent; + float BBoxleft, BBoxbottom, BBoxright, BBoxtop; + char * lineBuff; /* malloc'd */ + char fontname [2048]; + bool fontnameReported, widthHeightReported; + bool ascentDescentReported, BBoxReported; + + assert(nullParams == NULL); + + fontnameReported = FALSE; /* Initial value */ + widthHeightReported = FALSE; /* Initial value */ + ascentDescentReported = FALSE; /* Initial value */ + BBoxReported = FALSE; /* Initial value */ + + MALLOCARRAY_NOFAIL(lineBuff, lineBuffSize); + + while (fgets(lineBuff, lineBuffSize, inFileP) != NULL) { + unsigned int rWidthHeight, rAscentDescent, rBBox, rFontname; + + rWidthHeight = sscanf(lineBuff, "[(width height) %f %f]", + &width, &height); + + rAscentDescent = sscanf(lineBuff, "[(ascent descent) %f %f]", + &ascent, &descent); + + rBBox = sscanf(lineBuff, "[(bounding box) %f %f %f %f]", + &BBoxleft, &BBoxbottom, &BBoxright, &BBoxtop); + + rFontname = sscanf(lineBuff, "[(Fontname) /%2047s", fontname); + + if (rFontname == 1) + fontnameReported = TRUE; + else if (rWidthHeight == 2) + widthHeightReported = TRUE; + else if (rAscentDescent == 2) + ascentDescentReported = TRUE; + else if (rBBox == 4) + BBoxReported = TRUE; + else + pm_message("[gs] %s", lineBuff); + } + + if (fontnameReported) { + fontname[strlen(fontname)-1] = 0; + reportFontName(fontname); + + if (widthHeightReported && ascentDescentReported && BBoxReported) + reportMetrics(width, height, ascent, descent, + BBoxleft, BBoxbottom, BBoxright, BBoxtop); + } + fclose(inFileP); + pm_strfree(lineBuff); +} + + + +static void +executeProgram(const char * const psProgram, const char * const outputFname, struct CmdlineInfo const cmdline) { const char * const executableNm = "gs"; - const char ** const argList = gsArgList(psFname, outputFname, cmdline); + const char ** const argList = gsArgList(outputFname, cmdline); + + struct bufferDesc feedBuffer; + int termStatus; + unsigned int bytesFed; + + bytesFed = 0; /* Initial value */ - int termStatus; + feedBuffer.buffer = (unsigned char *) psProgram; + feedBuffer.size = strlen(psProgram); + feedBuffer.bytesTransferredP = &bytesFed; if (cmdline.verbose) reportGhostScript(executableNm, argList); pm_system2_vp(executableNm, argList, - pm_feed_null, NULL, - pm_accept_null, NULL, + &pm_feed_from_memory, &feedBuffer, + cmdline.verbose ? &acceptGSoutput : &pm_accept_null, NULL, &termStatus); if (termStatus != 0) { @@ -326,10 +619,11 @@ executeProgram(const char * const psFname, static void -writePbmToStdout(const char * const fileName){ +writePbm(const char * const fileName, + FILE * const ofP) { /*---------------------------------------------------------------------------- - Write the PBM image that is in the file named 'fileName" to Standard - Output. I.e. pbmtopbm. + Write the PBM image that is in the file named 'fileName" to file *ofP. + I.e. pbmtopbm. It's not a byte-for-byte copy because PBM allows the same image to be represented many ways (all of which we can accept as our input), but we use @@ -347,51 +641,59 @@ writePbmToStdout(const char * const fileName){ pm_error("Abnormal output from gs program. " "width x height = %u x %u", cols, rows); - pbm_writepbminit(stdout, cols, rows, 0); + pbm_writepbminit(ofP, cols, rows, 0); bitrow = pbm_allocrow_packed(cols); for (row = 0; row < rows; ++row) { pbm_readpbmrow_packed(ifP, bitrow, cols, format); - pbm_writepbmrow_packed(stdout, bitrow, cols, 0); + pbm_writepbmrow_packed(ofP, bitrow, cols, 0); } pbm_freerow_packed(bitrow); + pm_close(ifP); } static void -createOutputFile(struct CmdlineInfo const cmdline) { +generatePbm(struct CmdlineInfo const cmdline, + FILE * const ofP) { + + const char * const psProgram = postscriptProgram(cmdline); - const char * psFname; const char * tempPbmFname; - FILE * psFileP; FILE * pbmFileP; - pm_make_tmpfile(&psFileP, &psFname); - assert(psFileP != NULL && psFname != NULL); - fclose(psFileP); - - writeProgram(psFname, cmdline); - pm_make_tmpfile(&pbmFileP, &tempPbmFname); assert(pbmFileP != NULL && tempPbmFname != NULL); fclose(pbmFileP); - executeProgram(psFname, tempPbmFname, cmdline); - - unlink(psFname); - pm_strfree(psFname); + executeProgram(psProgram, tempPbmFname, cmdline); /* Although Ghostscript created a legal PBM file, it uses a different implementation of the format from libnetpbm's canonical output format, - so instead of copying the content of 'tempPbmFname' to Standard output - byte for byte, we copy it as a PBM image. + so instead of copying the content of 'tempPbmFname' to *ofP byte for + byte, we copy it as a PBM image. */ - writePbmToStdout(tempPbmFname); + writePbm(tempPbmFname, ofP); unlink(tempPbmFname); pm_strfree(tempPbmFname); + pm_strfree(psProgram); +} + + + +static void +dumpPsProgram(struct CmdlineInfo const cmdline) { + + const char * psProgram; + + psProgram = postscriptProgram(cmdline); + + puts(psProgram); + + pm_strfree(psProgram); } @@ -405,7 +707,15 @@ main(int argc, const char *argv[]) { parseCommandLine(argc, argv, &cmdline); - createOutputFile(cmdline); + if (cmdline.dump) + dumpPsProgram(cmdline); + else + generatePbm(cmdline, stdout); + + termCmdline(cmdline); return 0; } + + + -- cgit 1.4.1