Add image as attachment on copy/paste into cell (#15602)

* add pasted image as attachment

* handle duplicate image logic

* replace with regex

* address PR comments
This commit is contained in:
Maddy
2021-06-04 15:20:18 -07:00
committed by GitHub
parent bc766698ee
commit b490d53284
4 changed files with 81 additions and 9 deletions

View File

@@ -272,7 +272,11 @@ export class MarkdownToolbarComponent extends AngularDisposable {
if (imageCalloutResult.embedImage) { if (imageCalloutResult.embedImage) {
let base64String = await this.getFileContentBase64(URI.file(imageCalloutResult.imagePath)); let base64String = await this.getFileContentBase64(URI.file(imageCalloutResult.imagePath));
let mimeType = await this.getFileMimeType(URI.file(imageCalloutResult.imagePath)); let mimeType = await this.getFileMimeType(URI.file(imageCalloutResult.imagePath));
this.cellModel.addAttachment(mimeType, base64String, path.basename(imageCalloutResult.imagePath).replace(' ', '')); const originalImageName: string = path.basename(imageCalloutResult.imagePath).replace(/\s/g, '');
let attachmentName = this.cellModel.addAttachment(mimeType, base64String, originalImageName);
if (originalImageName !== attachmentName) {
imageCalloutResult.insertEscapedMarkdown = `![${attachmentName}](attachment:${attachmentName.replace(/\s/g, '')})`;
}
await insertFormattedMarkdown(imageCalloutResult.insertEscapedMarkdown, this.getCellEditorControl()); await insertFormattedMarkdown(imageCalloutResult.insertEscapedMarkdown, this.getCellEditorControl());
} }
await insertFormattedMarkdown(imageCalloutResult.insertEscapedMarkdown, this.getCellEditorControl()); await insertFormattedMarkdown(imageCalloutResult.insertEscapedMarkdown, this.getCellEditorControl());

View File

@@ -1175,7 +1175,7 @@ suite('Cell Model', function (): void {
let imageFilebase64Value = 'data:application/octet-stream;base64,iVBORw0KGgoAAAANSU'; let imageFilebase64Value = 'data:application/octet-stream;base64,iVBORw0KGgoAAAANSU';
let index = imageFilebase64Value.indexOf('base64,'); let index = imageFilebase64Value.indexOf('base64,');
const testImageAttachment: nb.ICellAttachment = { ['image/png']: imageFilebase64Value.substring(index + 7) }; const testImageAttachment: nb.ICellAttachment = { ['image/png']: imageFilebase64Value.substring(index + 7) };
const attachments: nb.ICellAttachments = { 'test.png': testImageAttachment }; let attachments: nb.ICellAttachments = { 'test.png': testImageAttachment };
let notebookModel = new NotebookModelStub({ let notebookModel = new NotebookModelStub({
name: '', name: '',
version: '', version: '',
@@ -1189,6 +1189,9 @@ suite('Cell Model', function (): void {
let model = factory.createCell(contents, { notebook: notebookModel, isTrusted: false }); let model = factory.createCell(contents, { notebook: notebookModel, isTrusted: false });
model.addAttachment('image/png', imageFilebase64Value, 'test.png'); model.addAttachment('image/png', imageFilebase64Value, 'test.png');
assert.deepEqual(model.attachments, attachments); assert.deepEqual(model.attachments, attachments);
attachments = { 'test.png': testImageAttachment, 'test1.png': testImageAttachment };
model.addAttachment('image/png', imageFilebase64Value, 'test1.png');
assert.deepEqual(model.attachments, attachments, 'addAttachment should add unique images');
}); });
test('addAttachment should not add an invalid attachment to cell', async function () { test('addAttachment should not add an invalid attachment to cell', async function () {
@@ -1207,4 +1210,26 @@ suite('Cell Model', function (): void {
cellModel.addAttachment('image/png', imageFilebase64Value, 'test.png'); cellModel.addAttachment('image/png', imageFilebase64Value, 'test.png');
assert.equal(cellModel.attachments, undefined); assert.equal(cellModel.attachments, undefined);
}); });
test('addAttachment should not add a duplicate attachment to cell', async function () {
let imageFilebase64Value = 'data:application/octet-stream;base64,iVBORw0KGgoAAAANSU';
let index = imageFilebase64Value.indexOf('base64,');
const testImageAttachment: nb.ICellAttachment = { ['image/png']: imageFilebase64Value.substring(index + 7) };
let attachments: nb.ICellAttachments = { 'test.png': testImageAttachment };
let notebookModel = new NotebookModelStub({
name: '',
version: '',
mimetype: ''
});
let contents: nb.ICellContents = {
cell_type: CellTypes.Code,
source: '',
metadata: {}
};
let cellModel = factory.createCell(contents, { notebook: notebookModel, isTrusted: false });
cellModel.addAttachment('image/png', imageFilebase64Value, 'test.png');
assert.deepEqual(cellModel.attachments, attachments);
cellModel.addAttachment('image/png', imageFilebase64Value, 'test.png');
assert.deepEqual(cellModel.attachments, attachments, 'addAttachment should not add duplicate images');
});
}); });

View File

@@ -34,7 +34,7 @@ import { IInsightOptions } from 'sql/workbench/common/editor/query/chartState';
let modelId = 0; let modelId = 0;
const ads_execute_command = 'ads_execute_command'; const ads_execute_command = 'ads_execute_command';
const validBase64OctetStreamRegex = /^data:application\/octet-stream;base64,(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})/; const validBase64OctetStreamRegex = /data:(?:(application\/octet-stream|image\/png));base64,(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})/;
export interface QueryResultId { export interface QueryResultId {
batchId: number; batchId: number;
id: number; id: number;
@@ -145,11 +145,11 @@ export class CellModel extends Disposable implements ICellModel {
return this._metadata; return this._metadata;
} }
public get attachments() { public get attachments(): nb.ICellAttachments | undefined {
return this._attachments; return this._attachments;
} }
addAttachment(mimeType: string, base64Encoding: string, name: string): void { addAttachment(mimeType: string, base64Encoding: string, name: string): string {
// base64Encoded value looks like: data:application/octet-stream;base64,<base64Value> // base64Encoded value looks like: data:application/octet-stream;base64,<base64Value>
// get the <base64Value> from the string // get the <base64Value> from the string
let index = base64Encoding.indexOf('base64,'); let index = base64Encoding.indexOf('base64,');
@@ -160,11 +160,17 @@ export class CellModel extends Disposable implements ICellModel {
if (!this._attachments) { if (!this._attachments) {
this._attachments = {}; this._attachments = {};
} }
// TO DO: Check if name already exists and message the user? // Check if name already exists and get a unique name
if (this._attachments[name] && this._attachments[name][mimeType] !== attachment[mimeType]) {
name = this.getUniqueAttachmentName(name.substring(0, name.lastIndexOf('.')), name.substring(name.lastIndexOf('.') + 1));
}
if (!this._attachments[name]) {
this._attachments[name] = attachment; this._attachments[name] = attachment;
this.sendChangeToNotebook(NotebookChangeType.CellMetadataUpdated); this.sendChangeToNotebook(NotebookChangeType.CellMetadataUpdated);
} }
} }
return name;
}
private isValidBase64OctetStream(base64Image: string): boolean { private isValidBase64OctetStream(base64Image: string): boolean {
return base64Image && validBase64OctetStreamRegex.test(base64Image); return base64Image && validBase64OctetStreamRegex.test(base64Image);
@@ -295,6 +301,7 @@ export class CellModel extends Disposable implements ICellModel {
} }
public set source(newSource: string | string[]) { public set source(newSource: string | string[]) {
newSource = this.attachImageFromSource(newSource);
newSource = this.getMultilineSource(newSource); newSource = this.getMultilineSource(newSource);
if (this._source !== newSource) { if (this._source !== newSource) {
this._source = newSource; this._source = newSource;
@@ -305,6 +312,35 @@ export class CellModel extends Disposable implements ICellModel {
this._preventNextChartCache = true; this._preventNextChartCache = true;
} }
private attachImageFromSource(newSource: string | string[]): string | string[] {
if (!Array.isArray(newSource) && this.isValidBase64OctetStream(newSource)) {
let results;
while ((results = validBase64OctetStreamRegex.exec(newSource)) !== null) {
let imageName = this.addAttachment(results[1], results[0], 'image.png');
newSource = newSource.replace(validBase64OctetStreamRegex, `attachment:${imageName}`);
}
return newSource;
}
return newSource;
}
/**
* Gets unique attachment name to add to cell metadata
* @param imgName a string defining name of the image.
* @param imgExtension extension of the image
* Returns the unique name
*/
private getUniqueAttachmentName(imgName?: string, imgExtension?: string): string {
let nextVal = 0;
// Note: this will go forever if it's coded wrong, or you have infinite images in a notebook!
while (true) {
let imageName = imgName ? `${imgName}${nextVal}.${imgExtension ?? 'png'}` : `image${nextVal}.png`;
if (!this._attachments || !this._attachments[imageName]) {
return imageName;
}
nextVal++;
}
}
public get modelContentChangedEvent(): IModelContentChangedEvent { public get modelContentChangedEvent(): IModelContentChangedEvent {
return this._modelContentChangedEvent; return this._modelContentChangedEvent;
} }

View File

@@ -535,7 +535,14 @@ export interface ICellModel {
readonly savedConnectionName: string | undefined; readonly savedConnectionName: string | undefined;
readonly attachments: nb.ICellAttachments; readonly attachments: nb.ICellAttachments;
readonly currentMode: CellEditModes; readonly currentMode: CellEditModes;
addAttachment(mimeType: string, base64Encoding: string, name: string): void; /**
* Adds image as an attachment to cell metadata
* @param mimeType a string defining mimeType of the image. Examples: image/png, image/jpeg
* @param base64Encoding the base64 encoded value of the image
* @param name the name of the image.
* Returns the name of the attachment added to metadata.
*/
addAttachment(mimeType: string, base64Encoding: string, name: string): string;
} }
export interface IModelFactory { export interface IModelFactory {