diff --git a/ui (gen4)/image.js b/ui (gen4)/image.js
index 20d3db87..b29ee218 100755
--- a/ui (gen4)/image.js	
+++ b/ui (gen4)/image.js	
@@ -171,6 +171,8 @@ function _loadImagePreviewURL(image, url){
 // NOTE: care must be taken to reset ALL attributes an image can have,
 // 		a common bug if this is not done correctly, is that some settings
 // 		may leak to newly loaded images...
+//
+// XXX this calls .correctImageProportionsForRotation(..)
 var updateImage =
 module.updateImage =
 function updateImage(image, gid, size, sync){
@@ -317,67 +319,12 @@ function updateImages(list, size, cmp){
 }
 
 
-// Compensate for viewer proportioned and rotated images.
-//
-// This will set the margins so as to make the rotated image offset the
-// same space as it is occupying visually...
-//
-// NOTE: this is not needed for square image blocks.
-// NOTE: if an image block is square, this will remove the margins.
 var correctImageProportionsForRotation =
 module.correctImageProportionsForRotation =
 function correctImageProportionsForRotation(images, container){
 	container = container == null ? $('.viewer') : container
 
-	var W = container.innerWidth()
-	var H = container.innerHeight()
-
-	var viewer_p = W > H ? 'landscape' : 'portrait'
-
-	return $(images).each(function(i, e){
-		var image = $(this)
-		// orientation...
-		var o = image.attr('orientation')
-		o = o == null ? 0 : o
-		var w = image.outerWidth()
-		var h = image.outerHeight()
-
-		// non-square image...
-		if(w != h){
-
-			var image_p = w > h ? 'landscape' : 'portrait'
-
-			// when the image is turned 90deg/270deg and its 
-			// proportions are the same as the screen...
-			if((o == 90 || o == 270) && image_p == viewer_p){
-				image.css({
-					width: h,
-					height: w,
-				})
-				image.css({
-					'margin-top': -((w - h)/2),
-					'margin-bottom': -((w - h)/2),
-					'margin-left': (w - h)/2,
-					'margin-right': (w - h)/2,
-				})
-
-			} else if((o == 0 || o == 180) && image_p != viewer_p){
-				image.css({
-					width: h,
-					height: w,
-				})
-				image.css({
-					'margin': '',
-				})
-			}
-
-		// square image...
-		} else {
-			image.css({
-				'margin': '',
-			})
-		}
-	})
+	// XXX
 }
 
 
diff --git a/ui (gen4)/images.js b/ui (gen4)/images.js
index 41838b06..ac10d8f5 100755
--- a/ui (gen4)/images.js	
+++ b/ui (gen4)/images.js	
@@ -13,6 +13,49 @@ console.log('>>> images')
 
 /*********************************************************************/
 
+// Calculate relative rotation angle...
+//
+// Calculate rotation angle relative to from:
+// 	calcRelativeRotation(from, 'cw')
+// 	calcRelativeRotation(from, 'ccw')
+// 		-> 0 | 90 | 180 | 270
+//
+// Validate an angle:
+// 	calcRelativeRotation(angle)
+// 	calcRelativeRotation(from, angle)
+// 		-> 0 | 90 | 180 | 270
+// 		-> null
+//
+//
+module.calcRelativeRotation = function(from, to){
+	if(to == null){
+		to = from
+		from = 0
+	}
+	to = to == 'cw' ? 1 
+		: to == 'ccw' ? -1
+		: [0, 90, 180, 270].indexOf(to*1) >= 0 ? to*1
+		: [-90, -180, -270].indexOf(to*1) >= 0 ? 360+(to*1)
+		: null
+
+	// relative rotation...
+	if(to == 1 || to == -1){
+		var res = from
+		res = res == null ? 0 : res*1
+		res += 90*to
+		res = res < 0 ? 270 
+			: res > 270 ? 0
+			: res
+
+	// explicit direction...
+	} else {
+		var res = to
+	}
+
+	return res
+}
+
+
 // cmp functions...
 // XXX is this the right way to seporate these???
 
@@ -174,6 +217,11 @@ module.ImagesPrototype = {
 
 	// Image data helpers...
 
+	// XXX see: ribbons.js for details...
+	getBestPreview: function(){
+		// XXX
+	},
+
 	// Get image filename...
 	getImageFileName: function(gid, do_unescape){
 		do_unescape = do_unescape == null ? true : do_unescape
@@ -234,6 +282,80 @@ module.ImagesPrototype = {
 		// XXX see ../ui/sort.js
 	},
 
+	// Actions...
+
+	// Rotate image...
+	//
+	// Rotate image clockwise:
+	//	.rotateImage(target, 'cw')
+	//		-> images
+	//
+	// Rotate image counterclockwise:
+	//	.rotateImage(target, 'ccw')
+	//		-> images
+	//
+	// Set explicit image rotation angle:
+	//	.rotateImage(target, 0|90|180|270)
+	//	.rotateImage(target, -90|-180|-270)
+	//		-> images
+	//
+	// NOTE: target can be a gid or a list of gids...
+	rotateImage: function(gids, direction){
+		gids = gids.constructor.name != 'Array' ? [gids] : gids
+		// validate direction...
+		if(module.calcRelativeRotation(direction) == null){
+			return this
+		}
+
+		var that = this
+		gids.forEach(function(key){
+			var img = that[key]
+			var o = direction == 'cw' || direction == 'ccw' 
+				? module.calcRelativeRotation(img.orientation, direction) 
+				: direction*1
+			if(o == 0){
+				delete img.orientation
+			} else {
+				img.orientation = o
+			}
+			// account for proportions...
+			//that.correctImageProportionsForRotation(img)
+			// XXX this is a bit of an overkill but it will update the 
+			// 		preview if needed...
+			//that.updateImage(img)
+		})
+		return this
+	},
+
+	// Flip image...
+	//
+	//	.flipImage(target, 'horizontal')
+	//	.flipImage(target, 'vertical')
+	//		-> images
+	//
+	flipImage: function(gids, direction){
+		gids = gids.constructor.name != 'Array' ? [gids] : gids
+		var that = this
+		gids.forEach(function(key){
+			var img = that[key]
+			var state = img.flipped
+			state = state == null ? [] : state
+			// toggle the specific state...
+			var i = state.indexOf(direction)
+			if(i >= 0){
+				state.splice(i, 1)
+			} else {
+				state.push(direction)
+			}
+			if(state.length == 0){
+				delete img.flipped
+			} else {
+				img.flipped = state
+			}
+		})
+		return this
+	},
+
 
 	// serialization...
 	loadJSON: function(data){
diff --git a/ui (gen4)/ribbons.js b/ui (gen4)/ribbons.js
index e7497c43..178c1349 100755
--- a/ui (gen4)/ribbons.js	
+++ b/ui (gen4)/ribbons.js	
@@ -18,6 +18,7 @@ require('ext-lib/jquery')
 
 var data = require('data')
 var image = require('image')
+var images = require('images')
 
 
 
@@ -83,6 +84,7 @@ module.RibbonsClassPrototype = {
 					.replace(/^"(.*)"$/g, '$1'))
 	},
 	// XXX NOTE: quots removal might render this incompatible with older data formats...
+	// XXX should this do .updateImage(..) ???
 	createImage: function(gid){
 		gid = gid != null ? gid+'' : gid
 		return $('
')
@@ -100,6 +102,8 @@ module.RibbonsPrototype = {
 	//
 	//	.viewer (jQuery object)
 	//
+	//	.images (Images object)
+	//
 	// XXX to update images we need to know about images...
 	
 	// Constructors...
@@ -333,6 +337,7 @@ module.RibbonsPrototype = {
 	//
 	// XXX interaction animation...
 	// XXX mode is ugly...
+	// XXX should this be strongly or weakly coupled to images.updateImage(..)???
 	placeImage: function(target, to, mode){
 		mode = mode == null ? 'before' : mode
 		var img = this.getImage(target)
@@ -384,6 +389,8 @@ module.RibbonsPrototype = {
 	//
 	// NOTE: gids and ribbon must be .getImage(..) and .getRibbon(..) 
 	// 		compatible...
+	//
+	// XXX should this be strongly or weakly coupled to images.updateImage(..)???
 	updateRibbon: function(gids, ribbon){
 		// get/create the ribbon...
 		var r = this.getRibbon(ribbon)
@@ -621,6 +628,196 @@ module.RibbonsPrototype = {
 	},
 
 
+	getImageMarks: function(){
+		// XXX
+	},
+	// XXX this needs:
+	// 		IMAGE_UPDATERS -- make it a callback/event (node/jquery)...
+	updateImageIndicators: function(gid, image){
+		gid = gid == null ? this.getElemGID() : gid
+		image = image == null ? this.getImage() : $(image)
+
+		IMAGE_UPDATERS.forEach(function(update){
+			update(gid, image)
+		})
+
+		return image
+	},
+	_loadImagePreviewURL: function(image, url){
+		// pre-cache and load image...
+		// NOTE: this will make images load without a blackout...
+		var img = new Image()
+		img.onload = function(){
+			image.css({
+					'background-image': 'url("'+ url +'")',
+				})
+		}
+		img.src = url
+		return img
+	},
+
+	// Update image{s}...
+	//
+	load_img_sync: false,
+	//
+	// XXX this needs: 
+	// 		STUB_IMAGE_DATA
+	// 		this.images
+	// XXX
+	updateImage: function(image, gid, size, sync){
+		image = image == null ? this.getImage() : $(image)
+		sync = sync == null ? this.load_img_sync : sync
+
+		var that = this
+		return image.each(function(){
+			var image = $(this)
+			var old_gid = that.getElemGID(image)
+
+			// same image -- update...
+			if(old_gid == gid || gid == null){
+				gid = old_gid
+
+			// reuse for different image -- reconstruct...
+			} else {
+				// remove old marks...
+				if(typeof(old_gid) == typeof('str')){
+					that.getImageMarks(old_gid).remove()
+				}
+				// reset gid...
+				image
+					.attr('gid', JSON.stringify(gid)
+						// this removes the extra quots...
+						.replace(/^"(.*)"$/g, '$1'))
+					.css({
+						// clear the old preview...
+						'background-image': '',
+					})
+			}
+			size = size == null ? that.getVisibleImageSize('max') : size
+
+			// get the image data...
+			var img_data = that.images[gid]
+			if(img_data == null){
+				img_data = STUB_IMAGE_DATA
+			}
+
+			/* XXX does not seem to be needing this...
+			// set the current class...
+			if(gid == DATA.current){
+				image.addClass('current')
+			} else {
+				image.removeClass('current')
+			}
+			*/
+
+			// preview...
+			var p_url = that.images.getBestPreview(gid, size).url
+
+			// update the preview if it's a new image or...
+			if(old_gid != gid 
+					// the new preview (purl) is different to current...
+					|| image.css('background-image').indexOf(encodeURI(p_url)) < 0){
+				// sync load...
+				if(sync){
+					that._loadImagePreviewURL(image, p_url)
+
+				// async load...
+				} else {
+					// NOTE: storing the url in .data() makes the image load the 
+					// 		last requested preview and in a case when we manage to 
+					// 		call updateImage(...) on the same element multiple times 
+					// 		before the previews get loaded...
+					// 		...setting the data().loading is sync while loading an 
+					// 		image is not, and if several loads are done in sequence
+					// 		there is no guarantee that they will happen in the same
+					// 		order as requested...
+					image.data().loading = p_url
+					setTimeout(function(){ 
+						that._loadImagePreviewURL(image, image.data().loading)
+					}, 0)
+				}
+			}
+
+			// main attrs...
+			image
+				.attr({
+					//order: DATA.order.indexOf(gid),
+					orientation: img_data.orientation == null ? 0 : img_data.orientation,
+				})
+
+			// flip...
+			setImageFlipState(image, img_data.flipped == null ? [] : img_data.flipped)
+
+			// NOTE: this only has effect on non-square image blocks...
+			that.correctImageProportionsForRotation(image)
+
+			// marks and other indicators...
+			that.updateImageIndicators(gid, image)
+		})
+	},
+
+	// XXX reread...
+	// Compensate for viewer proportioned and rotated images.
+	//
+	// This will set the margins so as to make the rotated image offset the
+	// same space as it is occupying visually...
+	//
+	// NOTE: this is not needed for square image blocks.
+	// NOTE: if an image block is square, this will remove the margins.
+	correctImageProportionsForRotation: function(images){
+		// XXX
+		var W = this.viewer.innerWidth()
+		var H = this.viewer.innerHeight()
+
+		var viewer_p = W > H ? 'landscape' : 'portrait'
+
+		return $(images).each(function(i, e){
+			var image = $(this)
+			// orientation...
+			var o = image.attr('orientation')
+			o = o == null ? 0 : o
+			var w = image.outerWidth()
+			var h = image.outerHeight()
+
+			// non-square image...
+			if(w != h){
+
+				var image_p = w > h ? 'landscape' : 'portrait'
+
+				// when the image is turned 90deg/270deg and its 
+				// proportions are the same as the screen...
+				if((o == 90 || o == 270) && image_p == viewer_p){
+					image.css({
+						width: h,
+						height: w,
+					})
+					image.css({
+						'margin-top': -((w - h)/2),
+						'margin-bottom': -((w - h)/2),
+						'margin-left': (w - h)/2,
+						'margin-right': (w - h)/2,
+					})
+
+				} else if((o == 0 || o == 180) && image_p != viewer_p){
+					image.css({
+						width: h,
+						height: w,
+					})
+					image.css({
+						'margin': '',
+					})
+				}
+
+			// square image...
+			} else {
+				image.css({
+					'margin': '',
+				})
+			}
+		})
+	},
+
+
 	// Image manipulation...
 
 	// Rotate an image...
@@ -633,45 +830,40 @@ module.RibbonsPrototype = {
 	//	.rotateImage(target, 'ccw')
 	//		-> image
 	//
+	// Set explicit image rotation angle:
+	//	.rotateImage(target, 0|90|180|270)
+	//	.rotateImage(target, -90|-180|-270)
+	//		-> image
 	//
 	// NOTE: target must be .getImage(..) compatible.
 	// NOTE: this can be applied in bulk, e.g. 
 	// 		this.rotateImage($('.image'), 'cw') will rotate all the 
 	// 		loaded images clockwise.
 	//
-	// Rotation tables...
-	_cw: {
-		null: 90,
-		0: 90,
-		90: 180,
-		180: 270,
-		//270: 0,
-		270: null,
-	},
-	_ccw: {
-		null: 270,
-		0: 270,
-		//90: 0,
-		90: null,
-		180: 90,
-		270: 180,
-	},
-	// NOTE: setting a value to null will remove the attribute, 0 will 
-	// 		set 0 explicitly...
+	// XXX should this be strongly or weakly coupled to images.updateImage(..) 
+	// 		or images.correctImageProportionsForRotation(..)???
+	// XXX should .correctImageProportionsForRotation(..) be in images???
 	rotateImage: function(target, direction){
-		var r_table = direction == 'cw' ? this._cw : this._ccw
 		target = this.getImage(target)
+
+		// validate direction...
+		if(images.calcRelativeRotation(direction) == null){
+			return target
+		}
+
+		var that = this
 		target.each(function(i, e){
 			var img = $(this)
-			var o = img.attr('orientation')
-			o = r_table[ o == null ? null : o ]
-			if(o == null){
+			var o = direction == 'cw' || direction == 'ccw' 
+				? images.calcRelativeRotation(img.attr('orientation'), direction)
+				: direction*1
+			if(o == 0){
 				img.removeAttr('orientation')
 			} else {
 				img.attr('orientation', o)
 			}
 			// account for proportions...
-			image.correctImageProportionsForRotation(img)
+			that.correctImageProportionsForRotation(img)
 			// XXX this is a bit of an overkill but it will update the 
 			// 		preview if needed...
 			//image.updateImage(img)
@@ -758,8 +950,9 @@ module.RibbonsPrototype = {
 	},
 
 
-	_setup: function(viewer){
+	_setup: function(viewer, images){
 		this.viewer = $(viewer)
+		this.images = images
 	},
 } 
 
@@ -768,13 +961,13 @@ module.RibbonsPrototype = {
 //
 var Ribbons =
 module.Ribbons =
-function Ribbons(viewer){
+function Ribbons(viewer, images){
 	// in case this is called as a function (without new)...
 	if(this.constructor.name != 'Ribbons'){
 		return new Ribbons(viewer)
 	}
 
-	this._setup(viewer)
+	this._setup(viewer, images)
 
 	return this
 }