diff --git a/canvas_modules/common-canvas/src/common-canvas/canvas-controller.js b/canvas_modules/common-canvas/src/common-canvas/canvas-controller.js index f92fc9d51b..4dcaf82d09 100644 --- a/canvas_modules/common-canvas/src/common-canvas/canvas-controller.js +++ b/canvas_modules/common-canvas/src/common-canvas/canvas-controller.js @@ -1735,6 +1735,20 @@ export default class CanvasController { this.objectModel.clearSavedZoomValues(); } + getViewPortDimensions() { + if (this.canvasContents) { + return this.getSVGCanvasD3().getTransformedViewportDimensions(); + } + return null; + } + + getCanvasDimensionsWithPadding() { + if (this.canvasContents) { + return this.getSVGCanvasD3().getCanvasDimensionsWithPadding(); + } + return null; + } + // --------------------------------------------------------------------------- // Utility/helper methods // --------------------------------------------------------------------------- diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.js index 894f3919e1..61532dc76a 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.js +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.js @@ -196,6 +196,10 @@ export default class SVGCanvasD3 { return this.renderer.getTransformedViewportDimensions(); } + getCanvasDimensionsWithPadding() { + return this.renderer.getCanvasDimensionsWithPadding(); + } + getGhostNode(nodeTemplate) { return this.renderer.getGhostNode(nodeTemplate); } diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js index b67bcae1e7..98ddc9383e 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js @@ -227,6 +227,10 @@ export default class SVGCanvasRenderer { return this.zoomUtils.getTransformedViewportDimensions(); } + getCanvasDimensionsWithPadding() { + return this.zoomUtils.getCanvasDimensionsWithPadding(); + } + // Returns the data object for the parent supernode that references the // active pipeline (managed by this renderer). We get the supernode by // looking through the overall canvas info object. diff --git a/canvas_modules/harness/package.json b/canvas_modules/harness/package.json index c3f09b117c..c15e8d1eca 100644 --- a/canvas_modules/harness/package.json +++ b/canvas_modules/harness/package.json @@ -19,8 +19,10 @@ "compression": "1.7.4", "express": "4.17.1", "express-session": "1.17.1", + "html-to-image": "1.11.11", "isomorphic-fetch": "3.0.0", "js-file-download": "0.4.12", + "jspdf": "2.5.1", "lodash": "4.17.21", "log4js": "6.4.1", "nconf": "0.12.0" diff --git a/canvas_modules/harness/src/client/App.js b/canvas_modules/harness/src/client/App.js index 2891194486..325e0f183d 100644 --- a/canvas_modules/harness/src/client/App.js +++ b/canvas_modules/harness/src/client/App.js @@ -29,6 +29,9 @@ import { hot } from "react-hot-loader/root"; import classNames from "classnames"; import { v4 as uuid4 } from "uuid"; +import { jsPDF } from "jspdf"; +import * as htmlToImage from "html-to-image"; + import { getMessages } from "../intl/intl-utils"; import * as HarnessBundles from "../intl/locales"; import CommandActionsBundles from "@elyra/canvas/locales/command-actions/locales"; @@ -356,6 +359,7 @@ class App extends React.Component { this.disableWideFlyoutPrimaryButton = this.disableWideFlyoutPrimaryButton.bind(this); this.clearSavedZoomValues = this.clearSavedZoomValues.bind(this); + this.saveToPdf = this.saveToPdf.bind(this); this.getPipelineFlow = this.getPipelineFlow.bind(this); this.setPipelineFlow = this.setPipelineFlow.bind(this); this.addNodeTypeToPalette = this.addNodeTypeToPalette.bind(this); @@ -1014,6 +1018,42 @@ class App extends React.Component { this.canvasController2.clearSavedZoomValues(); } + // Saves the canvas objects into and image and then places that image into + // a PDF file. Creating the image takes quite a while to execute so it would + // probably need a progress indicator if it was implemented in an application. + saveToPdf() { + const svgAreaElements = document + .getElementById("d3-svg-canvas-div-0") + .getElementsByClassName("svg-area"); + + const dims = this.canvasController.getCanvasDimensionsWithPadding(); + const heightToWidthRatio = dims.height / dims.width; + + htmlToImage.toPng(svgAreaElements[0], { + filter: (node) => this.shouldIncludeInImage(node), + height: dims.height, + width: dims.width + }) + .then(function(dataUrl) { + const img = document.createElement("img"); // new Image(); + img.src = dataUrl; + + const doc = new jsPDF(); + doc.addImage(img, "PNG", 10, 10, 190, 190 * heightToWidthRatio); + doc.save("common-canvas.pdf"); + }) + .catch(function(error) { + console.error("An error occurred create the PNG image.", error); + }); + } + + // Returns true if the node passed in should be included in a saved image of the canvas. + // The only node that is excluded is the canvas background rectangle which has the + // same dimensions as the viewport which is often smaller than the canvas itself. + shouldIncludeInImage(node) { + return node?.classList ? !(node.classList.contains("d3-svg-background")) : true; + } + generateNodeNotificationMessages(nodeMessages, currentPipelineId) { const nodeErrorMessages = []; const nodeWarningMessages = []; @@ -2634,7 +2674,8 @@ class App extends React.Component { setPaletteDropdownSelect2: this.setPaletteDropdownSelect2, selectedPaletteDropdownFile: this.state.selectedPaletteDropdownFile, selectedPaletteDropdownFile2: this.state.selectedPaletteDropdownFile2, - clearSavedZoomValues: this.clearSavedZoomValues + clearSavedZoomValues: this.clearSavedZoomValues, + saveToPdf: this.saveToPdf }; const sidePanelPropertiesConfig = { diff --git a/canvas_modules/harness/src/client/components/sidepanel/canvas/sidepanel-canvas.jsx b/canvas_modules/harness/src/client/components/sidepanel/canvas/sidepanel-canvas.jsx index 5fdf6398ef..90b834758f 100644 --- a/canvas_modules/harness/src/client/components/sidepanel/canvas/sidepanel-canvas.jsx +++ b/canvas_modules/harness/src/client/components/sidepanel/canvas/sidepanel-canvas.jsx @@ -570,6 +570,18 @@ export default class SidePanelForms extends React.Component { ); + var saveToPdf = (
+
+
+ +
+
+ ); + var entrySize = { "width": "80px", "minWidth": "80px" }; var snapToGrid = (
@@ -1575,6 +1587,8 @@ export default class SidePanelForms extends React.Component { {divider}
Canvas Content
{divider} + {saveToPdf} + {divider} {interactionType} {divider} {snapToGrid} @@ -1657,6 +1671,7 @@ SidePanelForms.propTypes = { selectedPaletteDropdownFile2: PropTypes.string, setPaletteDropdownSelect: PropTypes.func, setPaletteDropdownSelect2: PropTypes.func, - clearSavedZoomValues: PropTypes.func + clearSavedZoomValues: PropTypes.func, + saveToPdf: PropTypes.func }) };