tre-images
Version:
tre-images ---
319 lines (289 loc) • 9.5 kB
JavaScript
const pull = require('pull-stream')
const h = require('mutant/html-element')
const Value = require('mutant/value')
const Str = require('tre-string')
const computed = require('mutant/computed')
const watch = require('mutant/watch')
const setStyle = require('module-styles')('tre-images')
const debug = require('debug')('tre-image')
const FileSource = require('tre-file-importer/file-source')
const BufferList = require('bl')
const svgDataUri = require('mini-svg-data-uri')
const {importFiles, factory, parseFile} = require('./common')
module.exports = function Render(ssb, opts) {
opts = opts || {}
const {prototypes, renderCustomElement} = opts
if (!prototypes) throw new Error('need prototypes!')
styles()
const getSrcObs = Source(ssb)
return function render(kv, ctx) {
ctx = ctx || {}
const where = ctx.where || 'stage'
const content = kv.value.content
if (!content) return
if (content.type !== 'image') return
const bitmapObs = Value()
const dataURIObs = Value()
const ownContentObs = ctx.contentObs || Value({})
const previewObs = ctx.previewObs || Value(kv)
const previewContentObs = computed(previewObs, kv => kv && kv.value.content)
const thumbnailObs = computed(previewContentObs, kv => content && content.thumbnail)
function set(o) {
ownContentObs.set(Object.assign({}, ownContentObs(), o))
}
const renderStr = Str({
save: text => {
set({name: text})
}
})
if (where == 'editor') {
return renderEditor()
} else if (['thumbnail', 'tile'].includes(where)) {
return renderThumbnail()
}
return renderCanvasOrImg(upload)
// - if bitmapObs is set, render the bitmap
// to a canvas.
// - if dataURIObs is set, render it as img src.
// - Otherwise render the blob
// referred to in content
function renderCanvasOrImg(handleFile) {
return computed([bitmapObs, dataURIObs], (bitmap, dataURI) => {
if (!bitmap && !dataURI) return renderImg(previewContentObs, handleFile)
if (dataURI) {
return h('img.tre-image', Object.assign(dragAndDrop(handleFile), {
src: dataURI,
width: computed(previewContentObs, c => c && c.width),
height: computed(previewContentObs, c => c && c.height)
}))
}
return h('canvas.tre-image', Object.assign( {}, dragAndDrop(handleFile), {
width: bitmap.width,
height: bitmap.height,
hooks: [canvas => {
const ctx = canvas.getContext("bitmaprenderer")
ctx.transferFromImageBitmap(bitmap)
return el => {
bitmap.close()
}
}]
}))
})
}
function renderTag(cObs, opts) {
opts = opts || {}
const {handleFileDrop, placeholder, element} = opts
const src = getSrcObs(cObs)
const width = computed(cObs, content => content && content.width || 10)
const height = computed(cObs, content => content && content.height || 10)
const format = computed(previewContentObs, content => content && content.format)
return computed([src, width, height, format], (src, width, height, format) => {
if (!src) {
if (!placeholder) return h('.tre-image.empty', dragAndDrop(handleFileDrop))
return placeholder({handleFileDrop})
}
//console.warn(`render ${format} image ${width}x${height}: ${src}`)
return element({src, width, height, format, ctx, handleFileDrop})
})
}
function renderImg(cObs, handleFileDrop) {
let retry = 0
let timerId
function abort() {
if (timerId) {
clearTimeout(timerId)
timerId = null
}
}
return renderTag(cObs, {
handleFileDrop,
element: renderCustomElement || (({src, width, height, handleFileDrop}) => {
let el = h('img.tre-image', Object.assign(dragAndDrop(handleFileDrop), {
hooks: [el=>abort],
src, width, height,
'ev-error': ev =>{
console.warn('Error loading image', ev.type)
timerId = setTimeout( ()=>{
console.warn(`Retry ${retry} to load ${src}`)
el.setAttribute('src', src)
}, (1<<retry++) * 250)
}
}))
return el
})
})
}
function renderThumbnail() {
const formatObs = computed(previewContentObs, c => c && c.format)
const isSmall = computed(previewContentObs, c => {
return c && (c.width < 512 && c.height < 512)
})
const hasThumbnail = computed(previewContentObs, c => {
return c && c.thumbnail
})
return computed([formatObs, isSmall, hasThumbnail], (format, isSmall, hasThumbnail) => {
if (!format) return []
let obs = thumbnailObs
if (format.includes('svg') || isSmall || !hasThumbnail) {
obs = previewContentObs
}
return renderTag(obs, {
element: renderCustomElement || (({src, width, height}) => {
return h('img.tre-image-thumbnail', {
src, width, height
})
}),
placeholder: ()=> h('.tre-image-thumbnail', {}, 'no thumbnail')
})
})
}
function renderEditor() {
return h('.tre-images-editor', [
h('h1', renderStr(computed(previewObs, kv => kv && kv.value.content.name || 'No Name'))),
renderCanvasOrImg(upload)
])
}
function handleFile(file) {
set({extractedMeta: {}, width: 0, height: 0})
const parser = parseFile(file, {
onMeta: meta => set(meta),
onExif: extractedMeta =>{
set({extractedMeta})
},
forceExifParsing: true
})
pull(
parser,
pull.onEnd( err => {
parser.end()
if (err) console.error('parseFile error', err.message)
})
)
const c = contentObs()
const format = c && c.format
if (!format) {
return console.warn('image format detection failed')
}
if (format.includes('svg')) {
pull(
file.source(),
pull.collect( (err, buffers) => {
if (err) {
return console.warn('failed reading svg data')
}
const svgSrc = BufferList(buffers).toString()
dataURIObs.set(svgDataUri(svgSrc))
})
)
return
}
loadBitmap(file, bitmapObs)
}
function upload(file) {
if (file.type && file.type.includes('svg')) {
return doImport()
}
loadBitmap(file, bitmapObs, (err, bitmap) => {
if (err) return console.error(err)
doImport()
})
function doImport() {
importFiles(ssb, [file], {prototypes}, (err, content) => {
if (err) return console.error(err.message)
console.log('imported', content)
set(content)
})
}
}
}
}
module.exports.importFiles = importFiles
module.exports.factory = factory
// -- utils
function loadBitmap(file, bitmapObs, cb) {
// See
// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/createImageBitmap
options = {}
global.createImageBitmap(file, options).then( bmp => {
const old = bitmapObs()
if (old) old.close()
bitmapObs.set(bmp)
if (cb) cb(null, bmp)
}).catch( err => {
if (err) console.error('createImageBitmap error', err.message)
const old = bitmapObs()
if (old) old.close()
bitmapObs.set(null)
if (cb) cb(err)
})
}
function dragAndDrop(onfile) {
return {
'ev-dragenter': e => {
e.preventDefault()
e.stopPropagation()
e.target.classList.add('drag-hover')
},
'ev-dragleave': e => {
e.preventDefault()
e.stopPropagation()
e.target.classList.remove('drag-hover')
},
'ev-dragover': e => {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'all'
},
'ev-drop': e => {
e.preventDefault()
e.stopPropagation()
const files = e.dataTransfer.files || []
function source(file) {
return opts => FileSource(file, opts)
}
for(let file of files) {
file.source = source(file)
onfile(file)
}
}
}
}
function Source(ssb) {
const blobPrefix = Value()
ssb.ws.getAddress((err, address) => {
if (err) return console.error(err)
address = address.replace(/^ws:\/\//, 'http://').replace(/~.*$/, '/blobs/get/')
blobPrefix.set(address)
})
return function getSrcObs(cObs) {
return computed([blobPrefix, cObs], (bp, content) => {
if (!bp) return null
let contentType = content && content.file && content.file.type
if (contentType == 'image/svg') contentType = 'image/svg+xml'
const blob = content && content.blob
if (!blob) return null
return `${bp}${encodeURIComponent(blob)}${contentType ? '?contentType=' + encodeURIComponent(contentType) : ''}`
}, {comparer: (a,b)=>a==b})
}
}
function styles() {
setStyle(`
.tre-images-editor {
height: 100%;
}
.tre-image.empty {
width: 200px;
height: 200px;
border-radius: 10px;
border: 5px #999 dashed;
}
.tre-image.drag-hover {
border-radius: 10px;
border: 5px #994 dashed;
}
.tre-images-editor .tre-image {
width: 100%;
height: auto;
}
`)
}