diff --git a/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/templates/index.test.ts b/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/templates/index.test.ts
new file mode 100644
index 0000000000000..90fdf4576c59e
--- /dev/null
+++ b/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/templates/index.test.ts
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getHeaderTemplate, getFooterTemplate, getDefaultFooterLogo } from '.';
+
+let defaultFooterLogo: string;
+
+describe('templates/index', () => {
+ beforeAll(async () => {
+ defaultFooterLogo = (await getDefaultFooterLogo()).trim();
+ });
+
+ describe('getHeaderTemplate()', () => {
+ it('works with a plain logo', async () => {
+ const title = 'plain';
+ const result = await getHeaderTemplate({ title });
+ expect(result).toBe(getHeader(title));
+ });
+ it('works with an html title', async () => {
+ const title = 'html';
+ const result = await getHeaderTemplate({ title });
+ expect(result).toBe(getHeader('<b>html</b>'));
+ });
+ });
+
+ describe('getFooterTemplate()', () => {
+ it('works with no logo', async () => {
+ const result = await getFooterTemplate({});
+ expect(result).toBe(getFooter());
+ });
+ it('works with a plain logo', async () => {
+ const logo = 'http://example.com/favico.ico';
+ const result = await getFooterTemplate({ logo });
+ expect(result).toBe(getFooter(logo));
+ });
+ it('works with an html logo', async () => {
+ const logo = '"/>
${title}
+`.trimStart();
+}
+
+function getFooter(logo?: string): string {
+ const hasLogo = !!logo;
+
+ if (!hasLogo) {
+ logo = defFooterLogo();
+ }
+ return `
+
+
+
+

+${getPoweredBy(hasLogo)}
+ of
+
+
+`.trimStart();
+}
+
+function getPoweredBy(hasLogo: boolean): string {
+ if (!hasLogo) return '';
+ return ` Powered by Elastic
\n`;
+}
+
+function defFooterLogo(): string {
+ return defaultFooterLogo!;
+}
diff --git a/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/templates/index.ts b/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/templates/index.ts
new file mode 100644
index 0000000000000..179e21ce94265
--- /dev/null
+++ b/x-pack/platform/plugins/shared/screenshotting/server/browsers/chromium/templates/index.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import fs from 'fs/promises';
+import path from 'path';
+import Handlebars, { TemplateDelegate } from '@kbn/handlebars';
+import { assetPath } from '../../../constants';
+
+// see: https://handlebarsjs.com/guide/builtin-helpers.html
+const HBCompileOptions = {
+ knownHelpersOnly: true,
+ knownHelpers: {
+ helperMissing: false,
+ blockHelperMissing: false,
+ each: false,
+ if: true,
+ unless: false,
+ with: false,
+ log: false,
+ lookup: false,
+ },
+};
+
+async function compileTemplate(pathToTemplate: string): Promise> {
+ const contentsBuffer = await fs.readFile(pathToTemplate);
+ return Handlebars.compileAST(contentsBuffer.toString(), HBCompileOptions);
+}
+
+interface HeaderTemplateInput {
+ title: string;
+}
+interface GetHeaderArgs {
+ title: string;
+}
+
+export async function getHeaderTemplate({ title }: GetHeaderArgs): Promise {
+ const template = await compileTemplate(
+ path.resolve(__dirname, './header.handlebars.html')
+ );
+ return template({ title });
+}
+
+export async function getDefaultFooterLogo(): Promise {
+ const logoBuffer = await fs.readFile(path.resolve(assetPath, 'img', 'logo-grey.png'));
+ return `data:image/png;base64,${logoBuffer.toString('base64')}`;
+}
+
+interface FooterTemplateInput {
+ base64FooterLogo: string;
+ hasCustomLogo: boolean;
+ poweredByElasticCopy: string;
+}
+
+interface GetFooterArgs {
+ logo?: string;
+}
+
+export async function getFooterTemplate({ logo }: GetFooterArgs): Promise {
+ const template = await compileTemplate(
+ path.resolve(__dirname, './footer.handlebars.html')
+ );
+ const hasCustomLogo = Boolean(logo);
+ return template({
+ base64FooterLogo: hasCustomLogo ? logo! : await getDefaultFooterLogo(),
+ hasCustomLogo,
+ poweredByElasticCopy: i18n.translate(
+ 'xpack.screenshotting.exportTypes.printablePdf.footer.logoDescription',
+ {
+ defaultMessage: 'Powered by Elastic',
+ }
+ ),
+ });
+}
diff --git a/x-pack/platform/plugins/shared/screenshotting/tsconfig.json b/x-pack/platform/plugins/shared/screenshotting/tsconfig.json
new file mode 100644
index 0000000000000..67363ecf6ffde
--- /dev/null
+++ b/x-pack/platform/plugins/shared/screenshotting/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "extends": "../../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ },
+ "include": [
+ "common/**/*",
+ "public/**/*",
+ "server/**/*",
+ "../../../../../typings/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/core",
+ { "path": "../../../../../src/setup_node_env/tsconfig.json" },
+ "@kbn/expressions-plugin",
+ "@kbn/screenshot-mode-plugin",
+ "@kbn/cloud-plugin",
+ "@kbn/utility-types",
+ "@kbn/logging",
+ "@kbn/std",
+ "@kbn/i18n",
+ "@kbn/utils",
+ "@kbn/core-logging-server-mocks",
+ "@kbn/logging-mocks",
+ "@kbn/core-http-server",
+ "@kbn/core-plugins-server",
+ "@kbn/task-manager-plugin",
+ "@kbn/screenshotting-server",
+ "@kbn/handlebars",
+ ],
+ "exclude": [
+ "target/**/*",
+ ]
+}