You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by hu...@apache.org on 2023/10/31 01:54:39 UTC

(superset) branch master updated: feat(Export as PDF - rasterized): Adding rasterized pdf functionality to dashboard (#25696)

This is an automated email from the ASF dual-hosted git repository.

hugh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 74dbada473 feat(Export as PDF - rasterized): Adding rasterized pdf functionality to dashboard (#25696)
74dbada473 is described below

commit 74dbada473e150203986f22c5e38ac314c551f9c
Author: Jack <41...@users.noreply.github.com>
AuthorDate: Mon Oct 30 20:54:33 2023 -0500

    feat(Export as PDF - rasterized): Adding rasterized pdf functionality to dashboard (#25696)
---
 superset-frontend/package-lock.json                | 270 +++++++++++++++++++++
 superset-frontend/package.json                     |   1 +
 .../HeaderActionsDropdown.test.tsx                 |   9 +-
 .../Header/HeaderActionsDropdown/index.jsx         |  47 ++--
 .../DownloadMenuItems/DownloadAsImage.test.tsx     |  42 ++++
 .../menu/DownloadMenuItems/DownloadAsImage.tsx     |  37 +++
 .../menu/DownloadMenuItems/DownloadAsPdf.test.tsx  |  42 ++++
 .../menu/DownloadMenuItems/DownloadAsPdf.tsx       |  37 +++
 .../DownloadMenuItems/DownloadMenuItems.test.tsx   |  25 ++
 .../components/menu/DownloadMenuItems/index.tsx    |  62 +++++
 superset-frontend/src/logger/LogUtils.ts           |   3 +
 superset-frontend/src/types/dom-to-pdf.d.ts        |  19 ++
 superset-frontend/src/utils/downloadAsImage.ts     |   2 +-
 .../utils/{downloadAsImage.ts => downloadAsPdf.ts} |  42 ++--
 14 files changed, 574 insertions(+), 64 deletions(-)

diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 370cd3f1f2..b622368280 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -72,6 +72,7 @@
         "d3-color": "^3.1.0",
         "d3-scale": "^2.1.2",
         "dom-to-image-more": "^2.10.1",
+        "dom-to-pdf": "^0.3.2",
         "emotion-rgba": "0.0.9",
         "fast-glob": "^3.2.7",
         "fontsource-fira-code": "^4.0.0",
@@ -19604,6 +19605,12 @@
       "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
       "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
     },
+    "node_modules/@types/raf": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.2.tgz",
+      "integrity": "sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A==",
+      "optional": true
+    },
     "node_modules/@types/range-parser": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
@@ -23875,6 +23882,15 @@
       "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
       "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ=="
     },
+    "node_modules/base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+      "optional": true,
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
     "node_modules/base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -24508,6 +24524,17 @@
         "node-int64": "^0.4.0"
       }
     },
+    "node_modules/btoa": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
+      "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
+      "bin": {
+        "btoa": "bin/btoa.js"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
     "node_modules/buf-compare": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz",
@@ -24998,6 +25025,25 @@
         }
       ]
     },
+    "node_modules/canvg": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
+      "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
+      "optional": true,
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@types/raf": "^3.4.0",
+        "core-js": "^3.8.3",
+        "raf": "^3.4.1",
+        "regenerator-runtime": "^0.13.7",
+        "rgbcolor": "^1.0.1",
+        "stackblur-canvas": "^2.0.0",
+        "svg-pathdata": "^6.0.3"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/capture-exit": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
@@ -26971,6 +27017,15 @@
         "isobject": "^3.0.1"
       }
     },
+    "node_modules/css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "optional": true,
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
     "node_modules/css-loader": {
       "version": "6.8.1",
       "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz",
@@ -29446,11 +29501,25 @@
       "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
       "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs="
     },
+    "node_modules/dom-to-image": {
+      "version": "2.6.0",
+      "resolved": "git+ssh://git@github.com/dmapper/dom-to-image.git#a7c386a8ea813930f05449ac71ab4be0c262dff3",
+      "license": "MIT"
+    },
     "node_modules/dom-to-image-more": {
       "version": "2.10.1",
       "resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-2.10.1.tgz",
       "integrity": "sha512-gMG28V47WGj5/xvrsbSPJAWSaV7CBh4teLErn1iGD1sa29HsFsHxvnoLj8VxVvfqnjPgsiUGs2IV2VAxLJGb+A=="
     },
+    "node_modules/dom-to-pdf": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/dom-to-pdf/-/dom-to-pdf-0.3.2.tgz",
+      "integrity": "sha512-eHLQ/IK+2PQlRjybQ9UHYwpiTd/YZFKqGFyRCjVvi6CPlH58drWQnxf7HBCVRUyAjOtI3RG0kvLidPhC7dOhcQ==",
+      "dependencies": {
+        "dom-to-image": "git+https://github.com/dmapper/dom-to-image.git",
+        "jspdf": "^2.5.1"
+      }
+    },
     "node_modules/dom-walk": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz",
@@ -32262,6 +32331,11 @@
       "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-4.1.1.tgz",
       "integrity": "sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA=="
     },
+    "node_modules/fflate": {
+      "version": "0.4.8",
+      "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
+      "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
+    },
     "node_modules/figgy-pudding": {
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
@@ -35152,6 +35226,19 @@
         "node": ">=6"
       }
     },
+    "node_modules/html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "optional": true,
+      "dependencies": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
     "node_modules/htmlparser2": {
       "version": "3.10.1",
       "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@@ -40207,6 +40294,23 @@
         "node": "*"
       }
     },
+    "node_modules/jspdf": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz",
+      "integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==",
+      "dependencies": {
+        "@babel/runtime": "^7.14.0",
+        "atob": "^2.1.2",
+        "btoa": "^1.2.1",
+        "fflate": "^0.4.8"
+      },
+      "optionalDependencies": {
+        "canvg": "^3.0.6",
+        "core-js": "^3.6.0",
+        "dompurify": "^2.2.0",
+        "html2canvas": "^1.0.0-rc.5"
+      }
+    },
     "node_modules/jsprim": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -54038,6 +54142,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/rgbcolor": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+      "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+      "optional": true,
+      "engines": {
+        "node": ">= 0.8.15"
+      }
+    },
     "node_modules/rimraf": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -55431,6 +55544,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/stackblur-canvas": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz",
+      "integrity": "sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==",
+      "optional": true,
+      "engines": {
+        "node": ">=0.1.14"
+      }
+    },
     "node_modules/stackframe": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz",
@@ -56146,6 +56268,15 @@
       "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
       "dev": true
     },
+    "node_modules/svg-pathdata": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+      "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+      "optional": true,
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/svgo": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz",
@@ -56978,6 +57109,15 @@
         "node": ">=0.10"
       }
     },
+    "node_modules/text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "optional": true,
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
     "node_modules/text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -58498,6 +58638,15 @@
         "node": ">= 0.4.0"
       }
     },
+    "node_modules/utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "optional": true,
+      "dependencies": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
     "node_modules/uuid": {
       "version": "3.4.0",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
@@ -79412,6 +79561,12 @@
       "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
       "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
     },
+    "@types/raf": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.2.tgz",
+      "integrity": "sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A==",
+      "optional": true
+    },
     "@types/range-parser": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
@@ -82819,6 +82974,12 @@
       "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
       "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ=="
     },
+    "base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+      "optional": true
+    },
     "base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -83305,6 +83466,11 @@
         "node-int64": "^0.4.0"
       }
     },
+    "btoa": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
+      "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g=="
+    },
     "buf-compare": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz",
@@ -83669,6 +83835,22 @@
       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001514.tgz",
       "integrity": "sha512-ENcIpYBmwAAOm/V2cXgM7rZUrKKaqisZl4ZAI520FIkqGXUxJjmaIssbRW5HVVR5tyV6ygTLIm15aU8LUmQSaQ=="
     },
+    "canvg": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
+      "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
+      "optional": true,
+      "requires": {
+        "@babel/runtime": "^7.12.5",
+        "@types/raf": "^3.4.0",
+        "core-js": "^3.8.3",
+        "raf": "^3.4.1",
+        "regenerator-runtime": "^0.13.7",
+        "rgbcolor": "^1.0.1",
+        "stackblur-canvas": "^2.0.0",
+        "svg-pathdata": "^6.0.3"
+      }
+    },
     "capture-exit": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
@@ -85266,6 +85448,15 @@
         "isobject": "^3.0.1"
       }
     },
+    "css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "optional": true,
+      "requires": {
+        "utrie": "^1.0.2"
+      }
+    },
     "css-loader": {
       "version": "6.8.1",
       "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz",
@@ -87124,11 +87315,24 @@
         }
       }
     },
+    "dom-to-image": {
+      "version": "git+ssh://git@github.com/dmapper/dom-to-image.git#a7c386a8ea813930f05449ac71ab4be0c262dff3",
+      "from": "dom-to-image@git+https://github.com/dmapper/dom-to-image.git"
+    },
     "dom-to-image-more": {
       "version": "2.10.1",
       "resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-2.10.1.tgz",
       "integrity": "sha512-gMG28V47WGj5/xvrsbSPJAWSaV7CBh4teLErn1iGD1sa29HsFsHxvnoLj8VxVvfqnjPgsiUGs2IV2VAxLJGb+A=="
     },
+    "dom-to-pdf": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/dom-to-pdf/-/dom-to-pdf-0.3.2.tgz",
+      "integrity": "sha512-eHLQ/IK+2PQlRjybQ9UHYwpiTd/YZFKqGFyRCjVvi6CPlH58drWQnxf7HBCVRUyAjOtI3RG0kvLidPhC7dOhcQ==",
+      "requires": {
+        "dom-to-image": "git+https://github.com/dmapper/dom-to-image.git",
+        "jspdf": "^2.5.1"
+      }
+    },
     "dom-walk": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz",
@@ -89312,6 +89516,11 @@
       "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-4.1.1.tgz",
       "integrity": "sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA=="
     },
+    "fflate": {
+      "version": "0.4.8",
+      "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
+      "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
+    },
     "figgy-pudding": {
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
@@ -91480,6 +91689,16 @@
         }
       }
     },
+    "html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "optional": true,
+      "requires": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      }
+    },
     "htmlparser2": {
       "version": "3.10.1",
       "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@@ -95329,6 +95548,21 @@
         "through": ">=2.2.7 <3"
       }
     },
+    "jspdf": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz",
+      "integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==",
+      "requires": {
+        "@babel/runtime": "^7.14.0",
+        "atob": "^2.1.2",
+        "btoa": "^1.2.1",
+        "canvg": "^3.0.6",
+        "core-js": "^3.6.0",
+        "dompurify": "^2.2.0",
+        "fflate": "^0.4.8",
+        "html2canvas": "^1.0.0-rc.5"
+      }
+    },
     "jsprim": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -105883,6 +106117,12 @@
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
       "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
     },
+    "rgbcolor": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+      "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+      "optional": true
+    },
     "rimraf": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -107035,6 +107275,12 @@
         }
       }
     },
+    "stackblur-canvas": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz",
+      "integrity": "sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==",
+      "optional": true
+    },
     "stackframe": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz",
@@ -107577,6 +107823,12 @@
       "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
       "dev": true
     },
+    "svg-pathdata": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+      "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+      "optional": true
+    },
     "svgo": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz",
@@ -108182,6 +108434,15 @@
       "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==",
       "dev": true
     },
+    "text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "optional": true,
+      "requires": {
+        "utrie": "^1.0.2"
+      }
+    },
     "text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -109275,6 +109536,15 @@
       "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
       "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
     },
+    "utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "optional": true,
+      "requires": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
     "uuid": {
       "version": "3.4.0",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index fec56fdb45..a0f5bdd8c0 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -137,6 +137,7 @@
     "d3-color": "^3.1.0",
     "d3-scale": "^2.1.2",
     "dom-to-image-more": "^2.10.1",
+    "dom-to-pdf": "^0.3.2",
     "emotion-rgba": "0.0.9",
     "fast-glob": "^3.2.7",
     "fontsource-fira-code": "^4.0.0",
diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
index 218e2e4546..e112e7e531 100644
--- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
+++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
@@ -109,10 +109,10 @@ test('should render', () => {
   expect(container).toBeInTheDocument();
 });
 
-test('should render the dropdown button', () => {
+test('should render the Download dropdown button when not in edit mode', () => {
   const mockedProps = createProps();
   setup(mockedProps);
-  expect(screen.getByRole('button')).toBeInTheDocument();
+  expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
 });
 
 test('should render the menu items', async () => {
@@ -121,16 +121,17 @@ test('should render the menu items', async () => {
   expect(screen.getAllByRole('menuitem')).toHaveLength(4);
   expect(screen.getByText('Refresh dashboard')).toBeInTheDocument();
   expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
-  expect(screen.getByText('Download as image')).toBeInTheDocument();
   expect(screen.getByText('Enter fullscreen')).toBeInTheDocument();
+  expect(screen.getByText('Download')).toBeInTheDocument();
 });
 
 test('should render the menu items in edit mode', async () => {
   setup(editModeOnProps);
-  expect(screen.getAllByRole('menuitem')).toHaveLength(4);
+  expect(screen.getAllByRole('menuitem')).toHaveLength(5);
   expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
   expect(screen.getByText('Edit properties')).toBeInTheDocument();
   expect(screen.getByText('Edit CSS')).toBeInTheDocument();
+  expect(screen.getByText('Download')).toBeInTheDocument();
 });
 
 describe('with native filters feature flag disabled', () => {
diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx
index 1073d73ab0..4d8bcf4ee0 100644
--- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx
+++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx
@@ -28,6 +28,7 @@ import {
 import { Menu } from 'src/components/Menu';
 import { URL_PARAMS } from 'src/constants';
 import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems';
+import DownloadMenuItems from 'src/dashboard/components/menu/DownloadMenuItems';
 import CssEditor from 'src/dashboard/components/CssEditor';
 import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal';
 import SaveModal from 'src/dashboard/components/SaveModal';
@@ -35,11 +36,9 @@ import HeaderReportDropdown from 'src/features/reports/ReportModal/HeaderReportD
 import injectCustomCss from 'src/dashboard/util/injectCustomCss';
 import { SAVE_TYPE_NEWDASHBOARD } from 'src/dashboard/util/constants';
 import FilterScopeModal from 'src/dashboard/components/filterscope/FilterScopeModal';
-import downloadAsImage from 'src/utils/downloadAsImage';
 import getDashboardUrl from 'src/dashboard/util/getDashboardUrl';
 import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
 import { getUrlParam } from 'src/utils/urlUtils';
-import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
 
 const propTypes = {
   addSuccessToast: PropTypes.func.isRequired,
@@ -90,14 +89,12 @@ const MENU_KEYS = {
   SET_FILTER_MAPPING: 'set-filter-mapping',
   EDIT_PROPERTIES: 'edit-properties',
   EDIT_CSS: 'edit-css',
-  DOWNLOAD_AS_IMAGE: 'download-as-image',
+  DOWNLOAD_DASHBOARD: 'download-dashboard',
   TOGGLE_FULLSCREEN: 'toggle-fullscreen',
   MANAGE_EMBEDDED: 'manage-embedded',
   MANAGE_EMAIL_REPORT: 'manage-email-report',
 };
 
-const SCREENSHOT_NODE_SELECTOR = '.dashboard';
-
 class HeaderActionsDropdown extends React.PureComponent {
   static discardChanges() {
     window.location.reload();
@@ -158,7 +155,7 @@ class HeaderActionsDropdown extends React.PureComponent {
     this.props.startPeriodicRender(refreshInterval * 1000);
   }
 
-  handleMenuClick({ key, domEvent }) {
+  handleMenuClick({ key }) {
     switch (key) {
       case MENU_KEYS.REFRESH_DASHBOARD:
         this.props.forceRefreshAllCharts();
@@ -167,23 +164,6 @@ class HeaderActionsDropdown extends React.PureComponent {
       case MENU_KEYS.EDIT_PROPERTIES:
         this.props.showPropertiesModal();
         break;
-      case MENU_KEYS.DOWNLOAD_AS_IMAGE: {
-        // menu closes with a delay, we need to hide it manually,
-        // so that we don't capture it on the screenshot
-        const menu = document.querySelector(
-          '.ant-dropdown:not(.ant-dropdown-hidden)',
-        );
-        menu.style.visibility = 'hidden';
-        downloadAsImage(
-          SCREENSHOT_NODE_SELECTOR,
-          this.props.dashboardTitle,
-          true,
-        )(domEvent).then(() => {
-          menu.style.visibility = 'visible';
-        });
-        this.props.logEvent?.(LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE);
-        break;
-      }
       case MENU_KEYS.TOGGLE_FULLSCREEN: {
         const url = getDashboardUrl({
           pathname: window.location.pathname,
@@ -311,14 +291,19 @@ class HeaderActionsDropdown extends React.PureComponent {
             />
           </Menu.Item>
         )}
-        {!editMode && (
-          <Menu.Item
-            key={MENU_KEYS.DOWNLOAD_AS_IMAGE}
-            onClick={this.handleMenuClick}
-          >
-            {t('Download as image')}
-          </Menu.Item>
-        )}
+        <Menu.SubMenu
+          key={MENU_KEYS.DOWNLOAD_DASHBOARD}
+          disabled={isLoading}
+          title={t('Download')}
+          logEvent={this.props.logEvent}
+        >
+          <DownloadMenuItems
+            pdfMenuItemTitle={t('Export to PDF')}
+            imageMenuItemTitle={t('Download as Image')}
+            dashboardTitle={dashboardTitle}
+            addDangerToast={addDangerToast}
+          />
+        </Menu.SubMenu>
         {userCanShare && (
           <Menu.SubMenu
             key={MENU_KEYS.SHARE_DASHBOARD}
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
new file mode 100644
index 0000000000..7881e2a76b
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
@@ -0,0 +1,42 @@
+import React, { SyntheticEvent } from 'react';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { Menu } from 'src/components/Menu';
+import downloadAsImage from 'src/utils/downloadAsImage';
+import DownloadAsImage from './DownloadAsImage';
+
+jest.mock('src/utils/downloadAsImage', () => ({
+  __esModule: true,
+  default: jest.fn(() => (_e: SyntheticEvent) => {}),
+}));
+
+const createProps = () => ({
+  addDangerToast: jest.fn(),
+  text: 'Download as Image',
+  dashboardTitle: 'Test Dashboard',
+  logEvent: jest.fn(),
+});
+
+const renderComponent = () => {
+  render(
+    <Menu>
+      <DownloadAsImage {...createProps()} />
+    </Menu>,
+  );
+};
+
+test('Should call download image on click', async () => {
+  const props = createProps();
+  renderComponent();
+  await waitFor(() => {
+    expect(downloadAsImage).toBeCalledTimes(0);
+    expect(props.addDangerToast).toBeCalledTimes(0);
+  });
+
+  userEvent.click(screen.getByRole('button', { name: 'Download as Image' }));
+
+  await waitFor(() => {
+    expect(downloadAsImage).toBeCalledTimes(1);
+    expect(props.addDangerToast).toBeCalledTimes(0);
+  });
+});
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
new file mode 100644
index 0000000000..4d7d466348
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
@@ -0,0 +1,37 @@
+import React, { SyntheticEvent } from 'react';
+import { logging, t } from '@superset-ui/core';
+import { Menu } from 'src/components/Menu';
+import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
+import downloadAsImage from 'src/utils/downloadAsImage';
+
+export default function DownloadAsImage({
+  text,
+  logEvent,
+  dashboardTitle,
+  addDangerToast,
+  ...rest
+}: {
+  text: string;
+  addDangerToast: Function;
+  dashboardTitle: string;
+  logEvent?: Function;
+}) {
+  const SCREENSHOT_NODE_SELECTOR = '.dashboard';
+  const onDownloadImage = async (e: SyntheticEvent) => {
+    try {
+      downloadAsImage(SCREENSHOT_NODE_SELECTOR, dashboardTitle, true)(e);
+    } catch (error) {
+      logging.error(error);
+      addDangerToast(t('Sorry, something went wrong. Try again later.'));
+    }
+    logEvent?.(LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE);
+  };
+
+  return (
+    <Menu.Item key="download-image" {...rest}>
+      <div onClick={onDownloadImage} role="button" tabIndex={0}>
+        {text}
+      </div>
+    </Menu.Item>
+  );
+}
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx
new file mode 100644
index 0000000000..371026e7aa
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx
@@ -0,0 +1,42 @@
+import React, { SyntheticEvent } from 'react';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { Menu } from 'src/components/Menu';
+import downloadAsPdf from 'src/utils/downloadAsPdf';
+import DownloadAsPdf from './DownloadAsPdf';
+
+jest.mock('src/utils/downloadAsPdf', () => ({
+  __esModule: true,
+  default: jest.fn(() => (_e: SyntheticEvent) => {}),
+}));
+
+const createProps = () => ({
+  addDangerToast: jest.fn(),
+  text: 'Export as PDF',
+  dashboardTitle: 'Test Dashboard',
+  logEvent: jest.fn(),
+});
+
+const renderComponent = () => {
+  render(
+    <Menu>
+      <DownloadAsPdf {...createProps()} />
+    </Menu>,
+  );
+};
+
+test('Should call download pdf on click', async () => {
+  const props = createProps();
+  renderComponent();
+  await waitFor(() => {
+    expect(downloadAsPdf).toBeCalledTimes(0);
+    expect(props.addDangerToast).toBeCalledTimes(0);
+  });
+
+  userEvent.click(screen.getByRole('button', { name: 'Export as PDF' }));
+
+  await waitFor(() => {
+    expect(downloadAsPdf).toBeCalledTimes(1);
+    expect(props.addDangerToast).toBeCalledTimes(0);
+  });
+});
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx
new file mode 100644
index 0000000000..eb3616b731
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx
@@ -0,0 +1,37 @@
+import React, { SyntheticEvent } from 'react';
+import { logging, t } from '@superset-ui/core';
+import { Menu } from 'src/components/Menu';
+import downloadAsPdf from 'src/utils/downloadAsPdf';
+import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF } from 'src/logger/LogUtils';
+
+export default function DownloadAsPdf({
+  text,
+  logEvent,
+  dashboardTitle,
+  addDangerToast,
+  ...rest
+}: {
+  text: string;
+  addDangerToast: Function;
+  dashboardTitle: string;
+  logEvent?: Function;
+}) {
+  const SCREENSHOT_NODE_SELECTOR = '.dashboard';
+  const onDownloadPdf = async (e: SyntheticEvent) => {
+    try {
+      downloadAsPdf(SCREENSHOT_NODE_SELECTOR, dashboardTitle, true)(e);
+    } catch (error) {
+      logging.error(error);
+      addDangerToast(t('Sorry, something went wrong. Try again later.'));
+    }
+    logEvent?.(LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF);
+  };
+
+  return (
+    <Menu.Item key="download-pdf" {...rest}>
+      <div onClick={onDownloadPdf} role="button" tabIndex={0}>
+        {text}
+      </div>
+    </Menu.Item>
+  );
+}
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx
new file mode 100644
index 0000000000..e93d7bd3ed
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import DownloadMenuItems from '.';
+
+const createProps = () => ({
+  addDangerToast: jest.fn(),
+  pdfMenuItemTitle: 'Export to PDF',
+  imageMenuItemTitle: 'Download as Image',
+  dashboardTitle: 'Test Dashboard',
+  logEvent: jest.fn(),
+});
+
+const renderComponent = () => {
+  render(<DownloadMenuItems {...createProps()} />);
+};
+
+test('Should render menu items', () => {
+  renderComponent();
+  expect(
+    screen.getByRole('menuitem', { name: 'Export to PDF' }),
+  ).toBeInTheDocument();
+  expect(
+    screen.getByRole('menuitem', { name: 'Download as Image' }),
+  ).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx
new file mode 100644
index 0000000000..a67140a004
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { Menu } from 'src/components/Menu';
+import DownloadAsImage from './DownloadAsImage';
+import DownloadAsPdf from './DownloadAsPdf';
+
+export interface DownloadMenuItemProps {
+  pdfMenuItemTitle: string;
+  imageMenuItemTitle: string;
+  addDangerToast: Function;
+  dashboardTitle: string;
+  logEvent?: Function;
+}
+
+const DownloadMenuItems = (props: DownloadMenuItemProps) => {
+  const {
+    pdfMenuItemTitle,
+    imageMenuItemTitle,
+    addDangerToast,
+    dashboardTitle,
+    logEvent,
+    ...rest
+  } = props;
+
+  return (
+    <Menu selectable={false}>
+      <DownloadAsPdf
+        text={pdfMenuItemTitle}
+        addDangerToast={addDangerToast}
+        dashboardTitle={dashboardTitle}
+        logEvent={logEvent}
+        {...rest}
+      />
+      <DownloadAsImage
+        text={imageMenuItemTitle}
+        addDangerToast={addDangerToast}
+        dashboardTitle={dashboardTitle}
+        logEvent={logEvent}
+        {...rest}
+      />
+    </Menu>
+  );
+};
+
+export default DownloadMenuItems;
diff --git a/superset-frontend/src/logger/LogUtils.ts b/superset-frontend/src/logger/LogUtils.ts
index 31fae5b0c4..2020cb67c0 100644
--- a/superset-frontend/src/logger/LogUtils.ts
+++ b/superset-frontend/src/logger/LogUtils.ts
@@ -51,6 +51,8 @@ export const LOG_ACTIONS_CONFIRM_OVERWRITE_DASHBOARD_METADATA =
   'confirm_overwrite_dashboard_metadata';
 export const LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE =
   'dashboard_download_as_image';
+export const LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF =
+  'dashboard_download_as_pdf';
 export const LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE = 'chart_download_as_image';
 export const LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV = 'chart_download_as_csv';
 export const LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV_PIVOTED =
@@ -90,6 +92,7 @@ export const LOG_EVENT_TYPE_USER = new Set([
   LOG_ACTIONS_MOUNT_EXPLORER,
   LOG_ACTIONS_CONFIRM_OVERWRITE_DASHBOARD_METADATA,
   LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE,
+  LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF,
   LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE,
 ]);
 
diff --git a/superset-frontend/src/types/dom-to-pdf.d.ts b/superset-frontend/src/types/dom-to-pdf.d.ts
new file mode 100644
index 0000000000..19ecce85b4
--- /dev/null
+++ b/superset-frontend/src/types/dom-to-pdf.d.ts
@@ -0,0 +1,19 @@
+declare module 'dom-to-pdf' {
+  interface Image {
+    type: string;
+    quality: number;
+  }
+
+  interface Options {
+    margin: number;
+    filename: string;
+    image: Image;
+    html2canvas: object;
+  }
+
+  const domToPdf = (
+    elementToPrint: Element,
+    options?: Options,
+  ): Promise<any> => {};
+  export default domToPdf;
+}
diff --git a/superset-frontend/src/utils/downloadAsImage.ts b/superset-frontend/src/utils/downloadAsImage.ts
index de74543646..79373cc76a 100644
--- a/superset-frontend/src/utils/downloadAsImage.ts
+++ b/superset-frontend/src/utils/downloadAsImage.ts
@@ -70,7 +70,7 @@ export default function downloadAsImage(
 
     return domToImage
       .toJpeg(elementToPrint, {
-        quality: 0.95,
+        quality: 1,
         bgcolor: supersetTheme.colors.grayscale.light4,
         filter,
       })
diff --git a/superset-frontend/src/utils/downloadAsImage.ts b/superset-frontend/src/utils/downloadAsPdf.ts
similarity index 66%
copy from superset-frontend/src/utils/downloadAsImage.ts
copy to superset-frontend/src/utils/downloadAsPdf.ts
index de74543646..eebca66b8b 100644
--- a/superset-frontend/src/utils/downloadAsImage.ts
+++ b/superset-frontend/src/utils/downloadAsPdf.ts
@@ -17,9 +17,9 @@
  * under the License.
  */
 import { SyntheticEvent } from 'react';
-import domToImage from 'dom-to-image-more';
+import domToPdf from 'dom-to-pdf';
 import kebabCase from 'lodash/kebabCase';
-import { t, supersetTheme } from '@superset-ui/core';
+import { logging, t } from '@superset-ui/core';
 import { addWarningToast } from 'src/components/MessageToasts/actions';
 
 /**
@@ -40,7 +40,7 @@ const generateFileStem = (description: string, date = new Date()) =>
  * @param isExactSelector if false, searches for the closest ancestor that matches selector.
  * @returns event handler
  */
-export default function downloadAsImage(
+export default function downloadAsPdf(
   selector: string,
   description: string,
   isExactSelector = false,
@@ -52,36 +52,22 @@ export default function downloadAsImage(
 
     if (!elementToPrint) {
       return addWarningToast(
-        t('Image download failed, please refresh and try again.'),
+        t('PDF download failed, please refresh and try again.'),
       );
     }
 
-    // Mapbox controls are loaded from different origin, causing CORS error
-    // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL#exceptions
-    const filter = (node: Element) => {
-      if (typeof node.className === 'string') {
-        return (
-          node.className !== 'mapboxgl-control-container' &&
-          !node.className.includes('ant-dropdown')
-        );
-      }
-      return true;
+    const options = {
+      margin: 10,
+      filename: `${generateFileStem(description)}.pdf`,
+      image: { type: 'jpeg', quality: 1 },
+      html2canvas: { scale: 2 },
     };
-
-    return domToImage
-      .toJpeg(elementToPrint, {
-        quality: 0.95,
-        bgcolor: supersetTheme.colors.grayscale.light4,
-        filter,
-      })
-      .then(dataUrl => {
-        const link = document.createElement('a');
-        link.download = `${generateFileStem(description)}.jpg`;
-        link.href = dataUrl;
-        link.click();
+    return domToPdf(elementToPrint, options)
+      .then(() => {
+        // nothing to be done
       })
-      .catch(e => {
-        console.error('Creating image failed', e);
+      .catch((e: Error) => {
+        logging.error('PDF generation failed', e);
       });
   };
 }