Home | History | Annotate | Download | only in liblouis_nacl
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview JavaScript shim for the liblouis Native Client wrapper.
      7  */
      8 
      9 goog.provide('cvox.LibLouis');
     10 
     11 
     12 /**
     13  * Encapsulates a liblouis Native Client instance in the page.
     14  * @constructor
     15  * @param {string} nmfPath Path to .nmf file for the module.
     16  * @param {string=} opt_tablesDir Path to tables directory.
     17  */
     18 cvox.LibLouis = function(nmfPath, opt_tablesDir) {
     19   /**
     20    * Path to .nmf file for the module.
     21    * @private {string}
     22    */
     23   this.nmfPath_ = nmfPath;
     24 
     25   /**
     26    * Path to translation tables.
     27    * @private {?string}
     28    */
     29   this.tablesDir_ = goog.isDef(opt_tablesDir) ? opt_tablesDir : null;
     30 
     31   /**
     32    * Native Client <embed> element.
     33    * {@code null} when no <embed> is attached to the DOM.
     34    * @private {NaClEmbedElement}
     35    */
     36   this.embedElement_ = null;
     37 
     38   /**
     39    * The state of the instance.
     40    * @private {cvox.LibLouis.InstanceState}
     41    */
     42   this.instanceState_ =
     43       cvox.LibLouis.InstanceState.NOT_LOADED;
     44 
     45   /**
     46    * Pending requests to construct translators.
     47    * @private {!Array.<{tableName: string,
     48    *   callback: function(cvox.LibLouis.Translator)}>}
     49    */
     50   this.pendingTranslators_ = [];
     51 
     52   /**
     53    * Pending RPC callbacks. Maps from message IDs to callbacks.
     54    * @private {!Object.<string, function(!Object)>}
     55    */
     56   this.pendingRpcCallbacks_ = {};
     57 
     58   /**
     59    * Next message ID to be used. Incremented with each sent message.
     60    * @private {number}
     61    */
     62   this.nextMessageId_ = 1;
     63 };
     64 
     65 
     66 /**
     67  * Describes the loading state of the instance.
     68  * @enum {number}
     69  */
     70 cvox.LibLouis.InstanceState = {
     71   NOT_LOADED: 0,
     72   LOADING: 1,
     73   LOADED: 2,
     74   ERROR: -1
     75 };
     76 
     77 
     78 /**
     79  * Attaches the Native Client wrapper to the DOM as a child of the provided
     80  * element, assumed to already be in the document.
     81  * @param {!Element} elem Desired parent element of the instance.
     82  */
     83 cvox.LibLouis.prototype.attachToElement = function(elem) {
     84   if (this.isAttached()) {
     85     throw Error('instance already attached');
     86   }
     87 
     88   var embed = document.createElement('embed');
     89   embed.src = this.nmfPath_;
     90   embed.type = 'application/x-nacl';
     91   embed.width = 0;
     92   embed.height = 0;
     93   if (!goog.isNull(this.tablesDir_)) {
     94     embed.setAttribute('tablesdir', this.tablesDir_);
     95   }
     96   embed.addEventListener('load', goog.bind(this.onInstanceLoad_, this),
     97       false /* useCapture */);
     98   embed.addEventListener('error', goog.bind(this.onInstanceError_, this),
     99       false /* useCapture */);
    100   embed.addEventListener('message', goog.bind(this.onInstanceMessage_, this),
    101       false /* useCapture */);
    102   elem.appendChild(embed);
    103 
    104   this.embedElement_ = /** @type {!NaClEmbedElement} */ (embed);
    105   this.instanceState_ = cvox.LibLouis.InstanceState.LOADING;
    106 };
    107 
    108 
    109 /**
    110  * Detaches the Native Client instance from the DOM.
    111  */
    112 cvox.LibLouis.prototype.detach = function() {
    113   if (!this.isAttached()) {
    114     throw Error('cannot detach unattached instance');
    115   }
    116 
    117   this.embedElement_.parentNode.removeChild(this.embedElement_);
    118   this.embedElement_ = null;
    119   this.instanceState_ =
    120       cvox.LibLouis.InstanceState.NOT_LOADED;
    121 };
    122 
    123 
    124 /**
    125  * Determines whether the Native Client instance is attached.
    126  * @return {boolean} {@code true} if the <embed> element is attached to the DOM.
    127  */
    128 cvox.LibLouis.prototype.isAttached = function() {
    129   return this.embedElement_ !== null;
    130 };
    131 
    132 
    133 /**
    134  * Returns a translator for the desired table, asynchronously.
    135  * @param {string} tableNames Comma separated list of braille table names for
    136  *     liblouis.
    137  * @param {function(cvox.LibLouis.Translator)} callback
    138  *     Callback which will receive the translator, or {@code null} on failure.
    139  */
    140 cvox.LibLouis.prototype.getTranslator =
    141     function(tableNames, callback) {
    142   switch (this.instanceState_) {
    143     case cvox.LibLouis.InstanceState.NOT_LOADED:
    144     case cvox.LibLouis.InstanceState.LOADING:
    145       this.pendingTranslators_.push(
    146           { tableNames: tableNames, callback: callback });
    147       return;
    148     case cvox.LibLouis.InstanceState.ERROR:
    149       callback(null /* translator */);
    150       return;
    151     case cvox.LibLouis.InstanceState.LOADED:
    152       this.rpc_('CheckTable', { 'table_names': tableNames },
    153           goog.bind(function(reply) {
    154         if (reply['success']) {
    155           var translator = new cvox.LibLouis.Translator(
    156               this, tableNames);
    157           callback(translator);
    158         } else {
    159           callback(null /* translator */);
    160         }
    161       }, this));
    162       return;
    163   }
    164 };
    165 
    166 
    167 /**
    168  * Dispatches a message to the remote end and returns the reply asynchronously.
    169  * A message ID will be automatically assigned (as a side-effect).
    170  * @param {string} command Command name to be sent.
    171  * @param {!Object} message JSONable message to be sent.
    172  * @param {function(!Object)} callback Callback to receive the reply.
    173  * @private
    174  */
    175 cvox.LibLouis.prototype.rpc_ =
    176     function(command, message, callback) {
    177   if (this.instanceState_ !==
    178       cvox.LibLouis.InstanceState.LOADED) {
    179     throw Error('cannot send RPC: liblouis instance not loaded');
    180   }
    181   var messageId = '' + this.nextMessageId_++;
    182   message['message_id'] = messageId;
    183   message['command'] = command;
    184   var json = JSON.stringify(message);
    185   if (goog.DEBUG) {
    186     window.console.debug('RPC -> ' + json);
    187   }
    188   this.embedElement_.postMessage(json);
    189   this.pendingRpcCallbacks_[messageId] = callback;
    190 };
    191 
    192 
    193 /**
    194  * Invoked when the Native Client instance successfully loads.
    195  * @param {Event} e Event dispatched after loading.
    196  * @private
    197  */
    198 cvox.LibLouis.prototype.onInstanceLoad_ = function(e) {
    199   window.console.info('loaded liblouis Native Client instance');
    200   this.instanceState_ = cvox.LibLouis.InstanceState.LOADED;
    201   this.pendingTranslators_.forEach(goog.bind(function(record) {
    202     this.getTranslator(record.tableNames, record.callback);
    203   }, this));
    204   this.pendingTranslators_.length = 0;
    205 };
    206 
    207 
    208 /**
    209  * Invoked when the Native Client instance fails to load.
    210  * @param {Event} e Event dispatched after loading failure.
    211  * @private
    212  */
    213 cvox.LibLouis.prototype.onInstanceError_ = function(e) {
    214   window.console.error('failed to load liblouis Native Client instance');
    215   this.instanceState_ = cvox.LibLouis.InstanceState.ERROR;
    216   this.pendingTranslators_.forEach(goog.bind(function(record) {
    217     this.getTranslator(record.tableNames, record.callback);
    218   }, this));
    219   this.pendingTranslators_.length = 0;
    220 };
    221 
    222 
    223 /**
    224  * Invoked when the Native Client instance posts a message.
    225  * @param {Event} e Event dispatched after the message was posted.
    226  * @private
    227  */
    228 cvox.LibLouis.prototype.onInstanceMessage_ = function(e) {
    229   if (goog.DEBUG) {
    230     window.console.debug('RPC <- ' + e.data);
    231   }
    232   var message = /** @type {!Object} */ (JSON.parse(e.data));
    233   var messageId = message['in_reply_to'];
    234   if (!goog.isDef(messageId)) {
    235     window.console.warn('liblouis Native Client module sent message with no ID',
    236         message);
    237     return;
    238   }
    239   if (goog.isDef(message['error'])) {
    240     window.console.error('liblouis Native Client error', message['error']);
    241   }
    242   var callback = this.pendingRpcCallbacks_[messageId];
    243   if (goog.isDef(callback)) {
    244     delete this.pendingRpcCallbacks_[messageId];
    245     callback(message);
    246   }
    247 };
    248 
    249 
    250 /**
    251  * Braille translator which uses a Native Client instance of liblouis.
    252  * @constructor
    253  * @param {!cvox.LibLouis} instance The instance wrapper.
    254  * @param {string} tableNames Comma separated list of Table names to be passed
    255  *     to liblouis.
    256  */
    257 cvox.LibLouis.Translator = function(instance, tableNames) {
    258   /**
    259    * The instance wrapper.
    260    * @private {!cvox.LibLouis}
    261    */
    262   this.instance_ = instance;
    263 
    264   /**
    265    * The table name.
    266    * @private {string}
    267    */
    268   this.tableNames_ = tableNames;
    269 };
    270 
    271 
    272 /**
    273  * Translates text into braille cells.
    274  * @param {string} text Text to be translated.
    275  * @param {function(ArrayBuffer, Array.<number>, Array.<number>)} callback
    276  *     Callback for result.  Takes 3 parameters: the resulting cells,
    277  *     mapping from text to braille positions and mapping from braille to
    278  *     text positions.  If translation fails for any reason, all parameters are
    279  *     {@code null}.
    280  */
    281 cvox.LibLouis.Translator.prototype.translate = function(text, callback) {
    282   var message = { 'table_names': this.tableNames_, 'text': text };
    283   this.instance_.rpc_('Translate', message, function(reply) {
    284     var cells = null;
    285     var textToBraille = null;
    286     var brailleToText = null;
    287     if (reply['success'] && goog.isString(reply['cells'])) {
    288       cells = cvox.LibLouis.Translator.decodeHexString_(reply['cells']);
    289       if (goog.isDef(reply['text_to_braille'])) {
    290         textToBraille = reply['text_to_braille'];
    291       }
    292       if (goog.isDef(reply['braille_to_text'])) {
    293         brailleToText = reply['braille_to_text'];
    294       }
    295     } else if (text.length > 0) {
    296       // TODO(plundblad): The nacl wrapper currently returns an error
    297       // when translating an empty string.  Address that and always log here.
    298       console.error('Braille translation error for ' + JSON.stringify(message));
    299     }
    300     callback(cells, textToBraille, brailleToText);
    301   });
    302 };
    303 
    304 
    305 /**
    306  * Translates braille cells into text.
    307  * @param {!ArrayBuffer} cells Cells to be translated.
    308  * @param {function(?string)} callback Callback for result.
    309  */
    310 cvox.LibLouis.Translator.prototype.backTranslate =
    311     function(cells, callback) {
    312   if (cells.byteLength == 0) {
    313     // liblouis doesn't handle empty input, so handle that trivially
    314     // here.
    315     callback('');
    316     return;
    317   }
    318   var message = {
    319     'table_names': this.tableNames_,
    320     'cells': cvox.LibLouis.Translator.encodeHexString_(cells)
    321   };
    322   this.instance_.rpc_('BackTranslate', message, function(reply) {
    323     if (reply['success'] && goog.isString(reply['text'])) {
    324       callback(reply['text']);
    325     } else {
    326       callback(null /* text */);
    327     }
    328   });
    329 };
    330 
    331 
    332 /**
    333  * Decodes a hexadecimal string to an {@code ArrayBuffer}.
    334  * @param {string} hex Hexadecimal string.
    335  * @return {!ArrayBuffer} Decoded binary data.
    336  * @private
    337  */
    338 cvox.LibLouis.Translator.decodeHexString_ = function(hex) {
    339   if (!/^([0-9a-f]{2})*$/i.test(hex)) {
    340     throw Error('invalid hexadecimal string');
    341   }
    342   var array = new Uint8Array(hex.length / 2);
    343   var idx = 0;
    344   for (var i = 0; i < hex.length; i += 2) {
    345     array[idx++] = parseInt(hex.substring(i, i + 2), 16);
    346   }
    347   return array.buffer;
    348 };
    349 
    350 
    351 /**
    352  * Encodes an {@code ArrayBuffer} in hexadecimal.
    353  * @param {!ArrayBuffer} arrayBuffer Binary data.
    354  * @return {string} Hexadecimal string.
    355  * @private
    356  */
    357 cvox.LibLouis.Translator.encodeHexString_ = function(arrayBuffer) {
    358   var array = new Uint8Array(arrayBuffer);
    359   var hex = '';
    360   for (var i = 0; i < array.length; i++) {
    361     var b = array[i];
    362     hex += (b < 0x10 ? '0' : '') + b.toString(16);
    363   }
    364   return hex;
    365 };
    366