1 /** 2 * Copyright (C) 2013 KO GmbH <copyright@kogmbh.com> 3 * 4 * @licstart 5 * This file is part of WebODF. 6 * 7 * WebODF is free software: you can redistribute it and/or modify it 8 * under the terms of the GNU Affero General Public License (GNU AGPL) 9 * as published by the Free Software Foundation, either version 3 of 10 * the License, or (at your option) any later version. 11 * 12 * WebODF is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with WebODF. If not, see <http://www.gnu.org/licenses/>. 19 * @licend 20 * 21 * @source: http://www.webodf.org/ 22 * @source: https://github.com/kogmbh/WebODF/ 23 */ 24 25 /*global runtime, core, gui, odf, ops */ 26 27 /** 28 * @constructor 29 * @param {!ops.Session} session 30 * @param {!gui.SessionConstraints} sessionConstraints 31 * @param {!gui.SessionContext} sessionContext 32 * @param {!string} inputMemberId 33 * @param {!odf.ObjectNameGenerator} objectNameGenerator 34 */ 35 gui.ImageController = function ImageController( 36 session, 37 sessionConstraints, 38 sessionContext, 39 inputMemberId, 40 objectNameGenerator 41 ) { 42 "use strict"; 43 44 var /**@const 45 @type{!Object.<!string, !string>}*/ 46 fileExtensionByMimetype = { 47 "image/gif": ".gif", 48 "image/jpeg": ".jpg", 49 "image/png": ".png" 50 }, 51 /**@const 52 @type{!string}*/ 53 textns = odf.Namespaces.textns, 54 odtDocument = session.getOdtDocument(), 55 odfUtils = odf.OdfUtils, 56 formatting = odtDocument.getFormatting(), 57 eventNotifier = new core.EventNotifier([ 58 gui.HyperlinkController.enabledChanged 59 ]), 60 isEnabled = false; 61 62 /** 63 * @return {undefined} 64 */ 65 function updateEnabledState() { 66 var /**@type{!boolean}*/newIsEnabled = true; 67 68 if (sessionConstraints.getState(gui.CommonConstraints.EDIT.REVIEW_MODE) === true) { 69 newIsEnabled = /**@type{!boolean}*/(sessionContext.isLocalCursorWithinOwnAnnotation()); 70 } 71 72 if (newIsEnabled !== isEnabled) { 73 isEnabled = newIsEnabled; 74 eventNotifier.emit(gui.ImageController.enabledChanged, isEnabled); 75 } 76 } 77 78 /** 79 * @param {!ops.OdtCursor} cursor 80 * @return {undefined} 81 */ 82 function onCursorEvent(cursor) { 83 if (cursor.getMemberId() === inputMemberId) { 84 updateEnabledState(); 85 } 86 } 87 88 /** 89 * @return {!boolean} 90 */ 91 this.isEnabled = function () { 92 return isEnabled; 93 }; 94 95 /** 96 * @param {!string} eventid 97 * @param {!Function} cb 98 * @return {undefined} 99 */ 100 this.subscribe = function (eventid, cb) { 101 eventNotifier.subscribe(eventid, cb); 102 }; 103 104 /** 105 * @param {!string} eventid 106 * @param {!Function} cb 107 * @return {undefined} 108 */ 109 this.unsubscribe = function (eventid, cb) { 110 eventNotifier.unsubscribe(eventid, cb); 111 }; 112 113 114 /** 115 * @param {!string} name 116 * @return {!ops.Operation} 117 */ 118 function createAddGraphicsStyleOp(name) { 119 var op = new ops.OpAddStyle(); 120 op.init({ 121 memberid: inputMemberId, 122 styleName: name, 123 styleFamily: 'graphic', 124 isAutomaticStyle: false, 125 setProperties: { 126 "style:graphic-properties": { 127 "text:anchor-type": "paragraph", 128 "svg:x": "0cm", 129 "svg:y": "0cm", 130 "style:wrap": "dynamic", 131 "style:number-wrapped-paragraphs": "no-limit", 132 "style:wrap-contour": "false", 133 "style:vertical-pos": "top", 134 "style:vertical-rel": "paragraph", 135 "style:horizontal-pos": "center", 136 "style:horizontal-rel": "paragraph" 137 } 138 } 139 }); 140 return op; 141 } 142 143 /** 144 * @param {!string} styleName 145 * @param {!string} parentStyleName 146 * @return {!ops.Operation} 147 */ 148 function createAddFrameStyleOp(styleName, parentStyleName) { 149 var op = new ops.OpAddStyle(); 150 op.init({ 151 memberid: inputMemberId, 152 styleName: styleName, 153 styleFamily: 'graphic', 154 isAutomaticStyle: true, 155 setProperties: { 156 "style:parent-style-name": parentStyleName, 157 // a list of properties would be generated by default when inserting a image in LO. 158 // They have no UI impacts in webodf, but copied here in case LO requires them to display image correctly. 159 "style:graphic-properties": { 160 "style:vertical-pos": "top", 161 "style:vertical-rel": "baseline", 162 "style:horizontal-pos": "center", 163 "style:horizontal-rel": "paragraph", 164 "fo:background-color": "transparent", 165 "style:background-transparency": "100%", 166 "style:shadow": "none", 167 "style:mirror": "none", 168 "fo:clip": "rect(0cm, 0cm, 0cm, 0cm)", 169 "draw:luminance": "0%", 170 "draw:contrast": "0%", 171 "draw:red": "0%", 172 "draw:green": "0%", 173 "draw:blue": "0%", 174 "draw:gamma": "100%", 175 "draw:color-inversion": "false", 176 "draw:image-opacity": "100%", 177 "draw:color-mode": "standard" 178 } 179 } 180 }); 181 return op; 182 } 183 184 /** 185 * @param {!string} mimetype 186 * @return {?string} 187 */ 188 function getFileExtension(mimetype) { 189 mimetype = mimetype.toLowerCase(); 190 return fileExtensionByMimetype.hasOwnProperty(mimetype) ? fileExtensionByMimetype[mimetype] : null; 191 } 192 193 /** 194 * @param {!string} mimetype 195 * @param {!string} content base64 encoded string 196 * @param {!string} widthMeasure Width + units of the image 197 * @param {!string} heightMeasure Height + units of the image 198 * @return {undefined} 199 */ 200 function insertImageInternal(mimetype, content, widthMeasure, heightMeasure) { 201 var /**@const@type{!string}*/graphicsStyleName = "Graphics", 202 stylesElement = odtDocument.getOdfCanvas().odfContainer().rootElement.styles, 203 fileExtension = getFileExtension(mimetype), 204 fileName, 205 graphicsStyleElement, 206 frameStyleName, 207 op, operations = []; 208 209 runtime.assert(fileExtension !== null, "Image type is not supported: " + mimetype); 210 fileName = "Pictures/" + objectNameGenerator.generateImageName() + fileExtension; 211 212 // TODO: eliminate duplicate image 213 op = new ops.OpSetBlob(); 214 op.init({ 215 memberid: inputMemberId, 216 filename: fileName, 217 mimetype: mimetype, 218 content: content 219 }); 220 operations.push(op); 221 222 // Add the 'Graphics' style if it does not exist in office:styles. It is required by LO to popup the 223 // picture option dialog when double clicking the image 224 // TODO: in collab mode this can result in unsolvable conflict if two add this style at the same time 225 graphicsStyleElement = formatting.getStyleElement(graphicsStyleName, "graphic", [stylesElement]); 226 if (!graphicsStyleElement) { 227 op = createAddGraphicsStyleOp(graphicsStyleName); 228 operations.push(op); 229 } 230 231 // TODO: reuse an existing graphic style (if there is one) that has same style as default; 232 frameStyleName = objectNameGenerator.generateStyleName(); 233 op = createAddFrameStyleOp(frameStyleName, graphicsStyleName); 234 operations.push(op); 235 236 op = new ops.OpInsertImage(); 237 op.init({ 238 memberid: inputMemberId, 239 position: odtDocument.getCursorPosition(inputMemberId), 240 filename: fileName, 241 frameWidth: widthMeasure, 242 frameHeight: heightMeasure, 243 frameStyleName: frameStyleName, 244 frameName: objectNameGenerator.generateFrameName() 245 }); 246 operations.push(op); 247 248 session.enqueue(operations); 249 } 250 251 /** 252 * Scales the supplied image rect to fit within the page content horizontal 253 * and vertical limits, whilst preserving the aspect ratio. 254 * 255 * @param {!{width: number, height: number}} originalSize 256 * @param {!{width: number, height: number}} pageContentSize 257 * @return {!{width: number, height: number}} 258 */ 259 function scaleToAvailableContentSize(originalSize, pageContentSize) { 260 var widthRatio = 1, 261 heightRatio = 1, 262 ratio; 263 if (originalSize.width > pageContentSize.width) { 264 widthRatio = pageContentSize.width / originalSize.width; 265 } 266 if (originalSize.height > pageContentSize.height) { 267 heightRatio = pageContentSize.height / originalSize.height; 268 } 269 ratio = Math.min(widthRatio, heightRatio); 270 return { 271 width: originalSize.width * ratio, 272 height: originalSize.height * ratio 273 }; 274 } 275 276 /** 277 * @param {!string} mimetype 278 * @param {!string} content base64 encoded string 279 * @param {!number} widthInPx 280 * @param {!number} heightInPx 281 * @return {undefined} 282 */ 283 this.insertImage = function (mimetype, content, widthInPx, heightInPx) { 284 if (!isEnabled) { 285 return; 286 } 287 288 var paragraphElement, 289 styleName, 290 pageContentSize, 291 imageSize, 292 cssUnits = new core.CSSUnits(); 293 294 runtime.assert(widthInPx > 0 && heightInPx > 0, "Both width and height of the image should be greater than 0px."); 295 imageSize = { 296 width: widthInPx, 297 height: heightInPx 298 }; 299 // TODO: resize the image to fit in a cell if paragraphElement is in a table-cell 300 paragraphElement = odfUtils.getParagraphElement(odtDocument.getCursor(inputMemberId).getNode()); 301 styleName = paragraphElement.getAttributeNS(textns, 'style-name'); 302 if (styleName) { 303 // TODO cope with no paragraph style name being specified (i.e., use the default paragraph style) 304 pageContentSize = formatting.getContentSize(styleName, 'paragraph'); 305 imageSize = scaleToAvailableContentSize(imageSize, pageContentSize); 306 } 307 308 /* LO seems to be unable to digest px width and heights for image frames, and instead shows such 309 images as being 0.22" squares. To avoid that, let's use cm dimensions. 310 */ 311 insertImageInternal(mimetype, content, 312 cssUnits.convert(imageSize.width, "px", "cm") + "cm", 313 cssUnits.convert(imageSize.height, "px", "cm") + "cm" 314 ); 315 }; 316 317 /** 318 * @param {!function(!Error=)} callback, passing an error object in case of error 319 * @return {undefined} 320 */ 321 this.destroy = function (callback) { 322 odtDocument.unsubscribe(ops.Document.signalCursorMoved, onCursorEvent); 323 sessionConstraints.unsubscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState); 324 callback(); 325 }; 326 327 function init() { 328 odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorEvent); 329 sessionConstraints.subscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState); 330 updateEnabledState(); 331 } 332 init(); 333 }; 334 335 /**@const*/gui.ImageController.enabledChanged = "enabled/changed"; 336