UNPKG

jsroot

Version:
457 lines (373 loc) 15.6 kB
import { create, settings, isNodeJs, isStr, btoa_func, clTAxis, clTPaletteAxis, clTImagePalette, getDocument } from '../core.mjs'; import { toColor } from '../base/colors.mjs'; import { assignContextMenu, kNoReorder } from '../gui/menu.mjs'; import { DrawOptions } from '../base/BasePainter.mjs'; import { ObjectPainter } from '../base/ObjectPainter.mjs'; import { TPavePainter } from '../hist/TPavePainter.mjs'; import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs'; /** * @summary Painter for TASImage object. * * @private */ class TASImagePainter extends ObjectPainter { #contour; /** @summary Decode options string */ decodeOptions(opt) { const d = new DrawOptions(opt), obj = this.getObject(); if (d.check('CONST') && obj) obj.fConstRatio = true; this.setOptions({ Zscale: d.check('Z') }); } /** @summary Create RGBA buffers */ createRGBA(nlevels) { const obj = this.getObject(), pal = obj?.fPalette; if (!pal) return null; const rgba = new Array((nlevels + 1) * 4).fill(0); // precalculated colors for (let lvl = 0, indx = 1; lvl <= nlevels; ++lvl) { const l = lvl / nlevels; while ((pal.fPoints[indx] < l) && (indx < pal.fPoints.length - 1)) indx++; const r1 = (pal.fPoints[indx] - l) / (pal.fPoints[indx] - pal.fPoints[indx - 1]), r2 = (l - pal.fPoints[indx - 1]) / (pal.fPoints[indx] - pal.fPoints[indx - 1]); rgba[lvl * 4] = Math.min(255, Math.round((pal.fColorRed[indx - 1] * r1 + pal.fColorRed[indx] * r2) / 256)); rgba[lvl * 4 + 1] = Math.min(255, Math.round((pal.fColorGreen[indx - 1] * r1 + pal.fColorGreen[indx] * r2) / 256)); rgba[lvl * 4 + 2] = Math.min(255, Math.round((pal.fColorBlue[indx - 1] * r1 + pal.fColorBlue[indx] * r2) / 256)); rgba[lvl * 4 + 3] = Math.min(255, Math.round((pal.fColorAlpha[indx - 1] * r1 + pal.fColorAlpha[indx] * r2) / 256)); } return rgba; } /** @summary Cleanup painter * @private */ cleanup() { this.#contour = undefined; super.cleanup(); } /** @summary Return colors contour * @private */ getContour() { return this.#contour; } /** @summary Create url using image buffer * @private */ async makeUrlFromImageBuf(obj, fp) { const nlevels = 1000; this.rgba = this.createRGBA(nlevels); // precalculated colors let min = obj.fImgBuf[0], max = obj.fImgBuf[0]; for (let k = 1; k < obj.fImgBuf.length; ++k) { const v = obj.fImgBuf[k]; min = Math.min(v, min); max = Math.max(v, max); } // does not work properly in Node.js, causes 'Maximum call stack size exceeded' error // min = Math.min.apply(null, obj.fImgBuf), // max = Math.max.apply(null, obj.fImgBuf); // create contour like in hist painter to allow palette drawing this.#contour = { arr: new Array(200), rgba: this.rgba, getLevels() { return this.arr; }, getPaletteColor(pal, zval) { if (!this.arr || !this.rgba) return 'white'; const indx = Math.round((zval - this.arr[0]) / (this.arr.at(-1) - this.arr.at(0)) * (this.rgba.length - 4) / 4) * 4; return toColor(this.rgba[indx] / 255, this.rgba[indx + 1] / 255, this.rgba[indx + 2] / 255, this.rgba[indx + 3] / 255); } }; for (let k = 0; k < 200; k++) this.#contour.arr[k] = min + (max - min) / (200 - 1) * k; if (min >= max) max = min + 1; const z = this.getImageZoomRange(fp, obj.fConstRatio, obj.fWidth, obj.fHeight), pr = isNodeJs() ? import('canvas').then(h => h.default.createCanvas(z.xmax - z.xmin, z.ymax - z.ymin)) : new Promise(resolveFunc => { const c = document.createElement('canvas'); c.width = z.xmax - z.xmin; c.height = z.ymax - z.ymin; resolveFunc(c); }); return pr.then(canvas => { const context = canvas.getContext('2d'), imageData = context.getImageData(0, 0, canvas.width, canvas.height), arr = imageData.data; for (let i = z.ymin; i < z.ymax; ++i) { let dst = (z.ymax - i - 1) * (z.xmax - z.xmin) * 4; const row = i * obj.fWidth; for (let j = z.xmin; j < z.xmax; ++j) { let iii = Math.round((obj.fImgBuf[row + j] - min) / (max - min) * nlevels) * 4; // copy rgba value for specified point arr[dst++] = this.rgba[iii++]; arr[dst++] = this.rgba[iii++]; arr[dst++] = this.rgba[iii++]; arr[dst++] = this.rgba[iii]; } } context.putImageData(imageData, 0, 0); return { url: canvas.toDataURL(), constRatio: obj.fConstRatio, can_zoom: true }; }); } getImageZoomRange(fp, constRatio, width, height) { const res = { xmin: 0, xmax: width, ymin: 0, ymax: height }; if (!fp) return res; let offx = 0, offy = 0, sizex = width, sizey = height; if (constRatio) { const image_ratio = height / width, frame_ratio = fp.getFrameHeight() / fp.getFrameWidth(); if (image_ratio > frame_ratio) { const w2 = height / frame_ratio; offx = Math.round((w2 - width) / 2); sizex = Math.round(w2); } else { const h2 = frame_ratio * width; offy = Math.round((h2 - height) / 2); sizey = Math.round(h2); } } if (fp.zoom_xmin !== fp.zoom_xmax) { res.xmin = Math.min(width, Math.max(0, Math.round(fp.zoom_xmin * sizex) - offx)); res.xmax = Math.min(width, Math.max(0, Math.round(fp.zoom_xmax * sizex) - offx)); } if (fp.zoom_ymin !== fp.zoom_ymax) { res.ymin = Math.min(height, Math.max(0, Math.round(fp.zoom_ymin * sizey) - offy)); res.ymax = Math.min(height, Math.max(0, Math.round(fp.zoom_ymax * sizey) - offy)); } return res; } /** @summary Produce data url from png buffer */ async makeUrlFromPngBuf(obj, fp) { const buf = obj.fPngBuf; let pngbuf = ''; if (isStr(buf)) pngbuf = buf; else { for (let k = 0; k < buf.length; ++k) pngbuf += String.fromCharCode(buf[k] < 0 ? 256 + buf[k] : buf[k]); } const res = { url: 'data:image/png;base64,' + btoa_func(pngbuf), constRatio: obj.fConstRatio, can_zoom: fp && !isNodeJs() }, doc = getDocument(); if (!res.can_zoom || ((fp?.zoom_xmin === fp?.zoom_xmax) && (fp?.zoom_ymin === fp?.zoom_ymax))) return res; return new Promise(resolveFunc => { const image = doc.createElement('img'); image.onload = () => { const canvas = doc.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; const context = canvas.getContext('2d'); context.drawImage(image, 0, 0); const arr = context.getImageData(0, 0, image.width, image.height).data, z = this.getImageZoomRange(fp, res.constRatio, image.width, image.height), canvas2 = doc.createElement('canvas'); canvas2.width = z.xmax - z.xmin; canvas2.height = z.ymax - z.ymin; const context2 = canvas2.getContext('2d'), imageData2 = context2.getImageData(0, 0, canvas2.width, canvas2.height), arr2 = imageData2.data; for (let i = z.ymin; i < z.ymax; ++i) { let dst = (z.ymax - i - 1) * (z.xmax - z.xmin) * 4, src = ((image.height - i - 1) * image.width + z.xmin) * 4; for (let j = z.xmin; j < z.xmax; ++j) { // copy rgba value for specified point arr2[dst++] = arr[src++]; arr2[dst++] = arr[src++]; arr2[dst++] = arr[src++]; arr2[dst++] = arr[src++]; } } context2.putImageData(imageData2, 0, 0); res.url = canvas2.toDataURL(); resolveFunc(res); }; image.onerror = () => resolveFunc(res); image.src = res.url; }); } /** @summary Use in frame painter to check zoom Y is allowed * @protected */ get _wheel_zoomy() { return true; } /** @summary Draw image */ async drawImage() { const obj = this.getObject(), fp = this.getFramePainter(), rect = fp?.getFrameRect() ?? this.getPadPainter().getPadRect(); if (obj._blob) { // try to process blob data due to custom streamer if ((obj._blob.length === 15) && !obj._blob[0]) { obj.fImageQuality = obj._blob[1]; obj.fImageCompression = obj._blob[2]; obj.fConstRatio = obj._blob[3]; obj.fPalette = { _typename: clTImagePalette, fUniqueID: obj._blob[4], fBits: obj._blob[5], fNumPoints: obj._blob[6], fPoints: obj._blob[7], fColorRed: obj._blob[8], fColorGreen: obj._blob[9], fColorBlue: obj._blob[10], fColorAlpha: obj._blob[11] }; obj.fWidth = obj._blob[12]; obj.fHeight = obj._blob[13]; obj.fImgBuf = obj._blob[14]; if ((obj.fWidth * obj.fHeight !== obj.fImgBuf.length) || (obj.fPalette.fNumPoints !== obj.fPalette.fPoints.length)) { console.error(`TASImage _blob decoding error ${obj.fWidth * obj.fHeight} != ${obj.fImgBuf.length} ${obj.fPalette.fNumPoints} != ${obj.fPalette.fPoints.length}`); delete obj.fImgBuf; delete obj.fPalette; } } else if ((obj._blob.length === 3) && obj._blob[0]) { obj.fPngBuf = obj._blob[2]; if (obj.fPngBuf?.length !== obj._blob[1]) { console.error(`TASImage with png buffer _blob error ${obj._blob[1]} != ${obj.fPngBuf?.length}`); delete obj.fPngBuf; } } else console.error(`TASImage _blob len ${obj._blob.length} not recognized`); delete obj._blob; } let promise; if (obj.fImgBuf && obj.fPalette) promise = this.makeUrlFromImageBuf(obj, fp); else if (obj.fPngBuf) promise = this.makeUrlFromPngBuf(obj, fp); else promise = Promise.resolve(null); return promise.then(res => { if (!res?.url) return this; const img = this.createG(fp) .append('image') .attr('href', res.url) .attr('width', rect.width) .attr('height', rect.height) .attr('preserveAspectRatio', res.constRatio ? null : 'none'); if (!this.isBatchMode()) { if (settings.MoveResize || settings.ContextMenu) img.style('pointer-events', 'visibleFill'); if (res.can_zoom) img.style('cursor', 'pointer'); } assignContextMenu(this, kNoReorder); if (!fp || !res.can_zoom) return this; return this.drawColorPalette(this.getOptions().Zscale, true).then(() => { fp.setAxesRanges(create(clTAxis), 0, 1, create(clTAxis), 0, 1, null, 0, 0); fp.createXY({ ndim: 2, check_pad_range: false }); return fp.addInteractivity(); }); }); } /** @summary Fill TASImage context menu */ fillContextMenuItems(menu) { const obj = this.getObject(), o = this.getOptions(); if (obj) { menu.addchk(obj.fConstRatio, 'Const ratio', flag => { obj.fConstRatio = flag; this.interactiveRedraw('pad', `exec:SetConstRatio(${flag})`); }, 'Change const ratio flag of image'); } if (obj?.fPalette) { menu.addchk(o.Zscale, 'Color palette', flag => { o.Zscale = flag; this.drawColorPalette(flag, true); }, 'Toggle color palette'); } } /** @summary Checks if it makes sense to zoom inside specified axis range */ canZoomInside(axis, min, max) { const obj = this.getObject(); if (!obj) return false; if (((axis === 'x') || (axis === 'y')) && (max - min > 0.01)) return true; return false; } /** @summary Return palette - dummy here * @private */ getHistPalette() { return true; } /** @summary Draw color palette * @private */ async drawColorPalette(enabled, can_move) { if (!this.isMainPainter()) return null; if (!this.draw_palette) { const pal = create(clTPaletteAxis); Object.assign(pal, { fX1NDC: 0.91, fX2NDC: 0.95, fY1NDC: 0.1, fY2NDC: 0.9, fInit: 1 }); pal.fAxis.fChopt = '+'; this.draw_palette = pal; } let pal_painter = this.getPadPainter().findPainterFor(this.draw_palette); if (!enabled) { if (pal_painter) { pal_painter.Enabled = false; pal_painter.removeG(); // completely remove drawing without need to redraw complete pad } return null; } const fp = this.getFramePainter(); // keep palette width if (can_move && fp) { const pal = this.draw_palette; pal.fX2NDC = fp.fX2NDC + 0.01 + (pal.fX2NDC - pal.fX1NDC); pal.fX1NDC = fp.fX2NDC + 0.01; pal.fY1NDC = fp.fY1NDC; pal.fY2NDC = fp.fY2NDC; } if (pal_painter) { pal_painter.Enabled = true; return pal_painter.drawPave(''); } return TPavePainter.draw(this.getPadPainter(), this.draw_palette).then(p => { pal_painter = p; // mark painter as secondary - not in list of TCanvas primitives pal_painter.setSecondaryId(this); // make dummy redraw, palette will be updated only from histogram painter pal_painter.redraw = function() {}; }); } /** @summary Toggle colz draw option * @private */ toggleColz() { if (this.getObject()?.fPalette) { const o = this.getOptions(); o.Zscale = !o.Zscale; return this.drawColorPalette(o.Zscale, true); } } /** @summary Redraw image */ redraw() { return this.drawImage(); } /** @summary Process click on TASImage-defined buttons * @desc may return promise or simply false */ clickButton(funcname) { if (this.isMainPainter() && funcname === 'ToggleColorZ') return this.toggleColz(); return false; } /** @summary Fill pad toolbar for TASImage */ fillToolbar() { const pp = this.getPadPainter(); if (pp && this.getObject()?.fPalette) { pp.addPadButton('th2colorz', 'Toggle color palette', 'ToggleColorZ'); pp.showPadButtons(); } } /** @summary Draw TASImage object */ static async draw(dom, obj, opt) { const painter = new TASImagePainter(dom, obj, opt); painter.setAsMainPainter(); painter.decodeOptions(opt); return ensureTCanvas(painter, false) .then(() => painter.drawImage()) .then(() => { painter.fillToolbar(); return painter; }); } } // class TASImagePainter export { TASImagePainter };