Image upload pose and Typed Arrays you don't know

Keywords: Javascript JSON ascii Attribute

There are several questions about uploading pictures in the question of thinking or not, all of which involve the concept of ArrayBuffer. I want to sort out the knowledge in this area and hope that more people can get something.

Ladies and gentlemen, let's start together.

1. How to upload files

Generally, FormData is used to create request data for upload in the front end. The example is as follows:

var formData = new FormData();

formData.append("username", "Groucho");

// HTML file type input, user selected
formData.append("userfile", fileInputElement.files[0]);

// JavaScript file like object
var content = '<a id="a"><b id="b">hey!</b></a>'; // Body of new file
var blob = new Blob([content], { type: "text/xml"});

formData.append("webmasterfile", blob);

var request = new XMLHttpRequest();
request.open("POST", "http://foo.com/submitform.php");
request.send(formData);

The field type of a FormData object can be Blob, File, or string. If its field type is neither Blob nor File, it will be converted to string.

We use < input type = "input" / > to select the image, put the obtained file into FormData, and then submit it to the server.

If multiple files are uploaded, they are appended to the same field.

fileInputElement.files.forEach(file => {
  formData.append('userfile', file);
})

The examples of file like and new Blob show that we can construct a new file to upload directly.

Scene 1: clip image upload

We can get the data url or canvas by tailoring the library.

Take crowperjs as an example, use getcropped canvas to get canvas, then use its own toBlob to get file data, and then upload it through FormData.

The core code of transformation can refer to the following:

canvas = cropper.getCroppedCanvas({
  width: 160,
  height: 160,
});

initialAvatarURL = avatar.src;
avatar.src = canvas.toDataURL();

// Get blob data from canvs
canvas.toBlob(function (blob) {
  var formData = new FormData();
  formData.append('avatar', blob, 'avatar.jpg');
  
  // Now you can make a request
  makeRequest(formData)
})

Scenario 2: base64 image upload

After getting the base64 image, we can convert it to blob form through the following functions:

function btof(base64Data, fileName) {
  const dataArr = base64Data.split(",");
  const byteString = atob(dataArr[1]);

  const options = {
    type: "image/jpeg",
    endings: "native"
  };
  const u8Arr = new Uint8Array(byteString.length);
  for (let i = 0; i < byteString.length; i++) {
    u8Arr[i] = byteString.charCodeAt(i);
  }
  return new File([u8Arr], fileName + ".jpg", options);
}

So we get the file, and then we can upload it.

Scenario 3: URL image upload

If you want to upload directly with the image URL, we can do it in two parts:

  1. Get base64
  2. Then file

The key code is how to create a canvas from the URL. Here, by creating an Image object, after the Image is mounted, it is filled into the canvas.

var img =
  "https://Ss0.bdstatic.com/70cfuhsh q1ynxgkpowk1hf6hhy / it / u = 508387608248974022 & FM = 26 & GP = 0. JPG "; / / imgurl is your picture path
  
var image = new Image();
image.src = img;
image.setAttribute("crossOrigin", "Anonymous");
image.onload = function() {
  // Step 1: get a base64 image
  var base64 = getBase64Image(image);

  var formData = new FormData(); 

  // Step 2: convert base64 to file
  var file = btof(base64, "test");
  formData.append("imageName", file);
};

function getBase64Image(img) {
  var canvas = document.createElement("canvas");
  canvas.width = img.width;
  canvas.height = img.height;
  var ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0, img.width, img.height);
  var ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase();
  var dataURL = canvas.toDataURL("image/" + ext);

  return dataURL;
}

Class = "code" data height = "355" data theme id = "0" data default tab = "JS, result" data user = "ineo6" data slug hash = "mwgpgqz" data preview = "true" style = "height: 355px; box sizing: border box; display: Flex; align items: Center; justify content: Center; border: 2px solid; margin: 1eem 0; padding: 1eem;" data pen-pen-data-pen-pen-content; 1eem; "1eem;" 1eem; "1eem;" 1eem; "data pen-pen-pen-pen-data-pen-pen-pen-1;" 1eem; "1eem;" 1eem; "1eem;" 1eem; "data-pen-pen-pen-data-pen-pen-pen-pen-data-pen-pen-pen-pen-data-pen-pen-pen-data-pen-pen-pen-qz" title = "URL image to Base64" >
<span>See the Pen
url image to base64
by neo (@ineo6)
on CodePen.</span>
</p>
<script async src="https://static.codepen.io/ass...;></script>

2. thinking

Although we have solved the scenario mentioned above, it contains these keywords, which makes people think:

  • Blob
  • File
  • Uint8Array
  • ArrayBuffer
  • TypedArray
  • Base64
  • atob,btoa

These keywords all point to "file", "binary" and "encoding", which we usually don't pay much attention to.

Before using File and Blob, I had doubts.

What's the point of all this? Next let's take a look at the knowledge I've sorted out.

3. concept

3.1 Blob

Blob object represents an immutable, raw data class file object.

The File interface is also based on the Blob object, and is extended to support the File format of the user system.

3.1.1 create Blob object

To construct a blob from other non blob objects and data, use the Blob() constructor:

var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)], {type : 'application/json'});

3.1.1 read Blob object

Use FileReader to read the contents of Blob objects.

var reader = new FileReader();
reader.addEventListener("loadend", function() {
   //reader.result is the content
   console.log(reader.result)
});
reader.readAsArrayBuffer(blob);

3.1.1 Object URLs

Object URLs refer to addresses beginning with blob:, which can be used to display picture and text information.

This is similar to the display of base64 image, so we can also use it to preview the image.

The following code snippet is to turn the selected image into Object URLs.

function handleFiles(files) {
  if (!files.length) {
    fileList.innerHTML = "<p>No file!</p>";
  } else {
    fileList.innerHTML = "";
    var list = document.createElement("ul");
    fileList.appendChild(list);
    for (var i = 0; i < files.length; i++) {
      var li = document.createElement("li");
      list.appendChild(li);
      
      var img = document.createElement("img");
      // Create object url from file
      img.src = window.URL.createObjectURL(files[i]);
      img.height = 60;
      img.onload = function() {
        // Remember to release the object url after loading
        window.URL.revokeObjectURL(this.src);
      }
      li.appendChild(img);
      var info = document.createElement("span");
      info.innerHTML = files[i].name + ": " + files[i].size + " bytes";
      li.appendChild(info);
    }
  }
}

demo

3.2 Typed Arrays - typed arrays

A typed array is an array like object that provides access to raw binary data. But typed array and normal array are not the same class. The call to Array.isArray() will return false.

Typed Arrays has two parts:

  • Arraybuffer
  • Views (TypedArray and DataView)

3.2.1 ArrayBuffer

The ArrayBuffer object is used to represent a generic, fixed length raw binary data buffer.
ArrayBuffer cannot be operated directly, but through TypedArray or DataView objects. They represent the data in the buffer in specific formats, and read and write the contents of the buffer through these formats.

ArrayBuffer is mainly used to access binary data efficiently and quickly, such as the data used by WebGL, Canvas 2D or Web Audio.

Let's take a look at TypedArray.

3.2.2 TypedArray

TypedArray can build views based on different data types on the ArrayBuffer object.

// Create an 8-byte ArrayBuffer
const b = new ArrayBuffer(8);

// Create an Int32 view pointing to b, starting at byte 0 and ending at the end of the buffer
const v1 = new Int32Array(b);

// Create a Uint8 view pointing to b, starting at byte 2 and ending at the end of the buffer
const v2 = new Uint8Array(b, 2);

// Create an Int16 view pointing to b, starting at byte 2, with a length of 2
const v3 = new Int16Array(b, 2, 2);

Int32array and uint8array refer to TypedArray. TypedArray objects describe an array like view of the underlying binary data cache.

It has many members:

Int8Array(); 
Uint8Array(); 
Uint8ClampedArray();
Int16Array(); 
Uint16Array();
Int32Array(); 
Uint32Array(); 
Float32Array(); 
Float64Array();

And a little Chestnut:

var buffer = new ArrayBuffer(2) 
var bytes = new Uint8Array(buffer)

bytes[0] = 65 // ASCII for 'A'
bytes[1] = 66 // ASCII for 'B'

// View buffer content
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // AB

Byte order

In the above example, we write "A" first, then "B". Of course, we can also write two bytes through Uint16Array.

var buffer = new ArrayBuffer(2) // Two byte buffer
var word = new Uint16Array(buffer) // Access buffer with 16 bit integer

// Add 'A' to high and 'B' to low
var value = (65 << 8) + 66
word[0] = value

var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // BA

When you execute this code, why do you see "BA" instead of "AB"?

This is because of the existence of "byte order", namely, small byte order and large byte order.

For example, if a hexadecimal number 0x12345678 occupies four bytes, the most important byte determining its size is "12", and the least important is "78". Small endian byte order puts the least important byte first, and the storage order is 78563412; large endian byte order puts the most important byte first, and the storage order is 12345678.

Because browsers use small endian byte order, we see "BA". In order to solve the problem of byte order inconsistency, we can use DataView to set byte order.

TypedArray.prototype.buffer

The buffer property of the TypedArray instance, which returns the ArrayBuffer object corresponding to the whole memory area. The property is read-only.

const a = new Float32Array(64);
const b = new Uint8Array(a.buffer);

The a view object and b view object of the above code correspond to the same ArrayBuffer object, that is, the same memory segment.

TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset

The byteLength property returns the memory length occupied by the TypedArray array in bytes. The byteOffset property returns which byte of the underlying ArrayBuffer object the TypedArray array starts from. Both properties are read-only.

const b = new ArrayBuffer(8);

const v1 = new Int32Array(b);
const v2 = new Uint8Array(b, 2);
const v3 = new Int16Array(b, 2, 2);

v1.byteLength // 8
v2.byteLength // 6
v3.byteLength // 4

v1.byteOffset // 0
v2.byteOffset // 2
v3.byteOffset // 2
TypedArray.prototype.length

The length property indicates how many members the TypedArray array contains. Note the distinction between the length attribute, which is the member length and the byteLength attribute, which is the byte length.

const a = new Int16Array(8);

a.length // 8
a.byteLength // 16
TypedArray.prototype.set()

The set method of TypedArray array is used to copy an array (ordinary array or TypedArray array), that is, copy one piece of content completely to another piece of memory.

const a = new Uint8Array(8);
const b = new Uint8Array(8);

b.set(a);

The set method can also accept a second parameter that indicates which member of the b object to copy the a object from.

TypedArray.prototype.subarray()

The subarray method is to create a new view for a part of the TypedArray array.

const a = new Uint16Array(8);
const b = a.subarray(2,3);

a.byteLength // 16
b.byteLength // 2
TypedArray.prototype.slice()

slice method of TypeArray instance, which can return a new TypedArray instance at the specified location.

let ui8 = Uint8Array.of(0, 1, 2);
ui8.slice(-1)
// Uint8Array [ 2 ]
TypedArray.of()

All constructors of TypedArray arrays have a static method of, which is used to convert parameters to a TypedArray instance.

Float32Array.of(0.151, -8, 3.7)
// Float32Array [ 0.151, -8, 3.7 ]

The following three methods will generate the same TypedArray array.

// Method 1
let tarr = new Uint8Array([1,2,3]);

// Method two
let tarr = Uint8Array.of(1,2,3);

// Method three
let tarr = new Uint8Array(3);
tarr[0] = 1;
tarr[1] = 2;
tarr[2] = 3;
TypedArray.from()

The static method from takes a traversable data structure (such as an array) as a parameter and returns an instance of TypedArray based on this structure.

Uint16Array.from([0, 1, 2])
// Uint16Array [ 0, 1, 2 ]

This method can also convert one instance of TypedArray to another.

const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
ui16 instanceof Uint16Array // true

The from method can also accept a function as the second parameter to traverse each element, similar to the map method.

Int8Array.of(127, 126, 125).map(x => 2 * x)
// Int8Array [ -2, -4, -6 ]

Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
// Int16Array [ 254, 252, 250 ]

In the above example, the from method does not overflow, which indicates that the traversal is not aimed at the original 8-bit integer array. In other words, from will copy the TypedArray array specified by the first parameter to another section of memory, and then convert the result to the specified array format after processing.

Composite view

Because the view constructor can specify the starting position and length, different types of data can be stored in the same memory in turn, which is called "composite view".

const buffer = new ArrayBuffer(24);

const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);

The above code divides a 24 byte ArrayBuffer object into three parts:

  • Byte 0 to byte 3: 1 32-bit unsigned integer
  • Byte 4 to byte 19: 16 8-bit integers
  • Byte 20 to byte 23: 1 32-bit floating-point number

3.2.3 DataView - View

If a piece of data contains multiple types, we can also use the DataView view view to operate.

The DataView view provides eight methods to write to memory.

dataview.setXXX(byteOffset, value [, littleEndian])

  • byteOffset offset, in bytes
  • Value set value
  • littleEndian passed in false or undefined to use large byte order

setInt8: write an 8-bit integer of 1 byte.
setUint8: write an 8-bit unsigned integer of 1 byte.
setInt16: write a 2-byte 16 bit integer.
setUint16: write a 2-byte 16 bit unsigned integer.
setInt32: writes a 4-byte 32-bit integer.
setUint32: writes a 4-byte, 32-bit, unsigned integer.
setFloat32: writes a 4-byte 32-bit floating-point number.
setFloat64: writes a 64 bit floating-point number of 8 bytes.

There are also 8 ways to read memory:

getInt8: reads 1 byte and returns an 8-bit integer.
getUint8: reads 1 byte and returns an unsigned 8-bit integer.
getInt16: reads 2 bytes and returns a 16 bit integer.
getUint16: reads 2 bytes and returns an unsigned 16 bit integer.
getInt32: reads 4 bytes and returns a 32-bit integer.
getUint32: reads 4 bytes and returns an unsigned 32-bit integer.
getFloat32: reads 4 bytes and returns a 32-bit floating-point number.
getFloat64: reads 8 bytes and returns a 64 bit floating-point number.

Here is the header information of BMP file in the table:

Byte describe
2 "BM" mark
4 file size
2 Retain
2 Retain
4 Offset between file header and bitmap data

We can use DataView to implement this simply:

var buffer = new ArrayBuffer(14)
var view = new DataView(buffer)

view.setUint8(0, 66)     // Write 1 byte: 'B'
view.setUint8(1, 67)     // Write 1 byte: 'M'
view.setUint32(2, 1234)  // Write 4 bytes: 1234
view.setUint16(6, 0)     // Write 2-byte reserved bit
view.setUint16(8, 0)     // Write 2-byte reserved bit
view.setUint32(10, 0)    // Write 4-byte offset

The corresponding structure should be as follows:

Byte  |    0   |    1   |    2   |    3   |    4   |    5   | ... |
Type  |   I8   |   I8   |                I32                | ... |    
Data  |    B   |    M   |00000000|00000000|00000100|11010010| ... |

Back to the previous "BA" problem, we use DataView to re execute the following:

var buffer = new ArrayBuffer(2) 
var view = new DataView(buffer)

var value = (65 << 8) + 66
view.setUint16(0, value)

var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // AB

We get the correct result "AB", which also shows that DataView uses large byte order by default.

Reference articles

This article was published in the author's blog: Image upload pose and Typed Arrays you don't know

Posted by Mastodont on Sat, 02 Nov 2019 06:01:51 -0700