| Index: chrome/browser/resources/file_manager/js/image_editor/exif_encoder.js
|
| diff --git a/chrome/browser/resources/file_manager/js/image_editor/exif_encoder.js b/chrome/browser/resources/file_manager/js/image_editor/exif_encoder.js
|
| deleted file mode 100644
|
| index e81b8fba899399f8ef600ccbc5d932c1c7057442..0000000000000000000000000000000000000000
|
| --- a/chrome/browser/resources/file_manager/js/image_editor/exif_encoder.js
|
| +++ /dev/null
|
| @@ -1,569 +0,0 @@
|
| -// Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| -
|
| -'use strict';
|
| -
|
| -// TODO:(kaznacheev) Share the EXIF constants with exif_parser.js
|
| -var EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data).
|
| -var EXIF_MARK_SOI = 0xffd8; // Start of image data.
|
| -var EXIF_MARK_EOI = 0xffd9; // End of image data.
|
| -
|
| -var EXIF_MARK_APP0 = 0xffe0; // APP0 block, most commonly JFIF data.
|
| -var EXIF_MARK_EXIF = 0xffe1; // Start of exif block.
|
| -
|
| -var EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data.
|
| -var EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data.
|
| -
|
| -var EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data.
|
| -var EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory.
|
| -var EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD.
|
| -
|
| -var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail.
|
| -var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data.
|
| -
|
| -var EXIF_TAG_IMAGE_WIDTH = 0x0100;
|
| -var EXIF_TAG_IMAGE_HEIGHT = 0x0101;
|
| -
|
| -var EXIF_TAG_ORIENTATION = 0x0112;
|
| -var EXIF_TAG_X_DIMENSION = 0xA002;
|
| -var EXIF_TAG_Y_DIMENSION = 0xA003;
|
| -
|
| -/**
|
| - * The Exif metadata encoder.
|
| - * Uses the metadata format as defined by ExifParser.
|
| - * @param {Object} original_metadata Metadata to encode.
|
| - * @constructor
|
| - * @extends {ImageEncoder.MetadataEncoder}
|
| - */
|
| -function ExifEncoder(original_metadata) {
|
| - ImageEncoder.MetadataEncoder.apply(this, arguments);
|
| -
|
| - this.ifd_ = this.metadata_.ifd;
|
| - if (!this.ifd_)
|
| - this.ifd_ = this.metadata_.ifd = {};
|
| -}
|
| -
|
| -ExifEncoder.prototype = {__proto__: ImageEncoder.MetadataEncoder.prototype};
|
| -
|
| -ImageEncoder.registerMetadataEncoder(ExifEncoder, 'image/jpeg');
|
| -
|
| -/**
|
| - * @param {HTMLCanvasElement|Object} canvas Canvas or anything with
|
| - * width and height properties.
|
| - */
|
| -ExifEncoder.prototype.setImageData = function(canvas) {
|
| - var image = this.ifd_.image;
|
| - if (!image)
|
| - image = this.ifd_.image = {};
|
| -
|
| - // Only update width/height in this directory if they are present.
|
| - if (image[EXIF_TAG_IMAGE_WIDTH] && image[EXIF_TAG_IMAGE_HEIGHT]) {
|
| - image[EXIF_TAG_IMAGE_WIDTH].value = canvas.width;
|
| - image[EXIF_TAG_IMAGE_HEIGHT].value = canvas.height;
|
| - }
|
| -
|
| - var exif = this.ifd_.exif;
|
| - if (!exif)
|
| - exif = this.ifd_.exif = {};
|
| - ExifEncoder.findOrCreateTag(image, EXIF_TAG_EXIFDATA);
|
| - ExifEncoder.findOrCreateTag(exif, EXIF_TAG_X_DIMENSION).value = canvas.width;
|
| - ExifEncoder.findOrCreateTag(exif, EXIF_TAG_Y_DIMENSION).value = canvas.height;
|
| -
|
| - this.metadata_.width = canvas.width;
|
| - this.metadata_.height = canvas.height;
|
| -
|
| - // Always save in default orientation.
|
| - delete this.metadata_.imageTransform;
|
| - ExifEncoder.findOrCreateTag(image, EXIF_TAG_ORIENTATION).value = 1;
|
| -};
|
| -
|
| -
|
| -/**
|
| - * @param {HTMLCanvasElement} canvas Thumbnail canvas.
|
| - * @param {number} quality (0..1] Thumbnail encoding quality.
|
| - */
|
| -ExifEncoder.prototype.setThumbnailData = function(canvas, quality) {
|
| - // Empirical formula with reasonable behavior:
|
| - // 10K for 1Mpix, 30K for 5Mpix, 50K for 9Mpix and up.
|
| - var pixelCount = this.metadata_.width * this.metadata_.height;
|
| - var maxEncodedSize = 5000 * Math.min(10, 1 + pixelCount / 1000000);
|
| -
|
| - var DATA_URL_PREFIX = 'data:' + this.mimeType + ';base64,';
|
| - var BASE64_BLOAT = 4 / 3;
|
| - var maxDataURLLength =
|
| - DATA_URL_PREFIX.length + Math.ceil(maxEncodedSize * BASE64_BLOAT);
|
| -
|
| - for (;; quality *= 0.8) {
|
| - ImageEncoder.MetadataEncoder.prototype.setThumbnailData.call(
|
| - this, canvas, quality);
|
| - if (this.metadata_.thumbnailURL.length <= maxDataURLLength || quality < 0.2)
|
| - break;
|
| - }
|
| -
|
| - if (this.metadata_.thumbnailURL.length <= maxDataURLLength) {
|
| - var thumbnail = this.ifd_.thumbnail;
|
| - if (!thumbnail)
|
| - thumbnail = this.ifd_.thumbnail = {};
|
| -
|
| - ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_IMAGE_WIDTH).value =
|
| - canvas.width;
|
| -
|
| - ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_IMAGE_HEIGHT).value =
|
| - canvas.height;
|
| -
|
| - // The values for these tags will be set in ExifWriter.encode.
|
| - ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_JPG_THUMB_OFFSET);
|
| - ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_JPG_THUMB_LENGTH);
|
| -
|
| - // Always save in default orientation.
|
| - ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_ORIENTATION).value = 1;
|
| - } else {
|
| - console.warn(
|
| - 'Thumbnail URL too long: ' + this.metadata_.thumbnailURL.length);
|
| - // Delete thumbnail ifd so that it is not written out to a file, but
|
| - // keep thumbnailURL for display purposes.
|
| - if (this.ifd_.thumbnail) {
|
| - delete this.ifd_.thumbnail;
|
| - }
|
| - }
|
| - delete this.metadata_.thumbnailTransform;
|
| -};
|
| -
|
| -/**
|
| - * Return a range where the metadata is (or should be) located.
|
| - * @param {string} encodedImage Raw image data to look for metadata.
|
| - * @return {Object} An object with from and to properties.
|
| - */
|
| -ExifEncoder.prototype.findInsertionRange = function(encodedImage) {
|
| - function getWord(pos) {
|
| - if (pos + 2 > encodedImage.length)
|
| - throw 'Reading past the buffer end @' + pos;
|
| - return encodedImage.charCodeAt(pos) << 8 | encodedImage.charCodeAt(pos + 1);
|
| - }
|
| -
|
| - if (getWord(0) != EXIF_MARK_SOI)
|
| - throw new Error('Jpeg data starts from 0x' + getWord(0).toString(16));
|
| -
|
| - var sectionStart = 2;
|
| -
|
| - // Default: an empty range right after SOI.
|
| - // Will be returned in absence of APP0 or Exif sections.
|
| - var range = {from: sectionStart, to: sectionStart};
|
| -
|
| - for (;;) {
|
| - var tag = getWord(sectionStart);
|
| -
|
| - if (tag == EXIF_MARK_SOS)
|
| - break;
|
| -
|
| - var nextSectionStart = sectionStart + 2 + getWord(sectionStart + 2);
|
| - if (nextSectionStart <= sectionStart ||
|
| - nextSectionStart > encodedImage.length)
|
| - throw new Error('Invalid section size in jpeg data');
|
| -
|
| - if (tag == EXIF_MARK_APP0) {
|
| - // Assert that we have not seen the Exif section yet.
|
| - if (range.from != range.to)
|
| - throw new Error('APP0 section found after EXIF section');
|
| - // An empty range right after the APP0 segment.
|
| - range.from = range.to = nextSectionStart;
|
| - } else if (tag == EXIF_MARK_EXIF) {
|
| - // A range containing the existing EXIF section.
|
| - range.from = sectionStart;
|
| - range.to = nextSectionStart;
|
| - }
|
| - sectionStart = nextSectionStart;
|
| - }
|
| -
|
| - return range;
|
| -};
|
| -
|
| -/**
|
| - * @return {ArrayBuffer} serialized metadata ready to write to an image file.
|
| - */
|
| -ExifEncoder.prototype.encode = function() {
|
| - var HEADER_SIZE = 10;
|
| -
|
| - // Allocate the largest theoretically possible size.
|
| - var bytes = new Uint8Array(0x10000);
|
| -
|
| - // Serialize header
|
| - var hw = new ByteWriter(bytes.buffer, 0, HEADER_SIZE);
|
| - hw.writeScalar(EXIF_MARK_EXIF, 2);
|
| - hw.forward('size', 2);
|
| - hw.writeString('Exif\0\0'); // Magic string.
|
| -
|
| - // First serialize the content of the exif section.
|
| - // Use a ByteWriter starting at HEADER_SIZE offset so that tell() positions
|
| - // can be directly mapped to offsets as encoded in the dictionaries.
|
| - var bw = new ByteWriter(bytes.buffer, HEADER_SIZE);
|
| -
|
| - if (this.metadata_.littleEndian) {
|
| - bw.setByteOrder(ByteWriter.LITTLE_ENDIAN);
|
| - bw.writeScalar(EXIF_ALIGN_LITTLE, 2);
|
| - } else {
|
| - bw.setByteOrder(ByteWriter.BIG_ENDIAN);
|
| - bw.writeScalar(EXIF_ALIGN_BIG, 2);
|
| - }
|
| -
|
| - bw.writeScalar(EXIF_TAG_TIFF, 2);
|
| -
|
| - bw.forward('image-dir', 4); // The pointer should point right after itself.
|
| - bw.resolveOffset('image-dir');
|
| -
|
| - ExifEncoder.encodeDirectory(bw, this.ifd_.image,
|
| - [EXIF_TAG_EXIFDATA, EXIF_TAG_GPSDATA], 'thumb-dir');
|
| -
|
| - if (this.ifd_.exif) {
|
| - bw.resolveOffset(EXIF_TAG_EXIFDATA);
|
| - ExifEncoder.encodeDirectory(bw, this.ifd_.exif);
|
| - } else {
|
| - if (EXIF_TAG_EXIFDATA in this.ifd_.image)
|
| - throw new Error('Corrupt exif dictionary reference');
|
| - }
|
| -
|
| - if (this.ifd_.gps) {
|
| - bw.resolveOffset(EXIF_TAG_GPSDATA);
|
| - ExifEncoder.encodeDirectory(bw, this.ifd_.gps);
|
| - } else {
|
| - if (EXIF_TAG_GPSDATA in this.ifd_.image)
|
| - throw new Error('Missing gps dictionary reference');
|
| - }
|
| -
|
| - if (this.ifd_.thumbnail) {
|
| - bw.resolveOffset('thumb-dir');
|
| - ExifEncoder.encodeDirectory(
|
| - bw,
|
| - this.ifd_.thumbnail,
|
| - [EXIF_TAG_JPG_THUMB_OFFSET, EXIF_TAG_JPG_THUMB_LENGTH]);
|
| -
|
| - var thumbnailDecoded =
|
| - ImageEncoder.decodeDataURL(this.metadata_.thumbnailURL);
|
| - bw.resolveOffset(EXIF_TAG_JPG_THUMB_OFFSET);
|
| - bw.resolve(EXIF_TAG_JPG_THUMB_LENGTH, thumbnailDecoded.length);
|
| - bw.writeString(thumbnailDecoded);
|
| - } else {
|
| - bw.resolve('thumb-dir', 0);
|
| - }
|
| -
|
| - bw.checkResolved();
|
| -
|
| - var totalSize = HEADER_SIZE + bw.tell();
|
| - hw.resolve('size', totalSize - 2); // The marker is excluded.
|
| - hw.checkResolved();
|
| -
|
| - var subarray = new Uint8Array(totalSize);
|
| - for (var i = 0; i != totalSize; i++) {
|
| - subarray[i] = bytes[i];
|
| - }
|
| - return subarray.buffer;
|
| -};
|
| -
|
| -/*
|
| - * Static methods.
|
| - */
|
| -
|
| -/**
|
| - * Write the contents of an IFD directory.
|
| - * @param {ByteWriter} bw ByteWriter to use.
|
| - * @param {Object} directory A directory map as created by ExifParser.
|
| - * @param {Array} resolveLater An array of tag ids for which the values will be
|
| - * resolved later.
|
| - * @param {string} nextDirPointer A forward key for the pointer to the next
|
| - * directory. If omitted the pointer is set to 0.
|
| - */
|
| -ExifEncoder.encodeDirectory = function(
|
| - bw, directory, resolveLater, nextDirPointer) {
|
| -
|
| - var longValues = [];
|
| -
|
| - bw.forward('dir-count', 2);
|
| - var count = 0;
|
| -
|
| - for (var key in directory) {
|
| - var tag = directory[key];
|
| - bw.writeScalar(tag.id, 2);
|
| - bw.writeScalar(tag.format, 2);
|
| - bw.writeScalar(tag.componentCount, 4);
|
| -
|
| - var width = ExifEncoder.getComponentWidth(tag) * tag.componentCount;
|
| -
|
| - if (resolveLater && (resolveLater.indexOf(tag.id) >= 0)) {
|
| - // The actual value depends on further computations.
|
| - if (tag.componentCount != 1 || width > 4)
|
| - throw new Error('Cannot forward the pointer for ' + tag.id);
|
| - bw.forward(tag.id, width);
|
| - } else if (width <= 4) {
|
| - // The value fits into 4 bytes, write it immediately.
|
| - ExifEncoder.writeValue(bw, tag);
|
| - } else {
|
| - // The value does not fit, forward the 4 byte offset to the actual value.
|
| - width = 4;
|
| - bw.forward(tag.id, width);
|
| - longValues.push(tag);
|
| - }
|
| - bw.skip(4 - width); // Align so that the value take up exactly 4 bytes.
|
| - count++;
|
| - }
|
| -
|
| - bw.resolve('dir-count', count);
|
| -
|
| - if (nextDirPointer) {
|
| - bw.forward(nextDirPointer, 4);
|
| - } else {
|
| - bw.writeScalar(0, 4);
|
| - }
|
| -
|
| - // Write out the long values and resolve pointers.
|
| - for (var i = 0; i != longValues.length; i++) {
|
| - var longValue = longValues[i];
|
| - bw.resolveOffset(longValue.id);
|
| - ExifEncoder.writeValue(bw, longValue);
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * @param {{format:number, id:number}} tag EXIF tag object.
|
| - * @return {number} Width in bytes of the data unit associated with this tag.
|
| - * TODO(kaznacheev): Share with ExifParser?
|
| - */
|
| -ExifEncoder.getComponentWidth = function(tag) {
|
| - switch (tag.format) {
|
| - case 1: // Byte
|
| - case 2: // String
|
| - case 7: // Undefined
|
| - return 1;
|
| -
|
| - case 3: // Short
|
| - return 2;
|
| -
|
| - case 4: // Long
|
| - case 9: // Signed Long
|
| - return 4;
|
| -
|
| - case 5: // Rational
|
| - case 10: // Signed Rational
|
| - return 8;
|
| -
|
| - default: // ???
|
| - console.warn('Unknown tag format 0x' +
|
| - Number(tag.id).toString(16) + ': ' + tag.format);
|
| - return 4;
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Writes out the tag value.
|
| - * @param {ByteWriter} bw Writer to use.
|
| - * @param {Object} tag Tag, which value to write.
|
| - */
|
| -ExifEncoder.writeValue = function(bw, tag) {
|
| - if (tag.format == 2) { // String
|
| - if (tag.componentCount != tag.value.length) {
|
| - throw new Error(
|
| - 'String size mismatch for 0x' + Number(tag.id).toString(16));
|
| - }
|
| - bw.writeString(tag.value);
|
| - } else { // Scalar or rational
|
| - var width = ExifEncoder.getComponentWidth(tag);
|
| -
|
| - var writeComponent = function(value, signed) {
|
| - if (width == 8) {
|
| - bw.writeScalar(value[0], 4, signed);
|
| - bw.writeScalar(value[1], 4, signed);
|
| - } else {
|
| - bw.writeScalar(value, width, signed);
|
| - }
|
| - };
|
| -
|
| - var signed = (tag.format == 9 || tag.format == 10);
|
| - if (tag.componentCount == 1) {
|
| - writeComponent(tag.value, signed);
|
| - } else {
|
| - for (var i = 0; i != tag.componentCount; i++) {
|
| - writeComponent(tag.value[i], signed);
|
| - }
|
| - }
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * @param {{Object.<number,Object>}} directory EXIF directory.
|
| - * @param {number} id Tag id.
|
| - * @param {number} format Tag format
|
| - * (used in {@link ExifEncoder#getComponentWidth}).
|
| - * @param {number} componentCount Number of components in this tag.
|
| - * @return {{id:number, format:number, componentCount:number}}
|
| - * Tag found or created.
|
| - */
|
| -ExifEncoder.findOrCreateTag = function(directory, id, format, componentCount) {
|
| - if (!(id in directory)) {
|
| - directory[id] = {
|
| - id: id,
|
| - format: format || 3, // Short
|
| - componentCount: componentCount || 1
|
| - };
|
| - }
|
| - return directory[id];
|
| -};
|
| -
|
| -/**
|
| - * ByteWriter class.
|
| - * @param {ArrayBuffer} arrayBuffer Underlying buffer to use.
|
| - * @param {number} offset Offset at which to start writing.
|
| - * @param {number} length Maximum length to use.
|
| - * @class
|
| - * @constructor
|
| - */
|
| -function ByteWriter(arrayBuffer, offset, length) {
|
| - length = length || (arrayBuffer.byteLength - offset);
|
| - this.view_ = new DataView(arrayBuffer, offset, length);
|
| - this.littleEndian_ = false;
|
| - this.pos_ = 0;
|
| - this.forwards_ = {};
|
| -}
|
| -
|
| -/**
|
| - * Little endian byte order.
|
| - * @type {number}
|
| - */
|
| -ByteWriter.LITTLE_ENDIAN = 0;
|
| -
|
| -/**
|
| - * Bug endian byte order.
|
| - * @type {number}
|
| - */
|
| -ByteWriter.BIG_ENDIAN = 1;
|
| -
|
| -/**
|
| - * Set the byte ordering for future writes.
|
| - * @param {number} order ByteOrder to use {ByteWriter.LITTLE_ENDIAN}
|
| - * or {ByteWriter.BIG_ENDIAN}.
|
| - */
|
| -ByteWriter.prototype.setByteOrder = function(order) {
|
| - this.littleEndian_ = (order == ByteWriter.LITTLE_ENDIAN);
|
| -};
|
| -
|
| -/**
|
| - * @return {number} the current write position.
|
| - */
|
| -ByteWriter.prototype.tell = function() { return this.pos_ };
|
| -
|
| -/**
|
| - * Skips desired amount of bytes in output stream.
|
| - * @param {number} count Byte count to skip.
|
| - */
|
| -ByteWriter.prototype.skip = function(count) {
|
| - this.validateWrite(count);
|
| - this.pos_ += count;
|
| -};
|
| -
|
| -/**
|
| - * Check if the buffer has enough room to read 'width' bytes. Throws an error
|
| - * if it has not.
|
| - * @param {number} width Amount of bytes to check.
|
| - */
|
| -ByteWriter.prototype.validateWrite = function(width) {
|
| - if (this.pos_ + width > this.view_.byteLength)
|
| - throw new Error('Writing past the end of the buffer');
|
| -};
|
| -
|
| -/**
|
| - * Writes scalar value to output stream.
|
| - * @param {number} value Value to write.
|
| - * @param {number} width Desired width of written value.
|
| - * @param {boolean=} opt_signed True if value represents signed number.
|
| - */
|
| -ByteWriter.prototype.writeScalar = function(value, width, opt_signed) {
|
| - var method;
|
| -// The below switch is so verbose for two reasons:
|
| -// 1. V8 is faster on method names which are 'symbols'.
|
| -// 2. Method names are discoverable by full text search.
|
| - switch (width) {
|
| - case 1:
|
| - method = opt_signed ? 'setInt8' : 'setUint8';
|
| - break;
|
| -
|
| - case 2:
|
| - method = opt_signed ? 'setInt16' : 'setUint16';
|
| - break;
|
| -
|
| - case 4:
|
| - method = opt_signed ? 'setInt32' : 'setUint32';
|
| - break;
|
| -
|
| - case 8:
|
| - method = opt_signed ? 'setInt64' : 'setUint64';
|
| - break;
|
| -
|
| - default:
|
| - throw new Error('Invalid width: ' + width);
|
| - break;
|
| - }
|
| -
|
| - this.validateWrite(width);
|
| - this.view_[method](this.pos_, value, this.littleEndian_);
|
| - this.pos_ += width;
|
| -};
|
| -
|
| -/**
|
| - * Writes string.
|
| - * @param {string} str String to write.
|
| - */
|
| -ByteWriter.prototype.writeString = function(str) {
|
| - this.validateWrite(str.length);
|
| - for (var i = 0; i != str.length; i++) {
|
| - this.view_.setUint8(this.pos_++, str.charCodeAt(i));
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Allocate the space for 'width' bytes for the value that will be set later.
|
| - * To be followed by a 'resolve' call with the same key.
|
| - * @param {string} key A key to identify the value.
|
| - * @param {number} width Width of the value in bytes.
|
| - */
|
| -ByteWriter.prototype.forward = function(key, width) {
|
| - if (key in this.forwards_)
|
| - throw new Error('Duplicate forward key ' + key);
|
| - this.validateWrite(width);
|
| - this.forwards_[key] = {
|
| - pos: this.pos_,
|
| - width: width
|
| - };
|
| - this.pos_ += width;
|
| -};
|
| -
|
| -/**
|
| - * Set the value previously allocated with a 'forward' call.
|
| - * @param {string} key A key to identify the value.
|
| - * @param {number} value value to write in pre-allocated space.
|
| - */
|
| -ByteWriter.prototype.resolve = function(key, value) {
|
| - if (!(key in this.forwards_))
|
| - throw new Error('Undeclared forward key ' + key.toString(16));
|
| - var forward = this.forwards_[key];
|
| - var curPos = this.pos_;
|
| - this.pos_ = forward.pos;
|
| - this.writeScalar(value, forward.width);
|
| - this.pos_ = curPos;
|
| - delete this.forwards_[key];
|
| -};
|
| -
|
| -/**
|
| - * A shortcut to resolve the value to the current write position.
|
| - * @param {string} key A key to identify pre-allocated position.
|
| - */
|
| -ByteWriter.prototype.resolveOffset = function(key) {
|
| - this.resolve(key, this.tell());
|
| -};
|
| -
|
| -/**
|
| - * Check if every forward has been resolved, throw and error if not.
|
| - */
|
| -ByteWriter.prototype.checkResolved = function() {
|
| - for (var key in this.forwards_) {
|
| - throw new Error('Unresolved forward pointer ' + key.toString(16));
|
| - }
|
| -};
|
|
|