1 // blackrain at realizedsound dot net and charles picasso - thelych at gmail dot com
5 classvar <formats, <compositingOperations, <interpolations, <allPlotWindows;
6 var dataptr, <width, <height, <background, <>name, <url, <>autoMode=true, <filters, prCache, prFinalizer;
25 compositingOperations = [
32 'destinationOver', // 6
34 'destinationOut', // 8
35 'destinationATop', // 9
43 *new { arg multiple, height=nil;
45 if(multiple.isKindOf(Point), {
46 ^super.new.init(multiple.x, multiple.y);
49 if(multiple.isKindOf(Number), {
50 ^super.new.init(multiple, height ? multiple);
53 if(multiple.isKindOf(String), {
55 if ( multiple.beginsWith("http://").not
56 and:{ multiple.beginsWith("file://").not }
57 and:{ multiple.beginsWith("ftp://").not }) {
63 ^this.openURL( multiple );
67 *color { arg ... args;
68 var newone, color, filter;
69 newone = SCImage.new(*args);
71 if(color.isKindOf(Color).not, {color = Color.black});
72 filter = SCImageFilter(\CIConstantColorGenerator);
74 newone.applyFilters(filter);
78 path = path.standardizePath;
79 if ( File.exists(path) ) {
80 ^super.new.initFromURL("file://" +/+ path.replace(" ", "%20"));
82 format("SCImage: % not found.", path).error;
87 ^super.new.initFromURL(url.replace(" ", "%20"));
89 *fromName { arg imageNamed;
90 // return a system Image or a previously allocated instance of an Image named ...
93 *fromImage { arg scimage;
94 if(scimage.isKindOf(this), {
97 "SCImage: invalid instance to copy from.".error;
100 *fromWindow {arg window, rect;
101 if(window.isKindOf(SCWindow).not, {
102 "SCImage: fromWindowRect window argument is not instance of SCWindow.".error;
104 rect = rect ? window.view.bounds.moveTo(0,0);
105 if(rect.isKindOf(Rect).not, {
106 "SCImage: fromWindowRect rect argument is not instance of Rect.".error;
108 ^this.prFromWindowRect(window, rect);
112 * Converts a Color instance into
113 * a pixel datatype suitable for SCImage.
114 * This is a 32bit packed Integer in
117 *colorToPixel { arg col;
119 (col.red * 255 ).asInteger,
120 (col.green * 255).asInteger,
121 (col.blue * 255 ).asInteger,
122 (col.alpha * 255 ).asInteger);
125 *prFromWindowRect {arg window, rect;
126 _SCImage_fromWindowRect
127 ^this.primitiveFailed;
130 init { arg width, height;
131 this.prInit(width, height);
135 initFromFile { arg path;
136 url = "file://" ++ path.standardizePath;
137 this.prInitFromFile(path);
142 initFromURL { arg newURL;
144 this.prInitFromURL(url);
153 new = this.class.new(width, height);
154 new.name_(this.name);
156 new.autoMode_(this.autoMode);
159 this.drawAtPoint(0@0,nil,1,1.0);
168 ^Rect(0,0,width?0,height?0)
170 write { arg path, format; // ok
172 path = path.standardizePath;
173 format = format ?? { path.basename.splitext.at(1).asSymbol };
174 format = formats.indexOf(format) ? 0;
175 ^this.prWriteToFile(path, format);
177 format("SCImage:write invalid instance.").error;
180 if(new_url.isKindOf(String), {
181 url = new_url.standardizePath.replace(" ", "%20");
185 var filter, transform;
186 if(aRect.isKindOf(Rect).not, {
187 "SCImage: bad argument for cropping image !".warn;
190 aRect.top = this.height - aRect.top - aRect.height;
191 if(autoMode, {this.accelerated_(true)});
192 filter = SCImageFilter(\CICrop);
193 filter.rectangle_(aRect);
194 transform = SCImageFilter(\CIAffineTransform);
195 transform.transform_([aRect.left.neg, aRect.top.neg]);
196 this.applyFilters([filter, transform]);
200 if(autoMode, {this.accelerated_(true)});
201 this.applyFilters(SCImageFilter(\CIColorInvert));
206 index = this.prGetInterpolation;
207 ^this.class.interpolations[index];
209 interpolation_ {|mode|
211 if(mode.isKindOf(Integer), {
212 this.prSetInterpolation(mode.clip(0, this.class.interpolations.size - 1));
214 index = this.class.interpolations.indexOf(mode.asSymbol);
216 this.prSetInterpolation(index);
218 "SCImage: bad interpolation value as argument !".error;
223 // pixel manipulation
224 setPixel {|rgbaInteger, x, y|
225 ^this.prSetPixel(rgbaInteger, x, y);
228 ^this.prGetPixel(x,y);
230 setColor {|color, x, y|
231 ^this.prSetColor(color, x, y);
234 ^this.prGetColor(x, y);
238 if(autoMode, {this.accelerated_(false)});
239 if(width <= 0 or:{height <= 0}, {^nil});
240 pixelArray = Int32Array.newClear(width*height);
241 this.prLoadPixels(pixelArray);
244 loadPixels {arg array, region=nil, start=0;
245 if(autoMode, {this.accelerated_(false)});
246 if(array.isKindOf(Int32Array).not, {
247 "SCImage: array should be an Int32Array".warn;
250 this.prLoadPixels(array, region, start);
254 this.setPixels(array);
256 setPixels {|array, region=nil, start=0|
257 if(autoMode, {this.accelerated_(false)});
259 this.prUpdatePixels(array, start);
261 this.prUpdatePixelsInRect(array, region, start);
264 // this method should not be called directly
265 // unless you know exactly what you want to do
266 // use autoMode_(true) to let the class choose the best rep internally
267 // or autoMode_(false) with accelerated_ method to manually manage your representations
268 // a bad management can lead to : worse performance when manipulating images
269 // bad syncing issue between representations !
270 accelerated_ {|aBool| // if yes ensure to use CoreImages
271 _SCImage_setAccelerated
272 ^this.primitiveFailed;
276 _SCImage_isAccelerated
277 ^this.primitiveFailed;
282 if(filter.isKindOf(SCImageFilter), {
283 filters = filters.add(filter);
286 removeFilter {|filter|
287 // remove last occurence of a filter in the array
288 filters.reverseDo {|object, i|
289 if(object == filter, {
290 filters.removeAt(filter.size - i);
298 createCache { // only for Filters
299 prCache = this.filteredWith(filters);
302 flatten the receiver with all filters added to it
306 this.accelerated_(true); // force acceleration just in case
307 this.applyFilters(filters);
308 filters = []; // clear all filters
309 if(autoMode, {this.accelerated_(false)}); // ensure bitmap representation
312 applyFilters {|filters, crop=0, region|
313 if(filters.isNil, {^this});
314 // passing nil to crop says use the result extent of the filter
315 // may return huge extent so check and crop image if needed !
316 if(crop == 0, {crop = this.bounds});
317 if(crop.isKindOf(Rect).not and:{crop.isNil.not}, {
318 "SCImage: crop should be a Rect, 0, or Nil".warn;
321 if(filters.isKindOf(SCImageFilter), {
324 if(autoMode, {this.accelerated_(true)});
325 ^this.prApplyFilters(filters, true, region, crop);
328 // returns a copy of the receiver filtered with filter
329 filteredWith {|filters, crop=0|
330 if(filters.isNil, {^this});
331 if(filters.isKindOf(SCImageFilter), {
334 if(crop == 0, {crop = this.bounds});
335 if(crop.isKindOf(Rect).not and:{crop.isNil.not}, {
336 "SCImage: crop should be a Rect, 0, or Nil".warn;
339 if(autoMode, {this.accelerated_(true)});
340 ^this.prApplyFilters(filters, false, nil, crop);
344 if(prCache.notNil, {prCache.free; prCache=nil});
347 // still experimental
348 applyKernel {|kernel, crop|
349 if(kernel.isKindOf(SCImageKernel).not, {
350 "SCImage: aKernel should be a SCImageKernel !".warn;
353 if(kernel.isValid.not, {
354 "SCImage: kernel does not seem to be valid !".warn;
357 ^this.prApplyKernel(kernel, crop, true);
362 if(autoMode, {this.accelerated_(false)});
366 if(autoMode, {this.accelerated_(false)});
369 drawAtPoint { arg point, fromRect, operation='sourceOver', fraction=1.0;
370 if(filters.size == 0, {
371 operation = compositingOperations.indexOf(operation) ? 2;
372 this.prDrawAtPoint(point, fromRect, operation, fraction);
375 this.accelerated_(true); // we have to force acceleration
377 prCache.drawAtPoint(point, fromRect, operation, fraction);
380 drawInRect { arg rect, fromRect, operation='sourceOver', fraction=1.0;
381 if(filters.size == 0, {
382 operation = compositingOperations.indexOf(operation) ? 2;
383 this.prDrawInRect(rect, fromRect, operation, fraction);
386 this.accelerated_(true); // we have to force acceleration
388 prCache.drawInRect(rect, fromRect, operation, fraction);
391 tileInRect { arg rect, fromRect, operation='sourceOver', fraction=1.0; if(filters.size == 0, {
392 this.prTileInRect(rect, fromRect ? this.bounds,
393 compositingOperations.indexOf(operation) ? 2, fraction);
396 this.accelerated_(true); // we have to force acceleration
398 prCache.tileInRect(rect, fromRect, operation, fraction);
403 aFunction.value(this);
407 // string drawing support
408 drawStringAtPoint { arg string, point, font, color;
411 strbounds = string.bounds(font);
412 SCPen.use { // for now
413 SCPen.translate(0, this.height);
415 point.y = this.height - point.y - strbounds.height;
416 string.drawAtPoint(point, font, color);
421 // simple convenient function
422 plot { arg name, bounds, freeOnClose=false, background=nil, showInfo=true;
423 var uview, window, nw, nh, ratio = width / height, info="";
424 nw = width.min(600).max(200);
426 window = SCWindow.new(name ? "plot", bounds ? Rect(400,400,nw,nh)/*, textured: false*/);
427 allPlotWindows = allPlotWindows.add(window);
429 if(background.notNil, {
430 window.view.background_(background);
432 window.acceptsMouseOver = true;
434 uview = SCUserView(window, window.view.bounds)
436 .focusColor_(Color.clear);
439 allPlotWindows.remove(window);
447 this.drawInRect(window.view.bounds, this.bounds, 2, 1.0);
453 Color.black.alpha_(0.4).setFill;
454 Color.white.setStroke;
455 SCPen.fillRect(Rect(5.5,5.5,100,20));
456 SCPen.strokeRect(Rect(5.5,5.5,100,20));
457 info.drawAtPoint(10@10, Font.default, Color.white);
461 uview.mouseOverAction_({|v, x, y|
464 info = format("X: %, Y: %",
465 ((x / window.view.bounds.width) * this.width).floor.min(width-1),
466 ((y / window.view.bounds.height) * this.height).floor.min(height-1) );
468 info = "invalid image";
470 window.view.refreshInRect(Rect(5.5,5.5,100,20));
475 *closeAllPlotWindows {
476 allPlotWindows.do(_.close);
479 storeOn { arg stream;
480 stream << this.class.name << ".openURL(" << url.asCompileString <<")"
483 archiveAsCompileString { ^true }
485 // cocoa bridge additions
492 _SCImage_scalesWhenResized
493 ^this.primitiveFailed
495 scalesWhenResized_ { arg flag; // to test
496 _SCImage_setScalesWhenResized
497 ^this.primitiveFailed
501 this.setSize(w, height);
504 this.setSize(width, h);
506 setSize { arg width, height; // to test
508 ^this.primitiveFailed
512 // pixel manipulation private
513 prSetPixel {|rgbaInteger, x, y|
515 ^this.primitiveFailed
519 ^this.primitiveFailed
521 prSetColor {|color, x, y|
523 ^this.primitiveFailed
527 ^this.primitiveFailed
529 prApplyFilters {|filterArray, inPlaceBolean, region, maxSize|
530 _SCImageFilter_ApplyMultiple
531 ^this.primitiveFailed;
533 prApplyKernel {|kernel, crop, inPlace |
534 _SCImageFilter_ApplyKernel
535 ^this.primitiveFailed;
538 _SCImage_interpolation
539 ^this.primitiveFailed;
542 prSetInterpolation {|index|
543 _SCImage_setInterpolation
544 ^this.primitiveFailed;
547 prDrawAtPoint { arg point, fromRect, operation, fraction;
549 ^this.primitiveFailed
552 prDrawInRect { arg rect, fromRect, operation, fraction;
554 ^this.primitiveFailed
557 prSetName { arg newName; // currently does nothing
559 ^this.primitiveFailed
562 prSetBackground { arg color; // currently does nothing
563 _SCImage_setBackgroundColor
564 ^this.primitiveFailed
567 prSync { // should never be used -- be provided in case
569 ^this.primitiveFailed
573 prInit { arg width, height;
575 ^this.primitiveFailed
578 prInitFromFile { arg path;
580 ^this.primitiveFailed
583 prInitFromURL { arg url;
585 ^this.primitiveFailed
589 ^this.primitiveFailed
593 ^this.primitiveFailed
597 ^this.primitiveFailed
601 ^this.primitiveFailed
603 prLoadPixels {arg array, region, startIndex;
605 ^this.primitiveFailed
607 prUpdatePixels {arg array, startIndex;
608 _SCImage_updatePixels
609 ^this.primitiveFailed
611 prUpdatePixelsInRect {arg array, rect, startIndex;
612 _SCImage_updatePixelsInRect
613 ^this.primitiveFailed
615 prTileInRect { arg rect, fromRect, operation, fraction;
617 ^this.primitiveFailed
619 prWriteToFile { arg path, format;
621 ^this.primitiveFailed
627 var <name, <attributes, <values, <>enable=true;
634 var categoryNames = [
635 \CICategoryDistortionEffect,
636 \CICategoryGeometryAdjustment,
637 \CICategoryCompositeOperation,
638 \CICategoryHalftoneEffect,
639 \CICategoryColorAdjustment,
640 \CICategoryColorEffect,
641 \CICategoryTransition,
642 \CICategoryTileEffect,
643 \CICategoryGenerator,
649 \CICategoryStillImage,
650 \CICategoryInterlaced,
651 \CICategoryNonSquarePixels,
652 \CICategoryHighDynamicRange,
653 \CICategoryDistortionEffect,
657 categories = IdentityDictionary.new;
659 Platform.when(#[\_SCImageFilter_NamesInCategory], {
661 categoryNames.do {|key|
662 categories.add(key -> this.getFilterNames(key));
665 //"SCImage filter categories done !".postln;
670 *new {|filterName, args|
671 ^super.newCopyArgs(filterName.asSymbol).initSCImageFilter(args);
678 initSCImageFilter {|arguments|
679 attributes = this.class.getFilterAttributes(name);
681 this.attributes_(arguments);
684 *translateObject {|object|
685 if(object.isKindOf(Boolean), {
686 ^ if(object == true, {1}, {0});
689 if(object.isKindOf(Rect), {
690 ^ [object.left, object.top, object.width, object.height];
693 ^ object; // no translation
696 doesNotUnderstand { arg selector ... args;
698 if(selector.isSetter && attributes.includesKey(selector.asGetter), {
699 key = selector.asGetter;
700 args[0] = this.class.translateObject(args[0]);
701 if(args[0].isKindOf(attributes.at(key)),
703 index = values.indexOf(key);
705 values[index+1] = args[0];
707 values = values.add(key);
708 values = values.add(args[0]);
711 ("SCImageFilter: invalid value for filter attribute: "+key+"(a"+args[0].class.asString++") -> argument should be of class:"+attributes.at(key)).error;
714 ^attributes.at(selector);
718 *getFilterNames {|category|
719 ^this.prGetFilterNames(category, Array.newClear(64));
722 *getFilterAttributes {|filterName|
725 array = this.prGetFilterAttributes(filterName);
726 result = IdentityDictionary.new;
728 (array.size >> 1).do {|i|
734 \NSNumber, {class = \Number},
735 \CIVector, {class = \Array},
736 \CIImage, {class = \SCImage},
737 \CIColor, {class = \Color},
738 \NSAffineTransform, {class = \Array},
739 \NSPoint, {class = \Point}
742 result.put(key.asSymbol, class.asClass);
748 attributeRange { |attributeName|
749 var result = [nil, nil, nil];
750 this.prAttributeRange(attributeName.asSymbol, result);
756 var method, value, max;
757 if(array.isNil, {^this});
758 max = array.size.asInteger >> 1;
760 method = array[i << 1];
761 value = array[(i << 1) + 1];
762 this.perform(method.asSymbol.asSetter, value);
767 this.attributes_(values);
770 prAttributeRange { |attr|
771 _SCImageFilter_GetAttributeMinMax
772 ^this.primitiveFailed
775 *prGetFilterNames {|cat, array|
776 _SCImageFilter_NamesInCategory
777 ^this.primitiveFailed
780 // direct primitive call - should not be used !!!
781 *prFilterSet{ |filterName, filterArguments|
783 ^this.primitiveFailed
786 *prGetFilterAttributes {|filterName|
787 _SCImageFilter_Attributes
788 ^this.primitiveFailed
793 var <>shader, <>values, <>bounds, <>enabled, dataptr, finalizer;
794 *new {|shader, values, bounds|
795 if(values.isKindOf(Array).not and:{values.isNil.not}, {
796 "SCImageKernel values should be an Array !".warn;
799 if(shader.isKindOf(String).not and:{shader.isNil.not}, {
800 "SCImageKernel shader should be a String !".warn;
803 if(bounds.isKindOf(Rect).not and:{bounds.isNil.not}, {
804 "SCImageKernel shader should be a Rect !".warn;
808 ^super.newCopyArgs(shader, values, bounds, true);
812 ^(shader.notNil.or(shader.size > 0).and(values.notNil.or(values.size <= 0)));
816 _SCImageKernel_Compile
817 ^this.primitiveFailed
822 // integer additions to retrieve 8-bit pixel component from RGBA packed data
825 *fromRGBA {|r, g=0, b=0, a=255|
827 ((r.asInteger & 16r000000FF) << 24) | ((g.asInteger & 16r000000FF) << 16) | ((b.asInteger & 16r000000FF) << 8) | (a.asInteger & 16r000000FF)
833 (color.red * 255).asInteger,
834 (color.green * 255).asInteger,
835 (color.blue * 255).asInteger,
836 (color.alpha * 255).asInteger
841 ^Color.new255(this.red, this.green, this.blue, this.alpha);
845 ^[this.red, this.green, this.blue, this.alpha];
849 ^((this >> 24) & 16r000000FF);
852 ^((this >> 16) & 16r000000FF);
855 ^((this >> 8) & 16r000000FF);
858 ^(this & 16r000000FF);
864 ^Integer.fromRGBA(this.at(0)?0,this.at(1)?0, this.at(2)?0, this.at(3)?0);
871 (this.red * 255).asInteger,
872 (this.green * 255).asInteger,
873 (this.blue * 255).asInteger,
874 (this.alpha * 255).asInteger
880 SCView:backgroundImage
883 1 - fixed to left, fixed to top
884 2 - horizontally tile, fixed to top
885 3 - fixed to right, fixed to top
886 4 - fixed to left, vertically tile
887 5 - horizontally tile, vertically tile
888 6 - fixed to right, vertically tile
889 7 - fixed to left, fixed to bottom
890 8 - horizontally tile, fixed to bottom
891 9 - fixed to right, fixed to bottom
893 11 - center, center (scale)
894 12 - center , fixed to top
895 13 - center , fixed to bottom
896 14 - fixed to left, center
897 15 - fixed to right, center
898 16 - center, center (no scale)
902 backgroundImage_ { arg image, tileMode=1, alpha=1.0, fromRect;
903 this.setProperty(\backgroundImage, [image, tileMode, alpha, fromRect])
906 _SCView_RefreshInRect
907 ^this.primitiveFailed
914 ^this.class.new(left - h, top - v, width + h + h, height + v + v);