Skip to content

Commit a23a372

Browse files
committedMar 10, 2023
line based image serialization
1 parent be72b06 commit a23a372

File tree

3 files changed

+95
-89
lines changed

3 files changed

+95
-89
lines changed
 

‎src/ImageAddon.ts

+11-89
Original file line numberDiff line numberDiff line change
@@ -318,102 +318,24 @@ export class ImageAddon implements ITerminalAddon {
318318
/**
319319
* demo hack for complex terminal buffer serialization
320320
*/
321-
private _parts: string[] = [''];
322321

323-
// example for text/FG/BG serializer
324-
private _serText(num: number): number[] {
325-
// FIXME: no FG/BG attributes atm
326-
const buffer = this._terminal!._core.buffer;
327-
const line = buffer.lines.get(num);
328-
if (!line) return [];
329-
const cell = this._terminal?.buffer.active.getNullCell()!;
330-
const cols = this._terminal!.cols;
331-
const res: number[] = [];
332-
let partPos = 0;
333-
let content = '';
334-
for (let col = 0; col < cols; ++col) {
335-
line?.loadCell(col, cell as any);
336-
if (!cell.getChars()) {
337-
partPos = 0;
338-
if (content) this._parts.push(content);
339-
content = '';
340-
} else {
341-
partPos = this._parts.length;
342-
content += cell.getChars();
343-
}
344-
res.push(partPos);
345-
}
346-
if (content) this._parts.push(content);
347-
return res;
348-
}
349-
350-
private _encodeTile(canvas: HTMLCanvasElement): string {
351-
// repack cell tile into a proper cell covering canvas if it is too small
352-
const cw = this._renderer!.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
353-
const ch = this._renderer!.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
354-
if (canvas.width < cw || canvas.height < ch) {
355-
const newCanvas = ImageRenderer.createCanvas(window, Math.ceil(cw), Math.ceil(ch));
356-
newCanvas.getContext('2d')?.drawImage(canvas, 0, 0);
357-
canvas = newCanvas;
358-
}
322+
public serializeLine(num: number): string {
323+
const canvas = this._storage!.extractLineCanvas(num);
324+
if (!canvas) return '';
325+
const w = this._terminal!.cols;
359326
const data = canvas.toDataURL('image/png').slice(22);
360-
const iipSeq = `\x1b]1337;File=inline=1;width=1;height=1;preserveAspectRatio=0;size=${atob(data).length}:${data}`;
361-
return iipSeq + '\x1b[C'; // + cursor advance by one
362-
}
363-
364-
// example for image serializer
365-
private _serImages(num: number): number[] {
366-
const res: number[] = [];
367-
const cols = this._terminal!.cols;
368-
const buffer = this._terminal!._core.buffer;
369-
const line = buffer.lines.get(num);
370-
if (!line) return [];
371-
372-
for (let col = 0; col < cols; ++col) {
373-
// for simplicity only single cell tile encoding atm
374-
const canvas = this.extractTileAtBufferCell(col, num);
375-
if (!canvas || !canvas.width || !canvas.height) {
376-
res.push(0);
377-
continue;
378-
}
379-
res.push(this._parts.length);
380-
this._parts.push(this._encodeTile(canvas));
381-
}
382-
return res;
327+
const iipSeq = `\x1b]1337;File=inline=1;width=${w};height=1;preserveAspectRatio=0;size=${atob(data).length}:${data}`;
328+
return '\r' + iipSeq + `\x1b[${w}C`; // CR + IIP sequence + cursor advance by line width
383329
}
384330

385331
public serialize(start: number, end: number): string[] {
386332
const lines: string[] = [];
387-
const cols = this._terminal!.cols;
333+
const buffer = this._terminal!._core.buffer;
388334
for (let row = start; row < end; ++row) {
389-
const indices: number[][] = [];
390-
// FIXME: turn next 2 invocations into registered event handlers
391-
indices.push(this._serText(row));
392-
indices.push(this._serImages(row));
393-
394-
// fuse logic
395-
const entries: string[] = [];
396-
let cursorAdjust = 0;
397-
for (let i = 0; i < cols; ++i) {
398-
let entry = '';
399-
let handled = 0;
400-
for (let k = 0; k < indices.length; ++k) {
401-
handled |= indices[k][i];
402-
if (this._parts[indices[k][i]]) {
403-
entry += this._parts[indices[k][i]];
404-
this._parts[indices[k][i]] = '';
405-
}
406-
}
407-
if (handled && cursorAdjust) {
408-
entries.push(`\x1b[${cursorAdjust}C`);
409-
cursorAdjust = 0;
410-
} else if (!handled) {
411-
cursorAdjust++;
412-
}
413-
entries.push(entry);
414-
}
415-
lines.push(entries.join(''));
416-
this._parts.length = 1;
335+
const line = buffer.lines.get(row);
336+
if (!line) break;
337+
// FIXME: hook into serialize addon instead of translateToString
338+
lines.push(line.translateToString(true) + this.serializeLine(row));
417339
}
418340
return lines;
419341
}

‎src/ImageRenderer.ts

+33
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,39 @@ export class ImageRenderer implements IDisposable {
187187
);
188188
}
189189

190+
// TODO: cleanup, maybe merge with draw?
191+
public lineDraw(ctx: CanvasRenderingContext2D, imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
192+
const { width, height } = this.cellSize;
193+
194+
// Don't try to draw anything, if we cannot get valid renderer metrics.
195+
if (width === -1 || height === -1) {
196+
return;
197+
}
198+
199+
this._rescaleImage(imgSpec, width, height);
200+
const img = imgSpec.actual!;
201+
const cols = Math.ceil(img.width / width);
202+
203+
const sx = (tileId % cols) * width;
204+
const sy = Math.floor(tileId / cols) * height;
205+
const dx = col * width;
206+
// const dy = row * height;
207+
208+
// safari bug: never access image source out of bounds
209+
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
210+
const finalHeight = sy + height > img.height ? img.height - sy : height;
211+
212+
// Floor all pixel offsets to get stable tile mapping without any overflows.
213+
// Note: For not pixel perfect aligned cells like in the DOM renderer
214+
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
215+
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
216+
ctx.drawImage(
217+
img,
218+
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
219+
Math.floor(dx), 0, Math.ceil(finalWidth), Math.ceil(finalHeight)
220+
);
221+
}
222+
190223
/**
191224
* Extract a single tile from an image.
192225
*/

‎src/ImageStorage.ts

+51
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,57 @@ export class ImageStorage implements IDisposable {
484484
}
485485
}
486486

487+
// TODO: cleanup, merge with render? Extend it to an x*y dim extraction method?
488+
public extractLineCanvas(num: number): HTMLCanvasElement | undefined {
489+
if (!this._images.size) return;
490+
491+
const buffer = this._terminal._core.buffer;
492+
const cols = this._terminal._core.cols;
493+
const line = buffer.lines.get(num) as IBufferLineExt;
494+
if (!line) return;
495+
496+
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
497+
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
498+
const width = this._renderer.dimensions?.css.canvas.width || cw * this._terminal.cols;
499+
500+
const canvas = ImageRenderer.createCanvas(window, width, Math.ceil(ch));
501+
const ctx = canvas.getContext('2d');
502+
if (!ctx) return;
503+
504+
let hasTiles = false;
505+
506+
for (let col = 0; col < cols; ++col) {
507+
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
508+
let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
509+
const imageId = e.imageId;
510+
if (imageId === undefined || imageId === -1) {
511+
continue;
512+
}
513+
const imgSpec = this._images.get(imageId);
514+
if (e.tileId !== -1) {
515+
const startTile = e.tileId;
516+
const startCol = col;
517+
let count = 1;
518+
while (
519+
++col < cols
520+
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
521+
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
522+
&& (e.imageId === imageId)
523+
&& (e.tileId === startTile + count)
524+
) {
525+
count++;
526+
}
527+
col--;
528+
if (imgSpec && imgSpec.actual) {
529+
this._renderer.lineDraw(ctx, imgSpec, startTile, startCol, num, count);
530+
hasTiles = true;
531+
}
532+
}
533+
}
534+
}
535+
if (hasTiles) return canvas;
536+
}
537+
487538
/**
488539
* Extract active single tile at buffer position.
489540
*/

0 commit comments

Comments
 (0)
Please sign in to comment.