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 formatting = odtDocument.getFormatting(), 56 eventNotifier = new core.EventNotifier([ 57 gui.HyperlinkController.enabledChanged 58 ]), 59 isEnabled = false; 60 61 /** 62 * @return {undefined} 63 */ 64 function updateEnabledState() { 65 var /**@type{!boolean}*/newIsEnabled = true; 66 67 if (sessionConstraints.getState(gui.CommonConstraints.EDIT.REVIEW_MODE) === true) { 68 newIsEnabled = /**@type{!boolean}*/(sessionContext.isLocalCursorWithinOwnAnnotation()); 69 } 70 71 if (newIsEnabled !== isEnabled) { 72 isEnabled = newIsEnabled; 73 eventNotifier.emit(gui.ImageController.enabledChanged, isEnabled); 74 } 75 } 76 77 /** 78 * @param {!ops.OdtCursor} cursor 79 * @return {undefined} 80 */ 81 function onCursorEvent(cursor) { 82 if (cursor.getMemberId() === inputMemberId) { 83 updateEnabledState(); 84 } 85 } 86 87 /** 88 * @return {!boolean} 89 */ 90 this.isEnabled = function () { 91 return isEnabled; 92 }; 93 94 /** 95 * @param {!string} eventid 96 * @param {!Function} cb 97 * @return {undefined} 98 */ 99 this.subscribe = function (eventid, cb) { 100 eventNotifier.subscribe(eventid, cb); 101 }; 102 103 /** 104 * @param {!string} eventid 105 * @param {!Function} cb 106 * @return {undefined} 107 */ 108 this.unsubscribe = function (eventid, cb) { 109 eventNotifier.unsubscribe(eventid, cb); 110 }; 111 112 113 /** 114 * @param {!string} name 115 * @return {!ops.Operation} 116 */ 117 function createAddGraphicsStyleOp(name) { 118 var op = new ops.OpAddStyle(); 119 op.init({ 120 memberid: inputMemberId, 121 styleName: name, 122 styleFamily: 'graphic', 123 isAutomaticStyle: false, 124 setProperties: { 125 "style:graphic-properties": { 126 "text:anchor-type": "paragraph", 127 "svg:x": "0cm", 128 "svg:y": "0cm", 129 "style:wrap": "dynamic", 130 "style:number-wrapped-paragraphs": "no-limit", 131 "style:wrap-contour": "false", 132 "style:vertical-pos": "top", 133 "style:vertical-rel": "paragraph", 134 "style:horizontal-pos": "center", 135 "style:horizontal-rel": "paragraph" 136 } 137 } 138 }); 139 return op; 140 } 141 142 /** 143 * @param {!string} styleName 144 * @param {!string} parentStyleName 145 * @return {!ops.Operation} 146 */ 147 function createAddFrameStyleOp(styleName, parentStyleName) { 148 var op = new ops.OpAddStyle(); 149 op.init({ 150 memberid: inputMemberId, 151 styleName: styleName, 152 styleFamily: 'graphic', 153 isAutomaticStyle: true, 154 setProperties: { 155 "style:parent-style-name": parentStyleName, 156 // a list of properties would be generated by default when inserting a image in LO. 157 // They have no UI impacts in webodf, but copied here in case LO requires them to display image correctly. 158 "style:graphic-properties": { 159 "style:vertical-pos": "top", 160 "style:vertical-rel": "baseline", 161 "style:horizontal-pos": "center", 162 "style:horizontal-rel": "paragraph", 163 "fo:background-color": "transparent", 164 "style:background-transparency": "100%", 165 "style:shadow": "none", 166 "style:mirror": "none", 167 "fo:clip": "rect(0cm, 0cm, 0cm, 0cm)", 168 "draw:luminance": "0%", 169 "draw:contrast": "0%", 170 "draw:red": "0%", 171 "draw:green": "0%", 172 "draw:blue": "0%", 173 "draw:gamma": "100%", 174 "draw:color-inversion": "false", 175 "draw:image-opacity": "100%", 176 "draw:color-mode": "standard" 177 } 178 } 179 }); 180 return op; 181 } 182 183 /** 184 * @param {!string} mimetype 185 * @return {?string} 186 */ 187 function getFileExtension(mimetype) { 188 mimetype = mimetype.toLowerCase(); 189 return fileExtensionByMimetype.hasOwnProperty(mimetype) ? fileExtensionByMimetype[mimetype] : null; 190 } 191 192 /** 193 * @param {!string} mimetype 194 * @param {!string} content base64 encoded string 195 * @param {!string} widthMeasure Width + units of the image 196 * @param {!string} heightMeasure Height + units of the image 197 * @return {undefined} 198 */ 199 function insertImageInternal(mimetype, content, widthMeasure, heightMeasure) { 200 var /**@const@type{!string}*/graphicsStyleName = "Graphics", 201 stylesElement = odtDocument.getOdfCanvas().odfContainer().rootElement.styles, 202 fileExtension = getFileExtension(mimetype), 203 fileName, 204 graphicsStyleElement, 205 frameStyleName, 206 op, operations = []; 207 208 runtime.assert(fileExtension !== null, "Image type is not supported: " + mimetype); 209 fileName = "Pictures/" + objectNameGenerator.generateImageName() + fileExtension; 210 211 // TODO: eliminate duplicate image 212 op = new ops.OpSetBlob(); 213 op.init({ 214 memberid: inputMemberId, 215 filename: fileName, 216 mimetype: mimetype, 217 content: content 218 }); 219 operations.push(op); 220 221 // Add the 'Graphics' style if it does not exist in office:styles. It is required by LO to popup the 222 // picture option dialog when double clicking the image 223 // TODO: in collab mode this can result in unsolvable conflict if two add this style at the same time 224 graphicsStyleElement = formatting.getStyleElement(graphicsStyleName, "graphic", [stylesElement]); 225 if (!graphicsStyleElement) { 226 op = createAddGraphicsStyleOp(graphicsStyleName); 227 operations.push(op); 228 } 229 230 // TODO: reuse an existing graphic style (if there is one) that has same style as default; 231 frameStyleName = objectNameGenerator.generateStyleName(); 232 op = createAddFrameStyleOp(frameStyleName, graphicsStyleName); 233 operations.push(op); 234 235 op = new ops.OpInsertImage(); 236 op.init({ 237 memberid: inputMemberId, 238 position: odtDocument.getCursorPosition(inputMemberId), 239 filename: fileName, 240 frameWidth: widthMeasure, 241 frameHeight: heightMeasure, 242 frameStyleName: frameStyleName, 243 frameName: objectNameGenerator.generateFrameName() 244 }); 245 operations.push(op); 246 247 session.enqueue(operations); 248 } 249 250 /** 251 * Scales the supplied image rect to fit within the page content horizontal 252 * and vertical limits, whilst preserving the aspect ratio. 253 * 254 * @param {!{width: number, height: number}} originalSize 255 * @param {!{width: number, height: number}} pageContentSize 256 * @return {!{width: number, height: number}} 257 */ 258 function scaleToAvailableContentSize(originalSize, pageContentSize) { 259 var widthRatio = 1, 260 heightRatio = 1, 261 ratio; 262 if (originalSize.width > pageContentSize.width) { 263 widthRatio = pageContentSize.width / originalSize.width; 264 } 265 if (originalSize.height > pageContentSize.height) { 266 heightRatio = pageContentSize.height / originalSize.height; 267 } 268 ratio = Math.min(widthRatio, heightRatio); 269 return { 270 width: originalSize.width * ratio, 271 height: originalSize.height * ratio 272 }; 273 } 274 275 /** 276 * @param {!string} mimetype 277 * @param {!string} content base64 encoded string 278 * @param {!number} widthInPx 279 * @param {!number} heightInPx 280 * @return {undefined} 281 */ 282 this.insertImage = function (mimetype, content, widthInPx, heightInPx) { 283 if (!isEnabled) { 284 return; 285 } 286 287 var paragraphElement, 288 styleName, 289 pageContentSize, 290 imageSize; 291 292 runtime.assert(widthInPx > 0 && heightInPx > 0, "Both width and height of the image should be greater than 0px."); 293 imageSize = { 294 width: widthInPx, 295 height: heightInPx 296 }; 297 // TODO: resize the image to fit in a cell if paragraphElement is in a table-cell 298 paragraphElement = odtDocument.getParagraphElement(odtDocument.getCursor(inputMemberId).getNode()); 299 styleName = paragraphElement.getAttributeNS(textns, 'style-name'); 300 if (styleName) { 301 // TODO cope with no paragraph style name being specified (i.e., use the default paragraph style) 302 pageContentSize = formatting.getContentSize(styleName, 'paragraph'); 303 imageSize = scaleToAvailableContentSize(imageSize, pageContentSize); 304 } 305 306 insertImageInternal(mimetype, content, imageSize.width + "px", imageSize.height + "px"); 307 }; 308 309 /** 310 * @param {!function(!Error=)} callback, passing an error object in case of error 311 * @return {undefined} 312 */ 313 this.destroy = function (callback) { 314 odtDocument.unsubscribe(ops.Document.signalCursorMoved, onCursorEvent); 315 sessionConstraints.unsubscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState); 316 callback(); 317 }; 318 319 function init() { 320 odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorEvent); 321 sessionConstraints.subscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState); 322 updateEnabledState(); 323 } 324 init(); 325 }; 326 327 /**@const*/gui.ImageController.enabledChanged = "enabled/changed"; 328