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