From a1bcdf17a53fb98c476aae9e205817c4a80a363d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sat, 16 Nov 2024 15:13:50 +0000 Subject: [PATCH] feat(jupyter): Add `Deno.jupyter.image` API (#26284) This commit adds `Deno.jupyter.image` API to display PNG and JPG images: ``` const data = Deno.readFileSync("./my-image.jpg"); Deno.jupyter.image(data); Deno.jupyter.image("./my-image.jpg"); ``` --- cli/js/40_jupyter.js | 79 ++++++++++++++++++++++++++++++ cli/tsc/dts/lib.deno.unstable.d.ts | 26 ++++++++++ runtime/js/99_main.js | 2 + tests/unit/ops_test.ts | 2 +- 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/cli/js/40_jupyter.js b/cli/js/40_jupyter.js index ace50d6dcc..198b6a3502 100644 --- a/cli/js/40_jupyter.js +++ b/cli/js/40_jupyter.js @@ -177,6 +177,52 @@ function isCanvasLike(obj) { return obj !== null && typeof obj === "object" && "toDataURL" in obj; } +function isJpg(obj) { + // Check if obj is a Uint8Array + if (!(obj instanceof Uint8Array)) { + return false; + } + + // JPG files start with the magic bytes FF D8 + if (obj.length < 2 || obj[0] !== 0xFF || obj[1] !== 0xD8) { + return false; + } + + // JPG files end with the magic bytes FF D9 + if ( + obj.length < 2 || obj[obj.length - 2] !== 0xFF || + obj[obj.length - 1] !== 0xD9 + ) { + return false; + } + + return true; +} + +function isPng(obj) { + // Check if obj is a Uint8Array + if (!(obj instanceof Uint8Array)) { + return false; + } + + // PNG files start with a specific 8-byte signature + const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10]; + + // Check if the array is at least as long as the signature + if (obj.length < pngSignature.length) { + return false; + } + + // Check each byte of the signature + for (let i = 0; i < pngSignature.length; i++) { + if (obj[i] !== pngSignature[i]) { + return false; + } + } + + return true; +} + /** Possible HTML and SVG Elements */ function isSVGElementLike(obj) { return obj !== null && typeof obj === "object" && "outerHTML" in obj && @@ -233,6 +279,16 @@ async function format(obj) { if (isDataFrameLike(obj)) { return extractDataFrame(obj); } + if (isJpg(obj)) { + return { + "image/jpeg": core.ops.op_base64_encode(obj), + }; + } + if (isPng(obj)) { + return { + "image/png": core.ops.op_base64_encode(obj), + }; + } if (isSVGElementLike(obj)) { return { "image/svg+xml": obj.outerHTML, @@ -314,6 +370,28 @@ const html = createTaggedTemplateDisplayable("text/html"); */ const svg = createTaggedTemplateDisplayable("image/svg+xml"); +function image(obj) { + if (typeof obj === "string") { + try { + obj = Deno.readFileSync(obj); + } catch { + // pass + } + } + + if (isJpg(obj)) { + return makeDisplayable({ "image/jpeg": core.ops.op_base64_encode(obj) }); + } + + if (isPng(obj)) { + return makeDisplayable({ "image/png": core.ops.op_base64_encode(obj) }); + } + + throw new TypeError( + "Object is not a valid image or a path to an image. `Deno.jupyter.image` supports displaying JPG or PNG images.", + ); +} + function isMediaBundle(obj) { if (obj == null || typeof obj !== "object" || Array.isArray(obj)) { return false; @@ -465,6 +543,7 @@ function enableJupyter() { md, html, svg, + image, $display, }; } diff --git a/cli/tsc/dts/lib.deno.unstable.d.ts b/cli/tsc/dts/lib.deno.unstable.d.ts index 6234268c39..f8043f9325 100644 --- a/cli/tsc/dts/lib.deno.unstable.d.ts +++ b/cli/tsc/dts/lib.deno.unstable.d.ts @@ -1180,6 +1180,32 @@ declare namespace Deno { ...values: unknown[] ): Displayable; + /** + * Display a JPG or PNG image. + * + * ``` + * Deno.jupyter.image("./cat.jpg"); + * Deno.jupyter.image("./dog.png"); + * ``` + * + * @category Jupyter + * @experimental + */ + export function image(path: string): Displayable; + + /** + * Display a JPG or PNG image. + * + * ``` + * const img = Deno.readFileSync("./cat.jpg"); + * Deno.jupyter.image(img); + * ``` + * + * @category Jupyter + * @experimental + */ + export function image(data: Uint8Array): Displayable; + /** * Format an object for displaying in Deno * diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 2da5c5398c..b21575b8fb 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -471,6 +471,8 @@ const NOT_IMPORTED_OPS = [ // Related to `Deno.jupyter` API "op_jupyter_broadcast", "op_jupyter_input", + // Used in jupyter API + "op_base64_encode", // Related to `Deno.test()` API "op_test_event_step_result_failed", diff --git a/tests/unit/ops_test.ts b/tests/unit/ops_test.ts index 4ba7c5ce33..6de55f8b66 100644 --- a/tests/unit/ops_test.ts +++ b/tests/unit/ops_test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -const EXPECTED_OP_COUNT = 11; +const EXPECTED_OP_COUNT = 12; Deno.test(function checkExposedOps() { // @ts-ignore TS doesn't allow to index with symbol