Index: doc/photo.n ================================================================== --- doc/photo.n +++ doc/photo.n @@ -550,23 +550,24 @@ an additional alpha filtering for the overall image, which allows the background on which the image is displayed to show through. This usually also has the effect of desaturating the image. The \fIalphaValue\fR must be between 0.0 and 1.0. .TP -\fBsvg \-dpi\fI dpiValue\fB \-scale\fI scaleValue\fB \-unit\fI unitValue\fR +\fBsvg \-dpi\fI dpiValue\fB \-scale\fI scaleValue\fB \-scaletowidth \fI width\fB \-scaletoheight\fI height\fR . \fIdpiValue\fR is used in conversion between given coordinates and screen resolution. The value must be greater than 0 and the default value is 96. \fIscaleValue\fR is used to scale the resulting image. The value must be greater than 0 and the default value is 1. -\fIunitValue\fR is the unit of all coordinates in the SVG data. -Available units are px (default, coordinates in pixel), pt (1/72 inch), -pc (12 pt), mm , cm and in. +\fIwidth\fR and \fIheight\fR are the width or height that the image +will be adjusted to. Only one parameter among \fB\-scale\fR, +\fB\-scaletowidth\fR and \fB\-scaletoheight\fR can be given at a time +and the aspect ratio of the original image is always preserved. The svg format supports a wide range of SVG features, but the full SVG standard is not available, for instance the 'text' feature -is missing and silently ignores when reading the SVG data. +is missing and silently ignored when reading the SVG data. The supported SVG features are: . .RS \fB elements:\fR g, path, rect, circle, ellipse, line, polyline, polygon, linearGradient, radialGradient, stop, defs, svg, style Index: generic/tkImgSVGnano.c ================================================================== --- generic/tkImgSVGnano.c +++ generic/tkImgSVGnano.c @@ -28,22 +28,26 @@ #include "nanosvgrast.h" /* Additional parameters to nsvgRasterize() */ typedef struct { - double x; - double y; double scale; + int scaleToHeight; + int scaleToWidth; } RastOpts; /* * Per interp cache of last NSVGimage which was matched to * be immediately rasterized after the match. This helps to * eliminate double parsing of the SVG file/string. */ typedef struct { + /* A poiner to remember if it is the same svn image (data) + * It is a Tcl_Channel if image created by -file option + * or a Tcl_Obj, if image is created with the -data option + */ ClientData dataOrChan; Tcl_DString formatString; NSVGimage *nsvgImage; RastOpts ropts; } NSVGcache; @@ -66,10 +70,12 @@ RastOpts *ropts); static int RasterizeSVG(Tcl_Interp *interp, Tk_PhotoHandle imageHandle, NSVGimage *nsvgImage, int destX, int destY, int width, int height, int srcX, int srcY, RastOpts *ropts); +static double GetScaleFromParameters(NSVGimage *nsvgImage, + RastOpts *ropts, int *widthPtr, int *heightPtr); static NSVGcache * GetCachePtr(Tcl_Interp *interp); static int CacheSVG(Tcl_Interp *interp, ClientData dataOrChan, Tcl_Obj *formatObj, NSVGimage *nsvgImage, RastOpts *ropts); static NSVGimage * GetCachedSVG(Tcl_Interp *interp, ClientData dataOrChan, @@ -132,20 +138,19 @@ } data = Tcl_GetStringFromObj(dataObj, &length); nsvgImage = ParseSVGWithOptions(interp, data, length, formatObj, &ropts); Tcl_DecrRefCount(dataObj); if (nsvgImage != NULL) { - *widthPtr = (int) ceil(nsvgImage->width * ropts.scale); - *heightPtr = (int) ceil(nsvgImage->height * ropts.scale); - if ((*widthPtr <= 0) || (*heightPtr <= 0)) { - nsvgDelete(nsvgImage); - return 0; - } - if (!CacheSVG(interp, chan, formatObj, nsvgImage, &ropts)) { - nsvgDelete(nsvgImage); - } - return 1; + GetScaleFromParameters(nsvgImage, &ropts, widthPtr, heightPtr); + if ((*widthPtr <= 0.0) || (*heightPtr <= 0.0)) { + nsvgDelete(nsvgImage); + return 0; + } + if (!CacheSVG(interp, chan, formatObj, nsvgImage, &ropts)) { + nsvgDelete(nsvgImage); + } + return 1; } return 0; } /* @@ -237,20 +242,19 @@ CleanCache(interp); data = Tcl_GetStringFromObj(dataObj, &length); nsvgImage = ParseSVGWithOptions(interp, data, length, formatObj, &ropts); if (nsvgImage != NULL) { - *widthPtr = (int) ceil(nsvgImage->width * ropts.scale); - *heightPtr = (int) ceil(nsvgImage->height * ropts.scale); - if ((*widthPtr <= 0) || (*heightPtr <= 0)) { - nsvgDelete(nsvgImage); - return 0; - } - if (!CacheSVG(interp, dataObj, formatObj, nsvgImage, &ropts)) { - nsvgDelete(nsvgImage); - } - return 1; + GetScaleFromParameters(nsvgImage, &ropts, widthPtr, heightPtr); + if ((*widthPtr <= 0.0) || (*heightPtr <= 0.0)) { + nsvgDelete(nsvgImage); + return 0; + } + if (!CacheSVG(interp, dataObj, formatObj, nsvgImage, &ropts)) { + nsvgDelete(nsvgImage); + } + return 1; } return 0; } /* @@ -322,18 +326,18 @@ RastOpts *ropts) { Tcl_Obj **objv = NULL; int objc = 0; double dpi = 96.0; - char unit[3], *p; char *inputCopy = NULL; NSVGimage *nsvgImage; + int parameterScaleSeen = 0; static const char *const fmtOptions[] = { - "-dpi", "-scale", "-unit", NULL + "-dpi", "-scale", "-scaletoheight", "-scaletowidth", NULL }; enum fmtOptions { - OPT_DPI, OPT_SCALE, OPT_UNIT + OPT_DPI, OPT_SCALE, OPT_SCALE_TO_HEIGHT, OPT_SCALE_TO_WIDTH }; /* * The parser destroys the original input string, * therefore first duplicate. @@ -350,13 +354,13 @@ /* * Process elements of format specification as a list. */ - strcpy(unit, "px"); - ropts->x = ropts->y = 0.0; ropts->scale = 1.0; + ropts->scaleToHeight = 0; + ropts->scaleToWidth = 0; if ((formatObj != NULL) && Tcl_ListObjGetElements(interp, formatObj, &objc, &objv) != TCL_OK) { goto error; } for (; objc > 0 ; objc--, objv++) { @@ -383,10 +387,30 @@ } objc--; objv++; + /* + * check that only one scale option is given + */ + switch ((enum fmtOptions) optIndex) { + case OPT_SCALE: + case OPT_SCALE_TO_HEIGHT: + case OPT_SCALE_TO_WIDTH: + if ( parameterScaleSeen ) { + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "only one of -scale, -scaletoheight, -scaletowidth may be given", -1)); + Tcl_SetErrorCode(interp, "TK", "IMAGE", "SVG", "BAD_SCALE", + NULL); + goto error; + } + parameterScaleSeen = 1; + } + + /* + * Decode parameters + */ switch ((enum fmtOptions) optIndex) { case OPT_DPI: if (Tcl_GetDoubleFromObj(interp, objv[0], &dpi) == TCL_ERROR) { goto error; } @@ -409,21 +433,40 @@ Tcl_SetErrorCode(interp, "TK", "IMAGE", "SVG", "BAD_SCALE", NULL); goto error; } break; - case OPT_UNIT: - p = Tcl_GetString(objv[0]); - if ((p != NULL) && (p[0])) { - strncpy(unit, p, 3); - unit[2] = '\0'; + case OPT_SCALE_TO_HEIGHT: + if (Tcl_GetIntFromObj(interp, objv[0], &ropts->scaleToHeight) == + TCL_ERROR) { + goto error; + } + if (ropts->scaleToHeight <= 0) { + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "-scaletoheight value must be positive", -1)); + Tcl_SetErrorCode(interp, "TK", "IMAGE", "SVG", "BAD_SCALE", + NULL); + goto error; + } + break; + case OPT_SCALE_TO_WIDTH: + if (Tcl_GetIntFromObj(interp, objv[0], &ropts->scaleToWidth) == + TCL_ERROR) { + goto error; + } + if (ropts->scaleToWidth <= 0) { + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "-scaletowidth value must be positive", -1)); + Tcl_SetErrorCode(interp, "TK", "IMAGE", "SVG", "BAD_SCALE", + NULL); + goto error; } break; } } - nsvgImage = nsvgParse(inputCopy, unit, (float) dpi); + nsvgImage = nsvgParse(inputCopy, "px", (float) dpi); if (nsvgImage == NULL) { Tcl_SetObjResult(interp, Tcl_NewStringObj("cannot parse SVG image", -1)); Tcl_SetErrorCode(interp, "TK", "IMAGE", "SVG", "PARSE_ERROR", NULL); goto error; } @@ -468,13 +511,14 @@ { int w, h, c; NSVGrasterizer *rast; unsigned char *imgData; Tk_PhotoImageBlock svgblock; + double scale; - w = (int) ceil(nsvgImage->width * ropts->scale); - h = (int) ceil(nsvgImage->height * ropts->scale); + scale = GetScaleFromParameters(nsvgImage, ropts, &w, &h); + rast = nsvgCreateRasterizer(); if (rast == NULL) { Tcl_SetObjResult(interp, Tcl_NewStringObj("cannot initialize rasterizer", -1)); Tcl_SetErrorCode(interp, "TK", "IMAGE", "SVG", "RASTERIZER_ERROR", NULL); @@ -484,12 +528,12 @@ if (imgData == NULL) { Tcl_SetObjResult(interp, Tcl_NewStringObj("cannot alloc image buffer", -1)); Tcl_SetErrorCode(interp, "TK", "IMAGE", "SVG", "OUT_OF_MEMORY", NULL); goto cleanRAST; } - nsvgRasterize(rast, nsvgImage, (float) ropts->x, (float) ropts->y, - (float) ropts->scale, imgData, w, h, w * 4); + nsvgRasterize(rast, nsvgImage, 0, 0, + (float) scale, imgData, w, h, w * 4); /* transfer the data to a photo block */ svgblock.pixelPtr = imgData; svgblock.width = w; svgblock.height = h; svgblock.pitch = w * 4; @@ -518,10 +562,70 @@ cleanAST: nsvgDelete(nsvgImage); return TCL_ERROR; } + +/* + *---------------------------------------------------------------------- + * + * GetScaleFromParameters -- + * + * Get the scale value from the already parsed parameters -scale, + * -scaletoheight and -scaletowidth. + * + * The image width and height is also returned. + * + * Results: + * The evaluated or configured scale value, or 0.0 on failure + * + * Side effects: + * heightPtr and widthPtr are set to height and width of the image. + * + *---------------------------------------------------------------------- + */ + +static double +GetScaleFromParameters( + NSVGimage *nsvgImage, + RastOpts *ropts, + int *widthPtr, + int *heightPtr) +{ + double scale; + int width, height; + + if ((nsvgImage->width == 0.0) || (nsvgImage->height == 0.0)) { + width = height = 0; + scale = 1.0; + } else if (ropts->scaleToHeight > 0) { + /* + * Fixed height + */ + height = ropts->scaleToHeight; + scale = height / nsvgImage->height; + width = (int) ceil(nsvgImage->width * scale); + } else if (ropts->scaleToWidth > 0) { + /* + * Fixed width + */ + width = ropts->scaleToWidth; + scale = width / nsvgImage->width; + height = (int) ceil(nsvgImage->height * scale); + } else { + /* + * Scale factor + */ + scale = ropts->scale; + width = (int) ceil(nsvgImage->width * scale); + height = (int) ceil(nsvgImage->height * scale); + } + + *heightPtr = height; + *widthPtr = width; + return scale; +} /* *---------------------------------------------------------------------- * * GetCachePtr -- Index: tests/imgSVGnano.test ================================================================== --- tests/imgSVGnano.test +++ tests/imgSVGnano.test @@ -25,10 +25,16 @@ } set data(bad) { } + tcltest::makeFile $data(plus) plus.svg + set data(plusFilePath) [file join [tcltest::configure -tmpdir] plus.svg] + + tcltest::makeFile $data(bad) bad.svg + set data(badFilePath) [file join [tcltest::configure -tmpdir] bad.svg] + test imgSVGnano-1.1 {reading simple image} -setup { catch {rename foo ""} } -body { image create photo foo -data $data(plus) list [image width foo] [image height foo] @@ -59,21 +65,46 @@ test imgSVGnano-1.4 {image options} -setup { catch {rename foo ""} } -body { image create photo foo -data $data(plus) foo configure -format {svg -scale 2} - foo configure -format {svg -unit pt} - foo configure -format {svg -unit mm} - foo configure -format {svg -unit cm} - foo configure -format {svg -unit in} - foo configure -format {svg -unit px} foo configure -format {svg -dpi 600} list [image width foo] [image height foo] } -cleanup { + rename foo "" +} -result {100 100} +test imgSVGnano-1.5 {reading simple image from file} -setup { + catch {rename foo ""} +} -body { + image create photo foo -file $data(plusFilePath) + list [image width foo] [image height foo] +} -cleanup { rename foo "" } -result {100 100} +test imgSVGnano-1.6 {simple image with options} -setup { + catch {rename foo ""} +} -body { + image create photo foo -file $data(plusFilePath) -format {svg -dpi 100 -scale 3} + list [image width foo] [image height foo] +} -cleanup { + rename foo "" +} -result {300 300} +test imgSVGnano-1.7 {Very small scale gives 1x1 image} -body { + image create photo foo -format "svg -scale 0.000001"\ + -data $data(plus) + list [image width foo] [image height foo] +} -cleanup { + rename foo "" +} -result {1 1} +test imgSVGnano-1.8 {Very small scale gives 1x1 image from file} -body { + image create photo foo -format "svg -scale 0.000001"\ + -file $data(plusFilePath) + list [image width foo] [image height foo] +} -cleanup { + rename foo "" +} -result {1 1} test imgSVGnano-2.1 {reading a bad image} -body { image create photo foo -format svg -data $data(bad) } -returnCodes error -result {couldn't recognize image data} test imgSVGnano-2.2 {using bad option} -body { @@ -82,11 +113,101 @@ test imgSVGnano-2.3 {using bad option} -body { image create photo foo -data $data(plus) foo configure -format {svg 1.0} } -cleanup { rename foo "" -} -returnCodes error -result {bad option "1.0": must be -dpi, -scale, or -unit} +} -returnCodes error -result {bad option "1.0": must be -dpi, -scale, -scaletoheight, or -scaletowidth} +test imgSVGnano-2.4 {reading a bad image from file} -body { + image create photo foo -format svg -file $data(badFilePath) +} -returnCodes error -match glob\ + -result {couldn't recognize data in image file "*/win/bad.svg"} + +# -scaletoheight and -scaletowidth options +test imgSVGnano-3.1 {multiple scale options} -body { + image create photo foo -format "svg -scale 1 -scaletowidth 20"\ + -data $data(bad) +} -returnCodes error -result {only one of -scale, -scaletoheight, -scaletowidth may be given} + +test imgSVGnano-3.2 {no number parameter to -scaletowidth} -body { + image create photo foo -format "svg -scaletowidth invalid"\ + -data $data(plus) +} -returnCodes error -result {expected integer but got "invalid"} + +test imgSVGnano-3.3 {no number parameter to -scaletoheight} -body { + image create photo foo -format "svg -scaletoheight invalid"\ + -data $data(plus) +} -returnCodes error -result {expected integer but got "invalid"} + +test imgSVGnano-3.4 {zero parameter to -scaletowidth} -body { + image create photo foo -format "svg -scaletowidth 0"\ + -data $data(plus) +} -returnCodes error -result {-scaletowidth value must be positive} + +test imgSVGnano-3.5 {zero parameter to -scaletoheight} -body { + image create photo foo -format "svg -scaletoheight 0"\ + -data $data(plus) +} -returnCodes error -result {-scaletoheight value must be positive} + +test imgSVGnano-3.6 {no number parameter to -scaletoheight} -body { + image create photo foo -format "svg -scaletoheight invalid"\ + -data $data(plus) +} -returnCodes error -result {expected integer but got "invalid"} + +test imgSVGnano-3.7 {Option -scaletowidth} -body { + image create photo foo -format "svg -scaletowidth 20"\ + -data $data(plus) + image width foo +} -cleanup { + rename foo "" +} -result {20} + +test imgSVGnano-3.8 {Option -scaletoheight} -body { + image create photo foo -format "svg -scaletoheight 20"\ + -data $data(plus) + image height foo +} -cleanup { + rename foo "" +} -result {20} + +test imgSVGnano-3.10 {change from -scaletoheight to -scale} -body { + set res {} + image create photo foo -format "svg -scaletoheight 16"\ + -data $data(plus) + lappend res [image width foo] [image height foo] + foo configure -format "svg -scale 2" + lappend res [image width foo] [image height foo] +} -cleanup { + rename foo "" + unset res +} -result {16 16 200 200} + +# svg file access +test imgSVGnano-4.1 {reread file on configure -scale} -setup { + catch {rename foo ""} + set res {} +} -body { + image create photo foo -file $data(plusFilePath) + lappend res [image width foo] [image height foo] + foo configure -format "svg -scale 2" + lappend res [image width foo] [image height foo] +} -cleanup { + rename foo "" + unset res +} -result {100 100 200 200} + + +test imgSVGnano-4.2 {error on file not accessible on reread due to configure} -setup { + catch {rename foo ""} + tcltest::makeFile $data(plus) tmpplus.svg + image create photo foo -file [file join [tcltest::configure -tmpdir] tmpplus.svg] + tcltest::removeFile tmpplus.svg +} -body { + foo configure -format "svg -scale 2" +} -cleanup { + rename foo "" + tcltest::removeFile tmpplus.svg +} -returnCodes error -match glob -result {couldn't open "*/tmpplus.svg": no such file or directory} };# end of namespace svgnano namespace delete svgnano imageFinish