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 {
/** @summary Decode options string */
decodeOptions(opt) {
const d = new DrawOptions(opt);
this.options = { Zscale: false };
const obj = this.getObject();
if (d.check('CONST')) {
this.options.constRatio = true;
if (obj) obj.fConstRatio = true;
}
if (d.check('Z'))
this.options.Zscale = true;
}
/** @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 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.fContour = {
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.fContour.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 Draw image */
async drawImage() {
const obj = this.getObject(),
fp = this.getFramePainter(),
rect = fp?.getFrameRect() ?? this.getPadPainter().getPadRect();
this.wheel_zoomy = true;
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.options.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();
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(this.options.Zscale, 'Color palette', flag => {
this.options.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 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;
this._color_palette = true; // to emulate behavior of hist painter
}
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.setSecondary(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) {
this.options.Zscale = !this.options.Zscale;
return this.drawColorPalette(this.options.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 };