eligrey / Blob.js

An HTML5 Blob implementation
Other
1.15k stars 605 forks source link

Implement a FileReader? #33

Closed jimmywarting closed 6 years ago

jimmywarting commented 10 years ago

A way to read the blob data would be grate, whatever its underlying data is (raw, fakeblob, real blob, arraybuffer, whatever) especially for IE9.

var fakeBlob new Blob([arraybuffer, fakeBlob, "raw text", arraybufferView]);

I think its mandatory that you can read a blob if you can construct one.

var fr = new FileReader();
fr.readAsArrayBuffer(fakeBlob); // require typed array polyfill (not by you, but from the author)
fr.readAsText(fakeBlob);
fr.readAsDataURI(fakeBlob);
eligrey commented 10 years ago

URL.createObjectURL(fakeBlob) will return a data: URI and fakeBlob.data will contain the raw data you used to construct it.

fr.readAsArrayBuffer(fakeBlob); // require typed array polyfill (not by you, but from the author)

There is no point in making a typed array polyfill if it doesn't have any performance improvements. You might as well just use a normal array if you want to support IE9.

When would you ever use FileReader on blobs that you already have the creation data for? FileReader is more for blobs you didn't create (File objects (instanceof Blob) from file selectors).

I'll consider adding a FileReader, but just not at this moment.

jimmywarting commented 10 years ago

I didn't mean that you had to create a polyfill for arrayBuffer, since they already exist. You could just simply throw an error that "arrayBuffer isn't supported, if you like you could import this typed array library/polyfill to support readAsArrayBuffer..." In that way they can include what's nessecsary to save bytes.

one reason i could think of would be to download chunks of data and merge it into one blob. After that you could read it as text/dataURI/ArrayBuffer of your convenient choice. (a good way do convert the blob/data to another format). maybe you save it in some storage and decide you want to read the text later.

Another reason would be consistency. If you where to download a big file (say 800 mb) then the browser would start to hang if you load that in to the memory. however there is a way around it if you decided to download chunks file = new Blob([file, chunk]) I manage to load gigabytes of data (movies) from ajax into chrome. With that you could maybe do a hex editor thats only shows what would be able to fit into the screen

That last thing was a good reason... Download a image from ajax -> wrap it into a blob -> and read only the necessary metadata (size, camera, angle)

var fr = new FileReader();
fr.onload = function(){
    // Read metadata info
};
fr.readAsArrayBuffer(fakeBlob.slice(0,1024));

The FileReader could be async. Useful for large blobs where converting lots of data could lead to long running warning dialog in IE (setTimeout to avoid it)

eligrey commented 10 years ago

Download a image from ajax -> wrap it into a blob -> and read only the necessary metadata (size, camera, angle)

You can simplify this process using half as much memory: Download a image from ajax -> read only the necessary metadata (size, camera, angle)

Blobs are an unnecessary step that would duplicate the data. Even if you slice the blob, there is still memory and/or disk cache backing the entire blob. It's better to slice the string data (or arraybuffer data if you set request.responseType = "arraybuffer") you already have from the xhr.

There are probably other legitimate reasons you might want to use FileReader on fake blobs, so I will get to implementing this eventually. Just understand that this isn't very high priority for me.

jimmywarting commented 8 years ago

I ended up helping github/fetch polyfill and they do more or less depend on Blob & FileReader now when they are starting to use ReadableStream as there underlying source. so they need to convert a stream to a blob and a blob to arrayBuffer or Text

I started to develop a polyfill for the FileReader but wanted to use a other kind of underlying source (a buffer, Uint8Array) since it's more convenient for handling binaries

So i ended up with something like this that I later want to transpile

const wm = new WeakMap
const concatTypedArrays = arrays => {
  let totalLength = 0

  for (let arr of arrays) {
    totalLength += arr.byteLength
  }

  let result = new Uint8Array(totalLength)
  let offset = 0

  for (let arr of arrays) {
    result.set(arr, offset)
    offset += arr.byteLength
  }

  return result
}

/**
 * EventEmitter
 *
 * [description]
 */
class EventEmitter {
  constructor() {
    var delegate = document.createDocumentFragment()
    ;['addEventListener', 'dispatchEvent', 'removeEventListener'].forEach(f =>
      this[f] = (...xs) => delegate[f](...xs)
    )
  }
}

/**
 * URL
 *
 * [description]
 */
class URL {
  constructor(url, base) {
    if (!url)
      throw new TypeError('Invalid argument');

    let doc = document.implementation.createHTMLDocument('')
    if (base) {
      let baseElement = doc.createElement('base')
      baseElement.href = base
      doc.head.appendChild(baseElement)
    }
    let anchorElement = doc.createElement('a')
    anchorElement.href = url
    doc.body.appendChild(anchorElement)

    if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href))
      throw new TypeError('Invalid URL');

    wm.set(this, anchorElement)
  }

  static createObjectURL(blob) {
    if (!(blob instanceof Blob)) 
      throw new TypeError("Failed to execute 'createObjectURL' on 'URL': No function was found that matched the signature provided")

    let buffer = wm.get(blob).slice()
    let string = new TextDecoder().decode(buffer)
    return `data:${blob.type};base64,` + btoa(string)
  }

  static revokeObjectURL() {}

  toString() {
    return this.href
  }

  [Symbol.toStringTag]() {
    return 'URL'
  }
}

;['href','protocol','username','password','origin','URLSearchParams',
'host','hostname','port','pathname','search','hash'].forEach(name => {
  Object.defineProperty(URL.prototype, name, {
    get: function() {
      return wm.get(this)[name]
    },
    set: function(value) {
      return wm.get(this)[name] = value
    }
  })
})

/**
 * Blob
 *
 * [description]
 */
class Blob {
  constructor(chunks = [], opts = {}) {
    if (!Array.isArray(chunks))
      throw new TypeError('The 1st argument is not an array')

    // convert all chunks to typed arrays
    chunks = chunks.map(chunk =>
      chunk instanceof Blob ? wm(blob) : 
      ArrayBuffer.isView(chunk) ? chunk :
      chunk instanceof ArrayBuffer ? new Uint8Array(chunk) :
      new TextEncoder().encode(chunk += '')
    )

    let buffer = concatTypedArrays(chunks)
    wm.set(this, buffer)

    this.size = buffer.byteLength
    this.type = opts.type || ''
    this.isClosed = false
  }

  close() {
    this.isClosed = true
    this.size = 0
    wm.delete(this)
  }

  slice(start = 0, end, type = '') {
    let buffer = wm.get(this)
    let slice = buffer.slice(start, end || buffer.length)

    return new Blob([slice], type)
  }

  toString() {
    return '[object Blob]'
  }

  [Symbol.toStringTag]() {
    return 'Blob'
  }
}

/**
 * File
 *
 * [description]
 */
class File extends Blob {
  constructor(chunks, filename, opts = {}) {
    super(chunks, opts)

    this.name = filename + ''
    this.lastModified = Date.now()
    this.lastModifiedDate = new Date(this.lastModified)
  }

  toString() {
    return '[object File]'
  }

  get [Symbol.toStringTag]() {
    return 'File'
  }
}

class Event extends CustomEvent {
  constructor(fr, ...args) {
    super(...args)

    Object.defineProperties(this, {
      srcElement: {enumerable: true, value: fr},
      target: {enumerable: true, value: fr}
    })
  }
}

function emit(fr, result) {
  setTimeout(() => {
    fr.readyState = 2
    fr.result = result
    for (let type of ['loadstart', 'progress', 'load', 'loadend']) {
      let event = new Event(fr, type)
      let listener = fr['on' + type]

      fr.dispatchEvent(event)

      if(listener)
        listener.call(fr, event)
    }
  })
}

/**
 * FileReader
 *
 * [description]
 */
class FileReader extends EventEmitter {
  constructor() {
    super()

    this.readyState = 0
    this.result =
    this.error =
    this.onabort =
    this.onerror =
    this.onload =
    this.onloadend =
    this.onloadstart =
    this.onprogress = null
  }

  abort() {}

  readAsArrayBuffer(blob) {
    emit(this, wm.get(blob).slice().buffer)
  }

  readAsDataURL(blob) {
    let buffer = wm.get(blob).slice()
    let string = new TextDecoder().decode(buffer)
    emit(this, `data:${blob.type};base64,` + btoa(string))
  }

  readAsText(blob) {
    let buffer = wm.get(blob).slice()
    emit(this, new TextDecoder().decode(buffer))
  }

  toString() {
    return '[object FileReader]'
  }

  [Symbol.toStringTag]() {
    return 'FileReader'
  }
}

FileReader.EMPTY = 0
FileReader.LOADING = 1
FileReader.DONE = 2

/* // test
var fr = new FileReader
fr.readAsText(new File(['abc'], 'filename.txt'))
fr.onload = console.log
*/

export {FileReader, URL, Blob, File}

Do you feel like this lib needs a face lift? It has a bit more dependencies like TypedArrays, WeakMap, Symbols, TextEncoder, TextDecoder & CustomEvent

guest271314 commented 7 years ago

@jimmywarting Uncaught TypeError: Failed to construct 'CustomEvent': 1 argument required, but only 0 present.

jimmywarting commented 7 years ago

@guest271314 fixed

guest271314 commented 7 years ago

@jimmywarting Is it possible to convert a Blob or File object to an ArrayBuffer without using FileReader? How to create an ArrayBuffer and data URI from Blob and File objects without FileReader?