UNPKG

node-webodf

Version:

WebODF - JavaScript Document Engine http://webodf.org/

336 lines (306 loc) 12.1 kB
/** * Copyright (C) 2013 KO GmbH <copyright@kogmbh.com> * * @licstart * This file is part of WebODF. * * WebODF is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License (GNU AGPL) * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * WebODF is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with WebODF. If not, see <http://www.gnu.org/licenses/>. * @licend * * @source: http://www.webodf.org/ * @source: https://github.com/kogmbh/WebODF/ */ /*global runtime, core, gui, odf, ops */ /** * @constructor * @param {!ops.Session} session * @param {!gui.SessionConstraints} sessionConstraints * @param {!gui.SessionContext} sessionContext * @param {!string} inputMemberId * @param {!odf.ObjectNameGenerator} objectNameGenerator */ gui.ImageController = function ImageController( session, sessionConstraints, sessionContext, inputMemberId, objectNameGenerator ) { "use strict"; var /**@const @type{!Object.<!string, !string>}*/ fileExtensionByMimetype = { "image/gif": ".gif", "image/jpeg": ".jpg", "image/png": ".png" }, /**@const @type{!string}*/ textns = odf.Namespaces.textns, odtDocument = session.getOdtDocument(), odfUtils = odf.OdfUtils, formatting = odtDocument.getFormatting(), eventNotifier = new core.EventNotifier([ gui.HyperlinkController.enabledChanged ]), isEnabled = false; /** * @return {undefined} */ function updateEnabledState() { var /**@type{!boolean}*/newIsEnabled = true; if (sessionConstraints.getState(gui.CommonConstraints.EDIT.REVIEW_MODE) === true) { newIsEnabled = /**@type{!boolean}*/(sessionContext.isLocalCursorWithinOwnAnnotation()); } if (newIsEnabled !== isEnabled) { isEnabled = newIsEnabled; eventNotifier.emit(gui.ImageController.enabledChanged, isEnabled); } } /** * @param {!ops.OdtCursor} cursor * @return {undefined} */ function onCursorEvent(cursor) { if (cursor.getMemberId() === inputMemberId) { updateEnabledState(); } } /** * @return {!boolean} */ this.isEnabled = function () { return isEnabled; }; /** * @param {!string} eventid * @param {!Function} cb * @return {undefined} */ this.subscribe = function (eventid, cb) { eventNotifier.subscribe(eventid, cb); }; /** * @param {!string} eventid * @param {!Function} cb * @return {undefined} */ this.unsubscribe = function (eventid, cb) { eventNotifier.unsubscribe(eventid, cb); }; /** * @param {!string} name * @return {!ops.Operation} */ function createAddGraphicsStyleOp(name) { var op = new ops.OpAddStyle(); op.init({ memberid: inputMemberId, styleName: name, styleFamily: 'graphic', isAutomaticStyle: false, setProperties: { "style:graphic-properties": { "text:anchor-type": "paragraph", "svg:x": "0cm", "svg:y": "0cm", "style:wrap": "dynamic", "style:number-wrapped-paragraphs": "no-limit", "style:wrap-contour": "false", "style:vertical-pos": "top", "style:vertical-rel": "paragraph", "style:horizontal-pos": "center", "style:horizontal-rel": "paragraph" } } }); return op; } /** * @param {!string} styleName * @param {!string} parentStyleName * @return {!ops.Operation} */ function createAddFrameStyleOp(styleName, parentStyleName) { var op = new ops.OpAddStyle(); op.init({ memberid: inputMemberId, styleName: styleName, styleFamily: 'graphic', isAutomaticStyle: true, setProperties: { "style:parent-style-name": parentStyleName, // a list of properties would be generated by default when inserting a image in LO. // They have no UI impacts in webodf, but copied here in case LO requires them to display image correctly. "style:graphic-properties": { "style:vertical-pos": "top", "style:vertical-rel": "baseline", "style:horizontal-pos": "center", "style:horizontal-rel": "paragraph", "fo:background-color": "transparent", "style:background-transparency": "100%", "style:shadow": "none", "style:mirror": "none", "fo:clip": "rect(0cm, 0cm, 0cm, 0cm)", "draw:luminance": "0%", "draw:contrast": "0%", "draw:red": "0%", "draw:green": "0%", "draw:blue": "0%", "draw:gamma": "100%", "draw:color-inversion": "false", "draw:image-opacity": "100%", "draw:color-mode": "standard" } } }); return op; } /** * @param {!string} mimetype * @return {?string} */ function getFileExtension(mimetype) { mimetype = mimetype.toLowerCase(); return fileExtensionByMimetype.hasOwnProperty(mimetype) ? fileExtensionByMimetype[mimetype] : null; } /** * @param {!string} mimetype * @param {!string} content base64 encoded string * @param {!string} widthMeasure Width + units of the image * @param {!string} heightMeasure Height + units of the image * @return {undefined} */ function insertImageInternal(mimetype, content, widthMeasure, heightMeasure) { var /**@const@type{!string}*/graphicsStyleName = "Graphics", stylesElement = odtDocument.getOdfCanvas().odfContainer().rootElement.styles, fileExtension = getFileExtension(mimetype), fileName, graphicsStyleElement, frameStyleName, op, operations = []; runtime.assert(fileExtension !== null, "Image type is not supported: " + mimetype); fileName = "Pictures/" + objectNameGenerator.generateImageName() + fileExtension; // TODO: eliminate duplicate image op = new ops.OpSetBlob(); op.init({ memberid: inputMemberId, filename: fileName, mimetype: mimetype, content: content }); operations.push(op); // Add the 'Graphics' style if it does not exist in office:styles. It is required by LO to popup the // picture option dialog when double clicking the image // TODO: in collab mode this can result in unsolvable conflict if two add this style at the same time graphicsStyleElement = formatting.getStyleElement(graphicsStyleName, "graphic", [stylesElement]); if (!graphicsStyleElement) { op = createAddGraphicsStyleOp(graphicsStyleName); operations.push(op); } // TODO: reuse an existing graphic style (if there is one) that has same style as default; frameStyleName = objectNameGenerator.generateStyleName(); op = createAddFrameStyleOp(frameStyleName, graphicsStyleName); operations.push(op); op = new ops.OpInsertImage(); op.init({ memberid: inputMemberId, position: odtDocument.getCursorPosition(inputMemberId), filename: fileName, frameWidth: widthMeasure, frameHeight: heightMeasure, frameStyleName: frameStyleName, frameName: objectNameGenerator.generateFrameName() }); operations.push(op); session.enqueue(operations); } /** * Scales the supplied image rect to fit within the page content horizontal * and vertical limits, whilst preserving the aspect ratio. * * @param {!{width: number, height: number}} originalSize * @param {!{width: number, height: number}} pageContentSize * @return {!{width: number, height: number}} */ function scaleToAvailableContentSize(originalSize, pageContentSize) { var widthRatio = 1, heightRatio = 1, ratio; if (originalSize.width > pageContentSize.width) { widthRatio = pageContentSize.width / originalSize.width; } if (originalSize.height > pageContentSize.height) { heightRatio = pageContentSize.height / originalSize.height; } ratio = Math.min(widthRatio, heightRatio); return { width: originalSize.width * ratio, height: originalSize.height * ratio }; } /** * @param {!string} mimetype * @param {!string} content base64 encoded string * @param {!number} widthInPx * @param {!number} heightInPx * @return {undefined} */ this.insertImage = function (mimetype, content, widthInPx, heightInPx) { if (!isEnabled) { return; } var paragraphElement, styleName, pageContentSize, imageSize, cssUnits = new core.CSSUnits(); runtime.assert(widthInPx > 0 && heightInPx > 0, "Both width and height of the image should be greater than 0px."); imageSize = { width: widthInPx, height: heightInPx }; // TODO: resize the image to fit in a cell if paragraphElement is in a table-cell paragraphElement = odfUtils.getParagraphElement(odtDocument.getCursor(inputMemberId).getNode()); styleName = paragraphElement.getAttributeNS(textns, 'style-name'); if (styleName) { // TODO cope with no paragraph style name being specified (i.e., use the default paragraph style) pageContentSize = formatting.getContentSize(styleName, 'paragraph'); imageSize = scaleToAvailableContentSize(imageSize, pageContentSize); } /* LO seems to be unable to digest px width and heights for image frames, and instead shows such images as being 0.22" squares. To avoid that, let's use cm dimensions. */ insertImageInternal(mimetype, content, cssUnits.convert(imageSize.width, "px", "cm") + "cm", cssUnits.convert(imageSize.height, "px", "cm") + "cm" ); }; /** * @param {!function(!Error=)} callback, passing an error object in case of error * @return {undefined} */ this.destroy = function (callback) { odtDocument.unsubscribe(ops.Document.signalCursorMoved, onCursorEvent); sessionConstraints.unsubscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState); callback(); }; function init() { odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorEvent); sessionConstraints.subscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState); updateEnabledState(); } init(); }; /**@const*/gui.ImageController.enabledChanged = "enabled/changed";