| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 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 'use strict'; | |
| 6 | |
| 7 // TODO:(kaznacheev) Share the EXIF constants with exif_parser.js | |
| 8 var EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). | |
| 9 var EXIF_MARK_SOI = 0xffd8; // Start of image data. | |
| 10 var EXIF_MARK_EOI = 0xffd9; // End of image data. | |
| 11 | |
| 12 var EXIF_MARK_APP0 = 0xffe0; // APP0 block, most commonly JFIF data. | |
| 13 var EXIF_MARK_EXIF = 0xffe1; // Start of exif block. | |
| 14 | |
| 15 var EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. | |
| 16 var EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data. | |
| 17 | |
| 18 var EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data. | |
| 19 var EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory. | |
| 20 var EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD. | |
| 21 | |
| 22 var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail. | |
| 23 var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data. | |
| 24 | |
| 25 var EXIF_TAG_IMAGE_WIDTH = 0x0100; | |
| 26 var EXIF_TAG_IMAGE_HEIGHT = 0x0101; | |
| 27 | |
| 28 var EXIF_TAG_ORIENTATION = 0x0112; | |
| 29 var EXIF_TAG_X_DIMENSION = 0xA002; | |
| 30 var EXIF_TAG_Y_DIMENSION = 0xA003; | |
| 31 | |
| 32 /** | |
| 33 * The Exif metadata encoder. | |
| 34 * Uses the metadata format as defined by ExifParser. | |
| 35 * @param {Object} original_metadata Metadata to encode. | |
| 36 * @constructor | |
| 37 * @extends {ImageEncoder.MetadataEncoder} | |
| 38 */ | |
| 39 function ExifEncoder(original_metadata) { | |
| 40 ImageEncoder.MetadataEncoder.apply(this, arguments); | |
| 41 | |
| 42 this.ifd_ = this.metadata_.ifd; | |
| 43 if (!this.ifd_) | |
| 44 this.ifd_ = this.metadata_.ifd = {}; | |
| 45 } | |
| 46 | |
| 47 ExifEncoder.prototype = {__proto__: ImageEncoder.MetadataEncoder.prototype}; | |
| 48 | |
| 49 ImageEncoder.registerMetadataEncoder(ExifEncoder, 'image/jpeg'); | |
| 50 | |
| 51 /** | |
| 52 * @param {HTMLCanvasElement|Object} canvas Canvas or anything with | |
| 53 * width and height properties. | |
| 54 */ | |
| 55 ExifEncoder.prototype.setImageData = function(canvas) { | |
| 56 var image = this.ifd_.image; | |
| 57 if (!image) | |
| 58 image = this.ifd_.image = {}; | |
| 59 | |
| 60 // Only update width/height in this directory if they are present. | |
| 61 if (image[EXIF_TAG_IMAGE_WIDTH] && image[EXIF_TAG_IMAGE_HEIGHT]) { | |
| 62 image[EXIF_TAG_IMAGE_WIDTH].value = canvas.width; | |
| 63 image[EXIF_TAG_IMAGE_HEIGHT].value = canvas.height; | |
| 64 } | |
| 65 | |
| 66 var exif = this.ifd_.exif; | |
| 67 if (!exif) | |
| 68 exif = this.ifd_.exif = {}; | |
| 69 ExifEncoder.findOrCreateTag(image, EXIF_TAG_EXIFDATA); | |
| 70 ExifEncoder.findOrCreateTag(exif, EXIF_TAG_X_DIMENSION).value = canvas.width; | |
| 71 ExifEncoder.findOrCreateTag(exif, EXIF_TAG_Y_DIMENSION).value = canvas.height; | |
| 72 | |
| 73 this.metadata_.width = canvas.width; | |
| 74 this.metadata_.height = canvas.height; | |
| 75 | |
| 76 // Always save in default orientation. | |
| 77 delete this.metadata_.imageTransform; | |
| 78 ExifEncoder.findOrCreateTag(image, EXIF_TAG_ORIENTATION).value = 1; | |
| 79 }; | |
| 80 | |
| 81 | |
| 82 /** | |
| 83 * @param {HTMLCanvasElement} canvas Thumbnail canvas. | |
| 84 * @param {number} quality (0..1] Thumbnail encoding quality. | |
| 85 */ | |
| 86 ExifEncoder.prototype.setThumbnailData = function(canvas, quality) { | |
| 87 // Empirical formula with reasonable behavior: | |
| 88 // 10K for 1Mpix, 30K for 5Mpix, 50K for 9Mpix and up. | |
| 89 var pixelCount = this.metadata_.width * this.metadata_.height; | |
| 90 var maxEncodedSize = 5000 * Math.min(10, 1 + pixelCount / 1000000); | |
| 91 | |
| 92 var DATA_URL_PREFIX = 'data:' + this.mimeType + ';base64,'; | |
| 93 var BASE64_BLOAT = 4 / 3; | |
| 94 var maxDataURLLength = | |
| 95 DATA_URL_PREFIX.length + Math.ceil(maxEncodedSize * BASE64_BLOAT); | |
| 96 | |
| 97 for (;; quality *= 0.8) { | |
| 98 ImageEncoder.MetadataEncoder.prototype.setThumbnailData.call( | |
| 99 this, canvas, quality); | |
| 100 if (this.metadata_.thumbnailURL.length <= maxDataURLLength || quality < 0.2) | |
| 101 break; | |
| 102 } | |
| 103 | |
| 104 if (this.metadata_.thumbnailURL.length <= maxDataURLLength) { | |
| 105 var thumbnail = this.ifd_.thumbnail; | |
| 106 if (!thumbnail) | |
| 107 thumbnail = this.ifd_.thumbnail = {}; | |
| 108 | |
| 109 ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_IMAGE_WIDTH).value = | |
| 110 canvas.width; | |
| 111 | |
| 112 ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_IMAGE_HEIGHT).value = | |
| 113 canvas.height; | |
| 114 | |
| 115 // The values for these tags will be set in ExifWriter.encode. | |
| 116 ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_JPG_THUMB_OFFSET); | |
| 117 ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_JPG_THUMB_LENGTH); | |
| 118 | |
| 119 // Always save in default orientation. | |
| 120 ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_ORIENTATION).value = 1; | |
| 121 } else { | |
| 122 console.warn( | |
| 123 'Thumbnail URL too long: ' + this.metadata_.thumbnailURL.length); | |
| 124 // Delete thumbnail ifd so that it is not written out to a file, but | |
| 125 // keep thumbnailURL for display purposes. | |
| 126 if (this.ifd_.thumbnail) { | |
| 127 delete this.ifd_.thumbnail; | |
| 128 } | |
| 129 } | |
| 130 delete this.metadata_.thumbnailTransform; | |
| 131 }; | |
| 132 | |
| 133 /** | |
| 134 * Return a range where the metadata is (or should be) located. | |
| 135 * @param {string} encodedImage Raw image data to look for metadata. | |
| 136 * @return {Object} An object with from and to properties. | |
| 137 */ | |
| 138 ExifEncoder.prototype.findInsertionRange = function(encodedImage) { | |
| 139 function getWord(pos) { | |
| 140 if (pos + 2 > encodedImage.length) | |
| 141 throw 'Reading past the buffer end @' + pos; | |
| 142 return encodedImage.charCodeAt(pos) << 8 | encodedImage.charCodeAt(pos + 1); | |
| 143 } | |
| 144 | |
| 145 if (getWord(0) != EXIF_MARK_SOI) | |
| 146 throw new Error('Jpeg data starts from 0x' + getWord(0).toString(16)); | |
| 147 | |
| 148 var sectionStart = 2; | |
| 149 | |
| 150 // Default: an empty range right after SOI. | |
| 151 // Will be returned in absence of APP0 or Exif sections. | |
| 152 var range = {from: sectionStart, to: sectionStart}; | |
| 153 | |
| 154 for (;;) { | |
| 155 var tag = getWord(sectionStart); | |
| 156 | |
| 157 if (tag == EXIF_MARK_SOS) | |
| 158 break; | |
| 159 | |
| 160 var nextSectionStart = sectionStart + 2 + getWord(sectionStart + 2); | |
| 161 if (nextSectionStart <= sectionStart || | |
| 162 nextSectionStart > encodedImage.length) | |
| 163 throw new Error('Invalid section size in jpeg data'); | |
| 164 | |
| 165 if (tag == EXIF_MARK_APP0) { | |
| 166 // Assert that we have not seen the Exif section yet. | |
| 167 if (range.from != range.to) | |
| 168 throw new Error('APP0 section found after EXIF section'); | |
| 169 // An empty range right after the APP0 segment. | |
| 170 range.from = range.to = nextSectionStart; | |
| 171 } else if (tag == EXIF_MARK_EXIF) { | |
| 172 // A range containing the existing EXIF section. | |
| 173 range.from = sectionStart; | |
| 174 range.to = nextSectionStart; | |
| 175 } | |
| 176 sectionStart = nextSectionStart; | |
| 177 } | |
| 178 | |
| 179 return range; | |
| 180 }; | |
| 181 | |
| 182 /** | |
| 183 * @return {ArrayBuffer} serialized metadata ready to write to an image file. | |
| 184 */ | |
| 185 ExifEncoder.prototype.encode = function() { | |
| 186 var HEADER_SIZE = 10; | |
| 187 | |
| 188 // Allocate the largest theoretically possible size. | |
| 189 var bytes = new Uint8Array(0x10000); | |
| 190 | |
| 191 // Serialize header | |
| 192 var hw = new ByteWriter(bytes.buffer, 0, HEADER_SIZE); | |
| 193 hw.writeScalar(EXIF_MARK_EXIF, 2); | |
| 194 hw.forward('size', 2); | |
| 195 hw.writeString('Exif\0\0'); // Magic string. | |
| 196 | |
| 197 // First serialize the content of the exif section. | |
| 198 // Use a ByteWriter starting at HEADER_SIZE offset so that tell() positions | |
| 199 // can be directly mapped to offsets as encoded in the dictionaries. | |
| 200 var bw = new ByteWriter(bytes.buffer, HEADER_SIZE); | |
| 201 | |
| 202 if (this.metadata_.littleEndian) { | |
| 203 bw.setByteOrder(ByteWriter.LITTLE_ENDIAN); | |
| 204 bw.writeScalar(EXIF_ALIGN_LITTLE, 2); | |
| 205 } else { | |
| 206 bw.setByteOrder(ByteWriter.BIG_ENDIAN); | |
| 207 bw.writeScalar(EXIF_ALIGN_BIG, 2); | |
| 208 } | |
| 209 | |
| 210 bw.writeScalar(EXIF_TAG_TIFF, 2); | |
| 211 | |
| 212 bw.forward('image-dir', 4); // The pointer should point right after itself. | |
| 213 bw.resolveOffset('image-dir'); | |
| 214 | |
| 215 ExifEncoder.encodeDirectory(bw, this.ifd_.image, | |
| 216 [EXIF_TAG_EXIFDATA, EXIF_TAG_GPSDATA], 'thumb-dir'); | |
| 217 | |
| 218 if (this.ifd_.exif) { | |
| 219 bw.resolveOffset(EXIF_TAG_EXIFDATA); | |
| 220 ExifEncoder.encodeDirectory(bw, this.ifd_.exif); | |
| 221 } else { | |
| 222 if (EXIF_TAG_EXIFDATA in this.ifd_.image) | |
| 223 throw new Error('Corrupt exif dictionary reference'); | |
| 224 } | |
| 225 | |
| 226 if (this.ifd_.gps) { | |
| 227 bw.resolveOffset(EXIF_TAG_GPSDATA); | |
| 228 ExifEncoder.encodeDirectory(bw, this.ifd_.gps); | |
| 229 } else { | |
| 230 if (EXIF_TAG_GPSDATA in this.ifd_.image) | |
| 231 throw new Error('Missing gps dictionary reference'); | |
| 232 } | |
| 233 | |
| 234 if (this.ifd_.thumbnail) { | |
| 235 bw.resolveOffset('thumb-dir'); | |
| 236 ExifEncoder.encodeDirectory( | |
| 237 bw, | |
| 238 this.ifd_.thumbnail, | |
| 239 [EXIF_TAG_JPG_THUMB_OFFSET, EXIF_TAG_JPG_THUMB_LENGTH]); | |
| 240 | |
| 241 var thumbnailDecoded = | |
| 242 ImageEncoder.decodeDataURL(this.metadata_.thumbnailURL); | |
| 243 bw.resolveOffset(EXIF_TAG_JPG_THUMB_OFFSET); | |
| 244 bw.resolve(EXIF_TAG_JPG_THUMB_LENGTH, thumbnailDecoded.length); | |
| 245 bw.writeString(thumbnailDecoded); | |
| 246 } else { | |
| 247 bw.resolve('thumb-dir', 0); | |
| 248 } | |
| 249 | |
| 250 bw.checkResolved(); | |
| 251 | |
| 252 var totalSize = HEADER_SIZE + bw.tell(); | |
| 253 hw.resolve('size', totalSize - 2); // The marker is excluded. | |
| 254 hw.checkResolved(); | |
| 255 | |
| 256 var subarray = new Uint8Array(totalSize); | |
| 257 for (var i = 0; i != totalSize; i++) { | |
| 258 subarray[i] = bytes[i]; | |
| 259 } | |
| 260 return subarray.buffer; | |
| 261 }; | |
| 262 | |
| 263 /* | |
| 264 * Static methods. | |
| 265 */ | |
| 266 | |
| 267 /** | |
| 268 * Write the contents of an IFD directory. | |
| 269 * @param {ByteWriter} bw ByteWriter to use. | |
| 270 * @param {Object} directory A directory map as created by ExifParser. | |
| 271 * @param {Array} resolveLater An array of tag ids for which the values will be | |
| 272 * resolved later. | |
| 273 * @param {string} nextDirPointer A forward key for the pointer to the next | |
| 274 * directory. If omitted the pointer is set to 0. | |
| 275 */ | |
| 276 ExifEncoder.encodeDirectory = function( | |
| 277 bw, directory, resolveLater, nextDirPointer) { | |
| 278 | |
| 279 var longValues = []; | |
| 280 | |
| 281 bw.forward('dir-count', 2); | |
| 282 var count = 0; | |
| 283 | |
| 284 for (var key in directory) { | |
| 285 var tag = directory[key]; | |
| 286 bw.writeScalar(tag.id, 2); | |
| 287 bw.writeScalar(tag.format, 2); | |
| 288 bw.writeScalar(tag.componentCount, 4); | |
| 289 | |
| 290 var width = ExifEncoder.getComponentWidth(tag) * tag.componentCount; | |
| 291 | |
| 292 if (resolveLater && (resolveLater.indexOf(tag.id) >= 0)) { | |
| 293 // The actual value depends on further computations. | |
| 294 if (tag.componentCount != 1 || width > 4) | |
| 295 throw new Error('Cannot forward the pointer for ' + tag.id); | |
| 296 bw.forward(tag.id, width); | |
| 297 } else if (width <= 4) { | |
| 298 // The value fits into 4 bytes, write it immediately. | |
| 299 ExifEncoder.writeValue(bw, tag); | |
| 300 } else { | |
| 301 // The value does not fit, forward the 4 byte offset to the actual value. | |
| 302 width = 4; | |
| 303 bw.forward(tag.id, width); | |
| 304 longValues.push(tag); | |
| 305 } | |
| 306 bw.skip(4 - width); // Align so that the value take up exactly 4 bytes. | |
| 307 count++; | |
| 308 } | |
| 309 | |
| 310 bw.resolve('dir-count', count); | |
| 311 | |
| 312 if (nextDirPointer) { | |
| 313 bw.forward(nextDirPointer, 4); | |
| 314 } else { | |
| 315 bw.writeScalar(0, 4); | |
| 316 } | |
| 317 | |
| 318 // Write out the long values and resolve pointers. | |
| 319 for (var i = 0; i != longValues.length; i++) { | |
| 320 var longValue = longValues[i]; | |
| 321 bw.resolveOffset(longValue.id); | |
| 322 ExifEncoder.writeValue(bw, longValue); | |
| 323 } | |
| 324 }; | |
| 325 | |
| 326 /** | |
| 327 * @param {{format:number, id:number}} tag EXIF tag object. | |
| 328 * @return {number} Width in bytes of the data unit associated with this tag. | |
| 329 * TODO(kaznacheev): Share with ExifParser? | |
| 330 */ | |
| 331 ExifEncoder.getComponentWidth = function(tag) { | |
| 332 switch (tag.format) { | |
| 333 case 1: // Byte | |
| 334 case 2: // String | |
| 335 case 7: // Undefined | |
| 336 return 1; | |
| 337 | |
| 338 case 3: // Short | |
| 339 return 2; | |
| 340 | |
| 341 case 4: // Long | |
| 342 case 9: // Signed Long | |
| 343 return 4; | |
| 344 | |
| 345 case 5: // Rational | |
| 346 case 10: // Signed Rational | |
| 347 return 8; | |
| 348 | |
| 349 default: // ??? | |
| 350 console.warn('Unknown tag format 0x' + | |
| 351 Number(tag.id).toString(16) + ': ' + tag.format); | |
| 352 return 4; | |
| 353 } | |
| 354 }; | |
| 355 | |
| 356 /** | |
| 357 * Writes out the tag value. | |
| 358 * @param {ByteWriter} bw Writer to use. | |
| 359 * @param {Object} tag Tag, which value to write. | |
| 360 */ | |
| 361 ExifEncoder.writeValue = function(bw, tag) { | |
| 362 if (tag.format == 2) { // String | |
| 363 if (tag.componentCount != tag.value.length) { | |
| 364 throw new Error( | |
| 365 'String size mismatch for 0x' + Number(tag.id).toString(16)); | |
| 366 } | |
| 367 bw.writeString(tag.value); | |
| 368 } else { // Scalar or rational | |
| 369 var width = ExifEncoder.getComponentWidth(tag); | |
| 370 | |
| 371 var writeComponent = function(value, signed) { | |
| 372 if (width == 8) { | |
| 373 bw.writeScalar(value[0], 4, signed); | |
| 374 bw.writeScalar(value[1], 4, signed); | |
| 375 } else { | |
| 376 bw.writeScalar(value, width, signed); | |
| 377 } | |
| 378 }; | |
| 379 | |
| 380 var signed = (tag.format == 9 || tag.format == 10); | |
| 381 if (tag.componentCount == 1) { | |
| 382 writeComponent(tag.value, signed); | |
| 383 } else { | |
| 384 for (var i = 0; i != tag.componentCount; i++) { | |
| 385 writeComponent(tag.value[i], signed); | |
| 386 } | |
| 387 } | |
| 388 } | |
| 389 }; | |
| 390 | |
| 391 /** | |
| 392 * @param {{Object.<number,Object>}} directory EXIF directory. | |
| 393 * @param {number} id Tag id. | |
| 394 * @param {number} format Tag format | |
| 395 * (used in {@link ExifEncoder#getComponentWidth}). | |
| 396 * @param {number} componentCount Number of components in this tag. | |
| 397 * @return {{id:number, format:number, componentCount:number}} | |
| 398 * Tag found or created. | |
| 399 */ | |
| 400 ExifEncoder.findOrCreateTag = function(directory, id, format, componentCount) { | |
| 401 if (!(id in directory)) { | |
| 402 directory[id] = { | |
| 403 id: id, | |
| 404 format: format || 3, // Short | |
| 405 componentCount: componentCount || 1 | |
| 406 }; | |
| 407 } | |
| 408 return directory[id]; | |
| 409 }; | |
| 410 | |
| 411 /** | |
| 412 * ByteWriter class. | |
| 413 * @param {ArrayBuffer} arrayBuffer Underlying buffer to use. | |
| 414 * @param {number} offset Offset at which to start writing. | |
| 415 * @param {number} length Maximum length to use. | |
| 416 * @class | |
| 417 * @constructor | |
| 418 */ | |
| 419 function ByteWriter(arrayBuffer, offset, length) { | |
| 420 length = length || (arrayBuffer.byteLength - offset); | |
| 421 this.view_ = new DataView(arrayBuffer, offset, length); | |
| 422 this.littleEndian_ = false; | |
| 423 this.pos_ = 0; | |
| 424 this.forwards_ = {}; | |
| 425 } | |
| 426 | |
| 427 /** | |
| 428 * Little endian byte order. | |
| 429 * @type {number} | |
| 430 */ | |
| 431 ByteWriter.LITTLE_ENDIAN = 0; | |
| 432 | |
| 433 /** | |
| 434 * Bug endian byte order. | |
| 435 * @type {number} | |
| 436 */ | |
| 437 ByteWriter.BIG_ENDIAN = 1; | |
| 438 | |
| 439 /** | |
| 440 * Set the byte ordering for future writes. | |
| 441 * @param {number} order ByteOrder to use {ByteWriter.LITTLE_ENDIAN} | |
| 442 * or {ByteWriter.BIG_ENDIAN}. | |
| 443 */ | |
| 444 ByteWriter.prototype.setByteOrder = function(order) { | |
| 445 this.littleEndian_ = (order == ByteWriter.LITTLE_ENDIAN); | |
| 446 }; | |
| 447 | |
| 448 /** | |
| 449 * @return {number} the current write position. | |
| 450 */ | |
| 451 ByteWriter.prototype.tell = function() { return this.pos_ }; | |
| 452 | |
| 453 /** | |
| 454 * Skips desired amount of bytes in output stream. | |
| 455 * @param {number} count Byte count to skip. | |
| 456 */ | |
| 457 ByteWriter.prototype.skip = function(count) { | |
| 458 this.validateWrite(count); | |
| 459 this.pos_ += count; | |
| 460 }; | |
| 461 | |
| 462 /** | |
| 463 * Check if the buffer has enough room to read 'width' bytes. Throws an error | |
| 464 * if it has not. | |
| 465 * @param {number} width Amount of bytes to check. | |
| 466 */ | |
| 467 ByteWriter.prototype.validateWrite = function(width) { | |
| 468 if (this.pos_ + width > this.view_.byteLength) | |
| 469 throw new Error('Writing past the end of the buffer'); | |
| 470 }; | |
| 471 | |
| 472 /** | |
| 473 * Writes scalar value to output stream. | |
| 474 * @param {number} value Value to write. | |
| 475 * @param {number} width Desired width of written value. | |
| 476 * @param {boolean=} opt_signed True if value represents signed number. | |
| 477 */ | |
| 478 ByteWriter.prototype.writeScalar = function(value, width, opt_signed) { | |
| 479 var method; | |
| 480 // The below switch is so verbose for two reasons: | |
| 481 // 1. V8 is faster on method names which are 'symbols'. | |
| 482 // 2. Method names are discoverable by full text search. | |
| 483 switch (width) { | |
| 484 case 1: | |
| 485 method = opt_signed ? 'setInt8' : 'setUint8'; | |
| 486 break; | |
| 487 | |
| 488 case 2: | |
| 489 method = opt_signed ? 'setInt16' : 'setUint16'; | |
| 490 break; | |
| 491 | |
| 492 case 4: | |
| 493 method = opt_signed ? 'setInt32' : 'setUint32'; | |
| 494 break; | |
| 495 | |
| 496 case 8: | |
| 497 method = opt_signed ? 'setInt64' : 'setUint64'; | |
| 498 break; | |
| 499 | |
| 500 default: | |
| 501 throw new Error('Invalid width: ' + width); | |
| 502 break; | |
| 503 } | |
| 504 | |
| 505 this.validateWrite(width); | |
| 506 this.view_[method](this.pos_, value, this.littleEndian_); | |
| 507 this.pos_ += width; | |
| 508 }; | |
| 509 | |
| 510 /** | |
| 511 * Writes string. | |
| 512 * @param {string} str String to write. | |
| 513 */ | |
| 514 ByteWriter.prototype.writeString = function(str) { | |
| 515 this.validateWrite(str.length); | |
| 516 for (var i = 0; i != str.length; i++) { | |
| 517 this.view_.setUint8(this.pos_++, str.charCodeAt(i)); | |
| 518 } | |
| 519 }; | |
| 520 | |
| 521 /** | |
| 522 * Allocate the space for 'width' bytes for the value that will be set later. | |
| 523 * To be followed by a 'resolve' call with the same key. | |
| 524 * @param {string} key A key to identify the value. | |
| 525 * @param {number} width Width of the value in bytes. | |
| 526 */ | |
| 527 ByteWriter.prototype.forward = function(key, width) { | |
| 528 if (key in this.forwards_) | |
| 529 throw new Error('Duplicate forward key ' + key); | |
| 530 this.validateWrite(width); | |
| 531 this.forwards_[key] = { | |
| 532 pos: this.pos_, | |
| 533 width: width | |
| 534 }; | |
| 535 this.pos_ += width; | |
| 536 }; | |
| 537 | |
| 538 /** | |
| 539 * Set the value previously allocated with a 'forward' call. | |
| 540 * @param {string} key A key to identify the value. | |
| 541 * @param {number} value value to write in pre-allocated space. | |
| 542 */ | |
| 543 ByteWriter.prototype.resolve = function(key, value) { | |
| 544 if (!(key in this.forwards_)) | |
| 545 throw new Error('Undeclared forward key ' + key.toString(16)); | |
| 546 var forward = this.forwards_[key]; | |
| 547 var curPos = this.pos_; | |
| 548 this.pos_ = forward.pos; | |
| 549 this.writeScalar(value, forward.width); | |
| 550 this.pos_ = curPos; | |
| 551 delete this.forwards_[key]; | |
| 552 }; | |
| 553 | |
| 554 /** | |
| 555 * A shortcut to resolve the value to the current write position. | |
| 556 * @param {string} key A key to identify pre-allocated position. | |
| 557 */ | |
| 558 ByteWriter.prototype.resolveOffset = function(key) { | |
| 559 this.resolve(key, this.tell()); | |
| 560 }; | |
| 561 | |
| 562 /** | |
| 563 * Check if every forward has been resolved, throw and error if not. | |
| 564 */ | |
| 565 ByteWriter.prototype.checkResolved = function() { | |
| 566 for (var key in this.forwards_) { | |
| 567 throw new Error('Unresolved forward pointer ' + key.toString(16)); | |
| 568 } | |
| 569 }; | |
| OLD | NEW |