From b691edeec1fcfa816ad5a632447fc2f5469a6588 Mon Sep 17 00:00:00 2001 From: Christian Walther Date: Wed, 20 Jul 2011 13:48:08 +0200 Subject: [PATCH 1/2] Higher-quality cursors with a shadow to make them visible on light backgrounds. Same look as in 760606d except for the linking book, which got a slight curvature on the pages (inspired by Deledrius' one, 99bda8c). The SVG is hand-tweaked to work around some differences in rendering between rsvg and Inkscape. I hope editing it in Inkscape again won't break the tweaks, check the diff closely if you do! Effects (blurred shadows) appear to be clipped to the SVG viewport by rsvg, which is why drawing the whole SVG shifted for the book cursors no longer works and we shift individual layers inside the SVG instead. --- .../Apps/plClient/external/Cursor_Base.svg | 1053 ++++++++++++----- .../Apps/plClient/external/render_svg.py | 58 +- 2 files changed, 802 insertions(+), 309 deletions(-) diff --git a/Sources/Plasma/Apps/plClient/external/Cursor_Base.svg b/Sources/Plasma/Apps/plClient/external/Cursor_Base.svg index 737abfec..88a2deae 100644 --- a/Sources/Plasma/Apps/plClient/external/Cursor_Base.svg +++ b/Sources/Plasma/Apps/plClient/external/Cursor_Base.svg @@ -7,19 +7,83 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="634.85712" - height="634.85712" + width="48" + height="48" id="svg2" version="1.1" - inkscape:version="0.48.0 r9654" - inkscape:export-xdpi="11.1" - inkscape:export-ydpi="11.1" - sodipodi:docname="Cursor_Base.svg" - style="display:inline"> + inkscape:version="0.48.1 r9760" + sodipodi:docname="making of cursors.svg" + inkscape:export-xdpi="360" + inkscape:export-ydpi="360"> + id="defs4"> + + + + + + + + + + + + + - - - - - - - - - - - + inkscape:window-width="1541" + inkscape:window-height="889" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:object-nodes="true" + inkscape:snap-grids="true"> + + + @@ -104,280 +151,714 @@ - + id="layer1" + style="display:none" + sodipodi:insensitive="true"> + - + id="layer5" + inkscape:label="poised orig out" + style="display:none" + sodipodi:insensitive="true"> + - + id="layer2" + inkscape:label="4wayclosed orig in" + style="display:none" + sodipodi:insensitive="true"> + - + inkscape:groupmode="layer" + id="layer6" + inkscape:label="4wayclosed orig out" + style="opacity:0.5;display:none" + sodipodi:insensitive="true"> + + id="layer18" + inkscape:label="shapes 1" + style="display:none"> + + + sodipodi:nodetypes="ccccccccccccccccccsscccccccccssccccccc" /> + id="layer19" + inkscape:label="shapes 2" + style="display:none"> + id="path6749" + d="m 27.7,24 c 0,2.378573 -1.321428,3.7 -3.7,3.7 -2.378573,0 -3.7,-1.321427 -3.7,-3.7 0,-2.378572 1.321427,-3.7 3.7,-3.7 2.378572,0 3.700001,1.321428 3.7,3.7 z" + style="fill-rule:evenodd;stroke:none;display:inline" /> + + + + + id="layer20" + inkscape:label="shapes 3" + style="display:none"> + transform="translate(0,-192)" + id="path6758" + d="m 14.5,205 -1.5,1.5 5.15625,5.15625 C 17.409919,212.77794 17,214.22611 17,216 c 0,1.77389 0.409919,3.22206 1.15625,4.34375 L 13,225.5 l 1.5,1.5 5.15625,-5.15625 C 20.777944,222.59008 22.226111,223 24,223 c 1.773889,0 3.222056,-0.40992 4.34375,-1.15625 L 33.5,227 35,225.5 29.84375,220.34375 C 30.590081,219.22206 31,217.77389 31,216 c 0,-1.77389 -0.409919,-3.22206 -1.15625,-4.34375 L 35,206.5 33.5,205 28.34375,210.15625 C 27.222055,209.40992 25.773889,209 24,209 c -1.773889,0 -3.222056,0.40992 -4.34375,1.15625 L 14.5,205 z m 9.5,6 c 1.128621,0 2.082846,0.23139 2.84375,0.65625 l -1,1 C 25.330626,212.42921 24.710234,212.3125 24,212.3125 c -0.710234,0 -1.330626,0.11671 -1.84375,0.34375 l -1,-1 C 21.917154,211.23139 22.871379,211 24,211 z m -4.34375,2.15625 1,1 c -0.227037,0.51312 -0.34375,1.13352 -0.34375,1.84375 0,0.71023 0.116713,1.33063 0.34375,1.84375 l -1,1 C 19.23139,218.08285 19,217.12862 19,216 c 0,-1.12862 0.23139,-2.08285 0.65625,-2.84375 z m 8.6875,0 C 28.76861,213.91715 29,214.87138 29,216 c 0,1.12862 -0.23139,2.08285 -0.65625,2.84375 l -1,-1 c 0.227038,-0.51312 0.34375,-1.13352 0.34375,-1.84375 0,-0.71023 -0.116712,-1.33063 -0.34375,-1.84375 l 1,-1 z m -6.1875,6.1875 c 0.513124,0.22704 1.133516,0.34375 1.84375,0.34375 0.710234,0 1.330626,-0.11671 1.84375,-0.34375 l 1,1 C 26.082847,220.76861 25.128621,221 24,221 c -1.128621,0 -2.082846,-0.23139 -2.84375,-0.65625 l 1,-1 z" + style="stroke:none;display:inline" /> - + id="circleOuterShadow" + inkscape:label="shadow outer circle" + style="display:inline"> + + + + + + + + - - - - + inkscape:label="shadow lower arrow translucent" + id="arrowTranslucentLowerShadow" + inkscape:groupmode="layer"> + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/Plasma/Apps/plClient/external/render_svg.py b/Sources/Plasma/Apps/plClient/external/render_svg.py index b8d103c5..e19e3585 100644 --- a/Sources/Plasma/Apps/plClient/external/render_svg.py +++ b/Sources/Plasma/Apps/plClient/external/render_svg.py @@ -20,30 +20,32 @@ cursorList = { "cursor_up": ["circleOuter"], "cursor_poised": ["circleOuter", "circleInnerOpen"], "cursor_clicked": ["circleOuter", "circleInnerClosed"], - "cursor_disabled": ["circleOuter", "cross"], + "cursor_disabled": ["cross"], - "cursor_open": ["circleOuter", "arrowGreyUpper", "arrowGreyLower"], - "cursor_grab": ["circleOuter", "circleInnerClosed", "arrowGreyUpper", "arrowGreyLower"], - "cursor_updown_open": ["circleOuter", "circleInnerClosed", "arrowGreyUpper", "arrowGreyLower"], - "cursor_updown_closed": ["circleOuter", "circleInnerClosed", "arrowWhiteUpper", "arrowWhiteLower"], + "cursor_open": ["circleOuter", "arrowTranslucentUpper", "arrowTranslucentLower"], + "cursor_grab": ["circleOuter", "circleInnerOpen", "arrowTranslucentUpper", "arrowTranslucentLower"], + "cursor_updown_open": ["circleOuter", "circleInnerOpen", "arrowTranslucentUpper", "arrowTranslucentLower"], + "cursor_updown_closed": ["circleOuter", "circleInnerClosed", "arrowOpaqueUpper", "arrowOpaqueLower"], - "cursor_leftright_open": ["circleOuter", "circleInnerClosed", "arrowGreyRight", "arrowGreyLeft"], - "cursor_leftright_closed": ["circleOuter", "circleInnerClosed", "arrowWhiteRight", "arrowWhiteLeft"], + "cursor_leftright_open": ["circleOuter", "circleInnerOpen", "arrowTranslucentLeft", "arrowTranslucentRight"], + "cursor_leftright_closed": ["circleOuter", "circleInnerClosed", "arrowOpaqueLeft", "arrowOpaqueRight"], - "cursor_4way_open": ["circleOuter", "circleInnerClosed", "arrowGreyUpper", "arrowGreyRight", "arrowGreyLower", "arrowGreyLeft"], - "cursor_4way_closed": ["circleOuter", "circleInnerClosed", "arrowWhiteUpper", "arrowWhiteRight", "arrowWhiteLower", "arrowWhiteLeft"], + "cursor_4way_open": ["circleOuter", "circleInnerOpen", "arrowTranslucentUpper", "arrowTranslucentRight", "arrowTranslucentLower", "arrowTranslucentLeft"], + "cursor_4way_closed": ["circleOuter", "circleInnerClosed", "arrowOpaqueUpper", "arrowOpaqueRight", "arrowOpaqueLower", "arrowOpaqueLeft"], - "cursor_upward": ["circleOuter", "arrowWhiteUpper"], - "cursor_right": ["circleOuter", "arrowWhiteRight"], - "cursor_down": ["circleOuter", "arrowWhiteLower"], - "cursor_left": ["circleOuter", "arrowWhiteLeft"], + "cursor_upward": ["circleOuter", "arrowOpaqueUpper"], + "cursor_right": ["circleOuter", "arrowOpaqueRight"], + "cursor_down": ["circleOuter", "arrowOpaqueLower"], + "cursor_left": ["circleOuter", "arrowOpaqueLeft"], "cursor_book": ["circleOuter", "book"], "cursor_book_poised": ["circleOuter", "circleInnerOpen", "book"], "cursor_book_clicked": ["circleOuter", "circleInnerClosed", "book"], } cursorOffsetList = { - "book": [8, 8] + "cursor_book": [7, 7], + "cursor_book_poised": [7, 7], + "cursor_book_clicked": [7, 7] } textList = { @@ -62,6 +64,15 @@ def enable_only_layers(layerlist, layers): layers[layer].setAttribute("style","") else: layers[layer].setAttribute("style","display:none") + # sanity check + for layer in layerlist: + if layer not in layers: + print("warning: unknown layer", layer) + +def shift_all_layers(layers, shiftx, shifty): + # note: this assumes that all layers start out with no transform of their own + for layer in layers: + layers[layer].setAttribute("transform", "translate(%g,%g)" % (shiftx, shifty)) def get_layers_from_svg(svgData): inkscapeNS = "http://www.inkscape.org/namespaces/inkscape" @@ -75,13 +86,13 @@ def get_layers_from_svg(svgData): return layers def render_cursors(inpath, outpath): - resSize = {"width":32, "height":32} + scalefactor = 1 with open(os.path.join(inpath,"Cursor_Base.svg"), "r") as svgFile: cursorSVG = parse(svgFile) layers = get_layers_from_svg(cursorSVG) - ratioW = resSize["width"] / float(cursorSVG.documentElement.getAttribute("width")) - ratioH = resSize["height"] / float(cursorSVG.documentElement.getAttribute("height")) - surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, resSize["width"], resSize["height"]) + svgwidth = float(cursorSVG.documentElement.getAttribute("width")) + svgheight = float(cursorSVG.documentElement.getAttribute("height")) + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(math.ceil(scalefactor*svgwidth)), int(math.ceil(scalefactor*svgheight))) for cursor in cursorList: ctx = cairo.Context(surface) @@ -90,13 +101,14 @@ def render_cursors(inpath, outpath): ctx.paint() ctx.restore() - enable_only_layers(cursorList[cursor], layers) + enabledlayers = cursorList[cursor] + enabledlayers = enabledlayers + [l + "Shadow" for l in enabledlayers] + enable_only_layers(enabledlayers, layers) + + shift_all_layers(layers, *cursorOffsetList.get(cursor, [0, 0])) - for layerName in cursorOffsetList: - if layerName in cursorList[cursor]: - ctx.translate(*cursorOffsetList[layerName]) svg = rsvg.Handle(data=cursorSVG.toxml()) - ctx.scale(ratioW, ratioH) + ctx.scale(scalefactor, scalefactor) svg.render_cairo(ctx) surface.write_to_png(os.path.join(outpath, cursor + ".png")) From e5db4e166e626de708ed7842252211a1af3fe2fb Mon Sep 17 00:00:00 2001 From: Christian Walther Date: Wed, 20 Jul 2011 16:15:24 +0200 Subject: [PATCH 2/2] Improve detail rendition (in particular of thin lines) on the cursors by rendering them at higher resolution and then running them through a gamma-aware down-scaling algorithm. --- .../Apps/plClient/external/render_svg.py | 7 +- .../Apps/plClient/external/scalergba.py | 180 ++++++++++++++++++ 2 files changed, 185 insertions(+), 2 deletions(-) create mode 100755 Sources/Plasma/Apps/plClient/external/scalergba.py diff --git a/Sources/Plasma/Apps/plClient/external/render_svg.py b/Sources/Plasma/Apps/plClient/external/render_svg.py index e19e3585..7fb2fdd0 100644 --- a/Sources/Plasma/Apps/plClient/external/render_svg.py +++ b/Sources/Plasma/Apps/plClient/external/render_svg.py @@ -8,6 +8,7 @@ import os import math from xml.dom.minidom import parse from optparse import OptionParser +import scalergba try: import rsvg @@ -86,7 +87,7 @@ def get_layers_from_svg(svgData): return layers def render_cursors(inpath, outpath): - scalefactor = 1 + scalefactor = 4 with open(os.path.join(inpath,"Cursor_Base.svg"), "r") as svgFile: cursorSVG = parse(svgFile) layers = get_layers_from_svg(cursorSVG) @@ -111,7 +112,9 @@ def render_cursors(inpath, outpath): ctx.scale(scalefactor, scalefactor) svg.render_cairo(ctx) - surface.write_to_png(os.path.join(outpath, cursor + ".png")) + outfile = os.path.join(outpath, cursor + ".png") + surface.write_to_png(outfile) + scalergba.scale(outfile, outfile, scalefactor) def render_loading_books(inpath, outpath): resSize = {"width":256, "height":256} diff --git a/Sources/Plasma/Apps/plClient/external/scalergba.py b/Sources/Plasma/Apps/plClient/external/scalergba.py new file mode 100755 index 00000000..65593e98 --- /dev/null +++ b/Sources/Plasma/Apps/plClient/external/scalergba.py @@ -0,0 +1,180 @@ +#!/opt/local/bin/python2.7 + +# Christian Walther 2011-07-20 +# Public Domain + +# scalergba.py +# +# Scale image down by (integer) and save as PNG file . +# +# - Taking into account that adding (averaging) colors must be done in a linear +# color space, not with the power-law-encoded values stored in the files. +# (I know of no image processing application that does this right.) +# Gamma is hardcoded to 2.2. +# - Assuming that alpha compositing will be done directly with the raw +# power-law-encoded values rather than in the linear color space that would +# be correct, which is the way almost all software will do it, in particular +# OpenGL/Direct3D. (Photoshop has an option to do it right, maybe other +# high-end image processing software too.) + + +from __future__ import division +import sys +import math +try: + import Image +except ImportError: + print("Scaling requires the Python Imaging Library.") + raise + + +gamma = 2.2 + + +def add(a, b): + for i, y in enumerate(b): + a[i] += y + +def sub(a, b): + for i, y in enumerate(b): + a[i] -= y + +def subsc(a, b): + for i in range(3): + a[i] -= b + +def mul(a, b): + for i, y in enumerate(b): + a[i] *= y + +def mulsc(a, b): + for i in range(3): + a[i] *= b + +def pixel2linear(p): + l = [math.pow(p[i]/255.0, gamma) for i in range(3)] + if len(p) == 4: + l.append(p[3]/255.0) + else: + l.append(1.0) + return l + +def pixel2nonlinear(p): + return [p[i]/255.0 for i in range(3)], p[3]/255.0 if len(p) > 3 else 1.0 + +def clamp(x): + return 255 if x > 255 else 0 if x < 0 else x + +def linear2pixel(l): + p = [clamp(int(math.floor(math.pow(l[i], 1.0/gamma)*255 + 0.5))) for i in range(3)] + if len(l) == 4: + p.append(clamp(int(math.floor(l[3]*255 + 0.5)))) + return p + +def nonlinear2pixel(l): + return [clamp(int(math.floor(c*255 + 0.5))) for c in l] + + +def scale(infilename, outfilename, factor): + inimg = Image.open(infilename) + inpix = inimg.load() + + outw = inimg.size[0] // factor + outh = inimg.size[1] // factor + outimg = Image.new("RGBA", (outw, outh), None) + outpix = outimg.load() + + for oy in range(outh): + for ox in range(outw): + # scale down in linear color space to get a tentative color to compute the fixed points from + sum = [0.0, 0.0, 0.0, 0.0] + for j in range(factor): + for i in range(factor): + l = pixel2linear(inpix[ox*factor+i, oy*factor+j]) + mulsc(l, l[3]) + add(sum, l) + if sum[3] != 0: + mulsc(sum, 1.0/sum[3]) + sum[3] /= factor*factor + + # determine the two fixed points (background colors on which we will achieve the correct result) per component + # I used to use constant black and white for that, but later realized that that results in a large error (result too light) on midtones and I can do better by distributing the error more evenly. The dependency of the fixed points on the foreground color is empirical magic that has been experimentally determined to produce visually pleasing results. + fix1 = [0.04*sum[i] for i in range(3)] + fix2 = [0.4 + 0.6*sum[i] for i in range(3)] + fix1n = [math.pow(l, 1.0/gamma) for l in fix1] + fix2n = [math.pow(l, 1.0/gamma) for l in fix2] + + # composite against the fixed points in nonlinear color space as that's what the image expects (only matters in areas of medium alpha), then scale down in linear color space again + f1c = [0.0, 0.0, 0.0] + for j in range(factor): + for i in range(factor): + c, a = pixel2nonlinear(inpix[ox*factor+i, oy*factor+j]) + mulsc(c, a) + f = fix1n[:] + mulsc(f, 1.0 - a) + add(c, f) + add(f1c, [math.pow(p, gamma) for p in c]) + mulsc(f1c, 1.0/(factor*factor)) + + f2c = [0.0, 0.0, 0.0] + for j in range(factor): + for i in range(factor): + c, a = pixel2nonlinear(inpix[ox*factor+i, oy*factor+j]) + mulsc(c, a) + f = fix2n[:] + mulsc(f, 1.0 - a) + add(c, f) + add(f2c, [math.pow(p, gamma) for p in c]) + mulsc(f2c, 1.0/(factor*factor)) + + # go back to gamma-encoded color space, assuming that alpha blending will be done in that + f1cn = [math.pow(l, 1.0/gamma) for l in f1c] + f2cn = [math.pow(l, 1.0/gamma) for l in f2c] + + # compute color and alpha + # This gives us three alphas, in general different, but we can only output one - the best thing to do with them I can think of is to average them together and leave the color components alone, this ensures that the alpha deviation does not affect the case where background color equals foreground color. + a = [1.0 - (f2cn[i] - f1cn[i])/(fix2n[i] - fix1n[i]) for i in range(3)] + c = [(f1cn[i] - (1.0-a[i])*fix1n[i])/a[i] if math.floor(a[i]*255 + 0.5) > 0 else 0.0 for i in range(3)] + outpix[ox, oy] = tuple(nonlinear2pixel(c + [(a[0] + a[1] + a[2])/3])) + + # collect pixels that ended up with alpha 0 + transparent = [0]*outh*outw + for oy in range(outh): + for ox in range(outw): + if outpix[ox, oy][3] == 0: + transparent[oy*outw + ox] = 1 + + # expand neighboring color values from nonzero-alpha pixels into the zero-alpha region twice, so that bilinear interpolation cannot bleed the arbitrary background color (black here) from zero-alpha into nonzero-alpha territory + for i in range(2): + transp = transparent[:] + for oy in range(outh): + for ox in range(outw): + if transp[oy*outw + ox]: + count = 0 + sum = [0, 0, 0] + if ox > 0: + if not transp[oy*outw + ox-1]: + add(sum, outpix[ox-1, oy][0:3]) + count += 1 + if ox < outw-1: + if not transp[oy*outw + ox+1]: + add(sum, outpix[ox+1, oy][0:3]) + count += 1 + if oy > 0: + if not transp[(oy-1)*outw + ox]: + add(sum, outpix[ox, oy-1][0:3]) + count += 1 + if oy < outh-1: + if not transp[(oy+1)*outw + ox]: + add(sum, outpix[ox, oy+1][0:3]) + count += 1 + if count > 0: + mulsc(sum, 1.0/count) + outpix[ox, oy] = tuple(clamp(int(math.floor(c + 0.5))) for c in sum) + (0,) + transparent[oy*outw + ox] = 0 + + outimg.save(outfilename, "PNG") + + +if __name__ == "__main__": + scale(sys.argv[1], sys.argv[3], int(sys.argv[2]))