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 293 runtime.assert(widthInPx > 0 && heightInPx > 0, "Both width and height of the image should be greater than 0px."); 294 imageSize = { 295 width: widthInPx, 296 height: heightInPx 297 }; 298 // TODO: resize the image to fit in a cell if paragraphElement is in a table-cell 299 paragraphElement = odfUtils.getParagraphElement(odtDocument.getCursor(inputMemberId).getNode()); 300 styleName = paragraphElement.getAttributeNS(textns, 'style-name'); 301 if (styleName) { 302 // TODO cope with no paragraph style name being specified (i.e., use the default paragraph style) 303 pageContentSize = formatting.getContentSize(styleName, 'paragraph'); 304 imageSize = scaleToAvailableContentSize(imageSize, pageContentSize); 305 } 306 307 insertImageInternal(mimetype, content, imageSize.width + "px", imageSize.height + "px"); 308 }; 309 310 /** 311 * @param {!function(!Error=)} callback, passing an error object in case of error 312 * @return {undefined} 313 */ 314 this.destroy = function (callback) { 315 odtDocument.unsubscribe(ops.Document.signalCursorMoved, onCursorEvent); 316 sessionConstraints.unsubscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState); 317 callback(); 318 }; 319 320 function init() { 321 odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorEvent); 322 sessionConstraints.subscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState); 323 updateEnabledState(); 324 } 325 init(); 326 }; 327 328 /**@const*/gui.ImageController.enabledChanged = "enabled/changed"; 329