3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ServiceNow】クリスマスっぽいPDF出力してみる【PDFGenerationAPI】

Last updated at Posted at 2025-12-24

この記事は ServiceNow アドベントカレンダー 2025 の12月25日分の記事として執筆しています

はじめに

初めてアドベントカレンダーに参加してみました。
特にネタもなかったのですが12/25が空いていたのでクリスマスにちなんだ何かをします。

PDI環境

今回のPDI環境はこんな感じです。
Version: Zurich
image.png

何をするか

ずばりタイトルの通りクリスマスっぽいPDF出力をします。
実際ServiceNowにはOOB機能で既にPDF出力機能があるのですが
見た目のカスタマイズができなかったり、リッチテキスト項目にちゃんと対応できていなかったりしてどうしてもPDF生成処理を自前で書かないといけない場合があります。
そういう場合にPDFGenerationAPIというものが用意されています。
今回はこれをつかってクリスマスっぽいPDFを出力します。
それだけだと面白くないので透かし入れたり、PDFGenerationAPIを使った際に困った事があったので自分なりの対応方法も記事にしようかなと思います。

環境の準備

環境はカスタムワークスペースにしました。
標準のワークスペースやクラシックUIでも基本的には同じように動作すると思います。
まずはStudioを開いてCreate > Appでアプリケーションを作成します。
image.png

いい感じのクリスマスツリー画像を入れておきます。
image.png
このまま特に何も変更せずアプリ作成まで進みます。

アプリケーションが出来たら次にテーブルを作成します。
image.png

実際何でもいいですがTaskテーブルを継承して作っておきます。
image.png

AutoNumberやRoleは適宜設定ください。
ひとまず何でも良いのでテーブルを作成しました。
image.png

次にWorkspaceを作ります。
image.png

テーブル作成時のロールが自動的に選択されているのでこのままContinueします。
image.png

そしたらワークスペースが出来るのでStudioからWorkspaceを選択します。
image.png

ページにアクセスしてワークスペースが出来ている事を確認します。
ここで作成されたフォームにUIアクションを配置してPDF出力を行います。
image.png

PDF生成処理作成

それではPDF生成の処理を作っていきます。
使うリソースとしてはUIアクションとスクリプトインクルードになります。

PDF生成処理の作成

最初に核となるスクリプトインクルードを作成します。

  • GlideAjax enable:TRUE
  • Select a user role:admin
    image.png

最小限のスクリプトで動きを確認します。

  • ScriptInclude
var ChristmasPdfGenerator = Class.create();
ChristmasPdfGenerator.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

    generatePDF: function() {
        // PDF出力する為のHTML
        const html = "<html><body>Test PDF</body></html>";

        // UIアクションからパラメータ取得
        const table = this.getParameter('sysparm_table');
        const sysId = this.getParameter('sysparm_sys_id');

        // PDF生成
        const pdfAPI = new sn_pdfgeneratorutils.PDFGenerationAPI();
        const result = pdfAPI.convertToPDF(html, table, sysId, "test");
		
        if (result.status != "success") {
            return "error";
        }

        // 生成したPDFにアクセスする為のURLを生成
		const ins = gs.getProperty('glide.servlet.uri'); // インスタンスのURLを取得	
		const ret = `${ins}sys_attachment.do?sys_id=${result.attachment_id}`;
        return ret;
    },

    type: 'ChristmasPdfGenerator'
});

ボタンの作成

次にUIActionを作成し画面からPDF出力が出来るようにします。

  • UIAction
  • Name: PDF生成
  • Form Button: TRUE
  • Client: TRUE
  • Form Style: Primary
  • Workspace Form Button: TRUE
  • Format for Configurable Workspace: TRUE
function onClick(g_form) {
	// 新規レコードでは動かない
    if (g_form.isNewRecord()) {
		g_form.addErrorMessage("まず保存してください。保存後にPDFを生成できます。");
        return;
    }

    // --- ScriptInclude定義 ---
    var ga = new GlideAjax('<SCOPE_PREFIX>.<UI_ACTION_NAME>');
    ga.addParam('sysparm_name', 'generatePDF');
    ga.addParam('sysparm_table', g_form.getTableName());
    ga.addParam('sysparm_sys_id', g_form.getUniqueValue());	

    // --- ScriptInclude呼び出し ---
    ga.getXMLAnswer(function(answer) {
        if (!answer) return;

        open(answer, '_blank');
		// top.window.open(answer, '_blank'); // こちらの書き方でもよい
    });
}

上記を定義してワークスペースのフォーム画面でPDF生成処理を試してみます。
image.png

そうするとtest.pdfというPDFファイルがダウンロードされることを確認できます。
フォームをリロードする事で添付ファイルからも確認できます。
image.png

クリスマスっぽいPDFを返すようにする

ここまで来たらPDF化するHTMLをクリスマス仕様にするだけでクリスマスっぽいPDFが出力できます。
chatgptにクリスマスっぽいHTMLを作成してもらいます。
何度かやり取りしてチャッピーに作ってもらったHTMLを差し込んだScriptIncludeがこちら

  • ScriptInclude
var ChristmasPdfGenerator = Class.create();
ChristmasPdfGenerator.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

    generatePDF: function() {

        const html = `
			<!doctype html>
			<html lang="ja">
			<head>
				<meta charset="utf-8" />
				<meta name="viewport" content="width=device-width,initial-scale=1" />
				<title>Merry Christmas</title>
				<style>
				body {
					font-family: Arial, 'Noto Sans JP', sans-serif;
					background: #071428;
					color: #fff;
					margin: 0;
					padding: 24px;
				}

				/* 全体を Flex column */
				.wrapper {
					display: flex;
					flex-direction: column;
					gap: 24px;
				}
				.wrapper > * {
					flex-shrink: 0;
				}

				h1 {
					font-size: 28px;
					text-align: center;
					color: #ffd166;
					margin: 0;
				}

				.tree {
					display: flex;
					justify-content: center;
				}

				svg {
					max-width: 100%;
					height: auto;
				}

				.message {
					text-align: center;
					font-size: 16px;
					line-height: 1.6;
				}

				ul {
					padding-left: 20px;
				}
				li {
					margin: 6px 0;
				}

				/* 右寄せのための flex */
				.footer {
					display: flex;
					justify-content: flex-end;
					font-size: 12px;
					margin-top: 8px;
				}
				</style>
			</head>
				<body>
					<div class="wrapper">
						<h1>Merry Christmas</h1>
						<div class="tree">
							<svg width="200" height="240" viewBox="0 0 200 240">
							<polygon points="100,16 40,120 160,120" fill="#0b7a35"></polygon>
							<polygon points="100,70 30,160 170,160" fill="#0a6530"></polygon>
							<polygon points="100,120 18,208 182,208" fill="#065427"></polygon>
							<rect x="90" y="200" width="20" height="36" fill="#6b3f1f"></rect>
							</svg>
						</div>
						<div class="message">
							心温まるクリスマスをお過ごしください。<br>
							日々の感謝をこめて、素敵なホリデーシーズンをお祝いします。
						</div>
						<ul>
							<li>家族と過ごす幸せなひととき</li>
							<li>心温まる贈り物</li>
							<li>今年一年への感謝</li>
						</ul>
						<div class="footer">
							Generated Christmas PDF
						</div>
					</div>
				</body>
			</html>
			`;

        const table = this.getParameter('sysparm_table');
        const sysId = this.getParameter('sysparm_sys_id');

        const pdfAPI = new sn_pdfgeneratorutils.PDFGenerationAPI();
        const result = pdfAPI.convertToPDF(html, table, sysId, "Merry Christmas");

        if (result.status != "success") {
            return null;
        }
	
		// 生成したPDFにアクセスする為のURLを生成
		const ins = gs.getProperty('glide.servlet.uri'); // インスタンスのURLを取得	
		const ret = `${ins}sys_attachment.do?sys_id=${result.attachment_id}`;
        return ret;
    },

    type: 'ChristmasPdfGenerator'
});

これを実行するとこんなPDFが出力できます。
image.png

はい!
心温まりますね
素晴らしい出来栄えです!!
これでクリスマスっぽいPDFが作れるようになりました。
本当に今の生成AIの技術ってすごいなって思います。

問題点の改善

ボタンを押すたびに添付ファイルが追加される

ここまででクリスマスっぽいPDFが出力できるようになりましたが
現状のコードではボタンを押すたびに添付ファイルが追加される挙動になっています。これはPDFGenerationAPIの仕様上仕方ないのですが、やはり気になります。
その為、見た目上は添付ファイルに追加する事なくダウンロードできるようにコードを修正していきたいと思います。
方法としては単純で添付ファイルをbase64化してメモリに展開したら添付ファイルのレコードを消すだけです。

修正したスクリプトインクルードとUIアクションがこちらになります。
少しエラーチェックだったり微修正をしていますが、base64関連の処理を追加した部分については主にハイライトした箇所になります。

  • ScriptInclude
var ChristmasPdfGenerator = Class.create();
ChristmasPdfGenerator.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

	/**
	 * レスポンス作成関数
	 */
	_createResponse: function (status, message, data) {
		return JSON.stringify({
			status: status,
			message: message,
			data: data || null
		});
	},

    generatePDF: function() {

        const html = `<省略>`;

        const table = this.getParameter('sysparm_table');
        const sysId = this.getParameter('sysparm_sys_id');

		const pdfFileName = "Merry Christmas";
        const pdfAPI = new sn_pdfgeneratorutils.PDFGenerationAPI();
        const result = pdfAPI.convertToPDF(html, table, sysId, pdfFileName);

		if (result.status != "success") {
			gs.error("convertToPDF Functions is Error");
			gs.error(JSON.stringify(result));
			return this._createResponse(
					"error",
					"convertToPDF Functions is Error",
					null
			);
        }

+		const gsa = new GlideSysAttachment();
+		const attGr = new GlideRecord('sys_attachment');

+		let base64Data;
+		if(attGr.get(result.attachment_id)){
+			// base64形式で取得する為にGlideRecord型で取得する
+			base64Data = gsa.getContentBase64(attGr);
+			gs.info(`base64Data: ${base64Data}`);
			if (!base64Data) {
				gs.error("Base64 data is empty");
				return this._createResponse(
					"error",
					"Base64 data is empty",
					null
				);
			}
		}

		// 生成した添付ファイルを削除
+		if (base64Data) {
+			gsa.deleteAttachment(result.attachment_id);
+		}

		gs.info(this._createResponse(
					"success",
					"PDF generation was successful",
					{
						fileName: pdfFileName,
						base64: base64Data
					}
		));

		return this._createResponse(
					"success",
					"PDF generation was successful",
					{
						fileName: pdfFileName,
						base64: base64Data
					}
		);
    },

    type: 'ChristmasPdfGenerator'
});
  • UIアクション
function onClick(g_form) {
	// 新規レコードでは動かない
    if (g_form.isNewRecord()) {
		g_form.addErrorMessage("まず保存してください。保存後にPDFを生成できます。");
        return;
    }

    // --- ScriptInclude定義 ---
    var ga = new GlideAjax('<SCOPE_PREFIX>.ChristmasPdfGenerator');
    ga.addParam('sysparm_name', 'generatePDF');
    ga.addParam('sysparm_table', g_form.getTableName());
    ga.addParam('sysparm_sys_id', g_form.getUniqueValue());	

    // --- ScriptInclude呼び出し ---
    ga.getXMLAnswer(function(answer) {
		console.log(answer);
		const answerObj = JSON.parse(answer);
        if (answerObj.status === "error") {
			g_form.addErrorMessage(answerObj.message);
			return;
		}

+       const fileName = answerObj.data.fileName;
+		const base64Data = answerObj.data.base64;
+		const byteCharacters = atob(base64Data);
+		const byteNumbers = new Array(byteCharacters.length);
+		for (let i = 0; i < byteCharacters.length; i++){
+			byteNumbers[i] = byteCharacters.charCodeAt(i);
+		}
+		const byteArray = new Uint8Array(byteNumbers);
+		const blob = new Blob([byteArray], { type: 'application/pdf'} );
+		const link = top.document.createElement('a');
+		const url = URL.createObjectURL(blob);
+		link.href = url;
+		link.download = fileName;
+		link.click();
+		URL.revokeObjectURL(url);
    });
}

この修正でレコードの添付ファイル増殖がなくなるので気軽にボタンを連打する事が出来ます。

新規作成時はPDF出力できない

PDFGenerationAPIの仕様として添付先レコードのsys_idが必要となります。その為、対策してあげないと作成中でまだ保存していないレコードの場合はPDF出力をする事ができません。今回は内部の埋め込みHTMLをPDF化してるだけになりますが、実際はフォーム上に入力された値を用いてPDF化するケースが大半だと思います。そういった場合、レコード保存する前に出力されるPDFを確かめたいケースも考えられます。レコード内容によってはPDFが2ページになったりもします。
image.png

対策としてはいくつか考えられます。

  1. 内部的に必ず保存してからPDF生成
  2. 専用のカスタムテーブルを作成して一時レコード作成
  3. 同じテーブルに一時レコードを作成
  4. ...etc

今回は3番の方法で実装してみます。
ちなみにこの3番の注意点はChatgptがまとめてくれました。

❌ 問題になりやすい点

問題 内容
監査ログ insert / delete が履歴に残る
Business Rule After Insert が動く
Flow / Event 意図せず起動
必須項目 テーブル依存
パフォーマンス insert/delete は重い

実装としてはこちらになります。

  • ScriptInclude
    同じテーブルに一時レコードを発行し、そのレコードに添付ファイルを付けて処理が完了したら削除します。
    UIAction側からsys_idを貰う必要もなくなります。
    一時レコードを生成する関係上、必須フィールドを埋める必要があるので今回は不要ですが場合によって実装が必要になります。
var ChristmasPdfGenerator = Class.create();
ChristmasPdfGenerator.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

	/**
	 * レスポンス作成関数
	 */
	_createResponse: function (status, message, data) {
		return JSON.stringify({
			status: status,
			message: message,
			data: data || null
		});
	},

    generatePDF: function() {

        const html = `<省略>`;

        const table = this.getParameter('sysparm_table');
-       const sysId = this.getParameter('sysparm_sys_id');

+		// ----------------------------
+       // 一時レコード作成
+       // ----------------------------
+       const gr = new GlideRecord(table);
+       gr.initialize();

+       // // 必須フィールドがある場合は最低限セット
+       // if (gr.isValidField('short_description')) {
+       //     gr.setValue('short_description', 'Temporary PDF Record');
+       // }
		
+       const tempSysId = gr.insert();
+       if (!tempSysId) {
+           return this._createResponse(
+               "error",
+               "Temporary record insert failed",
+               null
+           );
+        }

		const pdfFileName = "Merry Christmas";
        const pdfAPI = new sn_pdfgeneratorutils.PDFGenerationAPI();
        const result = pdfAPI.convertToPDF(html, table, tempSysId, pdfFileName);

		if (result.status != "success") {
			gs.error("convertToPDF Functions is Error");
			gs.error(JSON.stringify(result));
			return this._createResponse(
					"error",
					"convertToPDF Functions is Error",
					null
			);
        }

		const gsa = new GlideSysAttachment();
		const attGr = new GlideRecord('sys_attachment');

		let base64Data;
		if(attGr.get(result.attachment_id)){
			// base64形式で取得する為にGlideRecord型で取得する
			base64Data = gsa.getContentBase64(attGr);
			gs.info(`base64Data: ${base64Data}`);
			if (!base64Data) {
				gs.error("Base64 data is empty");
				return this._createResponse(
					"error",
					"Base64 data is empty",
					null
				);
			}
		}

		// 生成した添付ファイルを削除
		if (base64Data) {
			gsa.deleteAttachment(result.attachment_id);
		}

+   	// ----------------------------
+   	// 一時レコード削除
+   	// ----------------------------
+   	// Reload the temporary record
+   	gr.get(tempSysId);
+   	gr.deleteRecord();

		gs.info(this._createResponse(
					"success",
					"PDF generation was successful",
					{
						fileName: pdfFileName,
						base64: base64Data
					}
		));

		return this._createResponse(
					"success",
					"PDF generation was successful",
					{
						fileName: pdfFileName,
						base64: base64Data
					}
		);
    },

    type: 'ChristmasPdfGenerator'
});

  • UIアクション

UIアクションからはハイライト赤のコードをコメントアウトか削除します。

function onClick(g_form) {
-	 // 新規レコードでは動かない
-    if (g_form.isNewRecord()) {-
-		g_form.addErrorMessage("ま-ず保存してください。保存後にPDFを生成できます。");
-        return;
-    }

    // --- ScriptInclude定義 ---
    var ga = new GlideAjax('<SCOPE_PREFIX>.ChristmasPdfGenerator');
    ga.addParam('sysparm_name', 'generatePDF');
    ga.addParam('sysparm_table', g_form.getTableName());
-   ga.addParam('sysparm_sys_id', g_form.getUniqueValue());	

    // --- ScriptInclude呼び出し ---
    ga.getXMLAnswer(function(answer) {
		console.log(answer);
		const answerObj = JSON.parse(answer);
        if (answerObj.status === "error") {
			g_form.addErrorMessage(answerObj.message);
			return;
		}

        var fileName = answerObj.data.fileName;
		var base64Data = answerObj.data.base64;
		const byteCharacters = atob(base64Data);
		const byteNumbers = new Array(byteCharacters.length);
		for (let i = 0; i < byteCharacters.length; i++){
			byteNumbers[i] = byteCharacters.charCodeAt(i);
		}
		const byteArray = new Uint8Array(byteNumbers);
		const blob = new Blob([byteArray], { type: 'application/pdf'} );
		const link = top.document.createElement('a');
		const url = URL.createObjectURL(blob);
		link.href = url;
		link.download = fileName;
		link.click();
		URL.revokeObjectURL(url);
    });
}

こうする事で作成中のレコードでもPDF生成を行う事ができ、入力中の値でPDFの確認をする事が出来ます。

透かしとか入れてみる

折角なので透かしっぽいのをPDFに入れてみたいと思います。
静的なHTMLであればsvgや画像を半透明化しcssで対応する事も可能ですが、PDFGenerationAPIは複数ページになる場合もあるのでページ対応が必要になります。
折角なのでServiceNowのAPIを使って実現したいと思います。
修正するのはScriptIncldeのみになります。

var ChristmasPdfGenerator = Class.create();
ChristmasPdfGenerator.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

	/**
	 * レスポンス作成関数
	 */
	_createResponse: function (status, message, data) {
		return JSON.stringify({
			status: status,
			message: message,
			data: data || null
		});
	},

+	/* -----------------------------
+    * 透かしSVG生成(繰り返し・斜め)
+    * ----------------------------- */
+    _createWatermarkSVG: function (text, pageW, pageH) {
+
+       const fontSize = Math.floor(Math.min(pageW, pageH) * 0.12);
+       const stepX = Math.floor(pageW * 0.6);
+       const stepY = Math.floor(pageH * 0.35);
+
+       let texts = '';
+
+       for (let y = -pageH; y < pageH * 2; y += stepY) {
+           for (let x = -pageW; x < pageW * 2; x += stepX) {
+               texts += `
+                   <text
+                       x="${x}"
+                       y="${y}"
+                       font-family="Arial"
+                       font-size="${fontSize}"
+                       fill="#999999"
+                       fill-opacity="0.12"
+                       transform="rotate(-30 ${x} ${y})">
+                       ${text}
+                   </text>
+               `;
+            }
+       }
+
+       return `
+           <svg xmlns="http://www.w3.org/2000/svg"
+                width="${pageW}"
+                height="${pageH}">
+                ${texts}
+            </svg>
+       `;
+   },

    generatePDF: function() {

        const html = `<省略>`;

        const table = this.getParameter('sysparm_table');
        const sysId = this.getParameter('sysparm_sys_id');

		/* ---------- 一時レコード作成 ---------- */
        const gr = new GlideRecord(table);
        gr.initialize();

        // // 必須フィールドがある場合は最低限セット
        // if (gr.isValidField('short_description')) {
        //     gr.setValue('short_description', 'Temporary PDF Record');
        // }
		
		const tempSysId = gr.insert();
        if (!tempSysId) {
            return this._createResponse(
                "error",
                "Temporary record insert failed",
                null
            );
        }

		/* ---------- PDF生成 ---------- */
		const pdfFileName = "Merry Christmas";
        const pdfAPI = new sn_pdfgeneratorutils.PDFGenerationAPI();
        const pdfResult  = pdfAPI.convertToPDF(html, table, tempSysId, pdfFileName);

		if (pdfResult .status != "success") {
			gs.error("convertToPDF Functions is Error");
			gs.error(JSON.stringify(pdfResult ));
			return this._createResponse(
					"error",
					"convertToPDF Functions is Error",
					null
			);
        }

+		/* ---------- ページサイズ取得 ---------- */
+       const pageInfo = pdfAPI.getPdfPageSizes(pdfResult.attachment_id);
+
+		/* ---------- 透かし重ね ---------- */
+       const svgApi = new sn_pdfgeneratorutils.SVGToPDFConversionAPI();
+       let currentPdfId = pdfResult.attachment_id;
+		const pageCount = Object.keys(pageInfo.pages_size).length;
+		const watermarkText = "☆*Merry X'mas*☆";
+		for (let i = 1; i <= pageCount; i++) {
+           const pageW = pageInfo.pages_size[i][0]; // width
+           const pageH = pageInfo.pages_size[i][1]; // height
+
+           const svg = this._createWatermarkSVG(
+               watermarkText,
+               pageW,
+               pageH
+           );
+
+           const out = svgApi.addSVGToPDF(
+               svg,
+               currentPdfId,
+               table,
+               tempSysId,
+               pdfFileName,
+               i,
+               0,
+               0,
+               pageW,
+               pageH
+           );
+
+           currentPdfId = out.attachment_id;
+       }

		const gsa = new GlideSysAttachment();
		const attGr = new GlideRecord('sys_attachment');

		/* ---------- base64取得 ---------- */
		let base64Data;
		if(attGr.get(currentPdfId)){
			// base64形式で取得する為にGlideRecord型で取得する
			base64Data = gsa.getContentBase64(attGr);
			gs.info(`base64Data: ${base64Data}`);
			if (!base64Data) {
				gs.error("Base64 data is empty");
				return this._createResponse(
					"error",
					"Base64 data is empty",
					null
				);
			}
		}

+		/* ---------- 添付全削除 ---------- */
+    	attGr.addQuery('table_sys_id', tempSysId);
+		attGr.query();
+		while(attGr.next())
+		{
+			gsa.deleteAttachment(attGr.getUniqueValue());
+		}

		// ----------------------------
		// 一時レコード削除
		// ----------------------------
		// Reload the temporary record
		gr.get(tempSysId);
		gr.deleteRecord();

		gs.info(this._createResponse(
					"success",
					"PDF generation was successful",
					{
						fileName: pdfFileName,
						base64: base64Data
					}
		));

		return this._createResponse(
					"success",
					"PDF generation was successful",
					{
						fileName: pdfFileName,
						base64: base64Data
					}
		);
    },

    type: 'ChristmasPdfGenerator'
});

この修正を行う事で出力されるPDFはこちらになります。
image.png

SVGToPDFConversionAPIについてですが、ポイントとピクセル指定が混在していたり、戻り値がオブジェクト形式になっていて実際に見てみないと分かりづらかったりかなり癖のある印象です。
なにはともあれクリスマスっぽいPDFに透かしを入れることが出来ました。

さいごに

いかがでしたでしょうか。
PDF出力はサンプルを探せば結構出てくると思いますのであまり目新しい情報ではないかもしれませんが何らかの参考になれば幸いです。

それでは良きホリデーシーズン
心温まるクリスマスをお過ごしください。

※コードに関しては生成AIも利用しており、充分にデバッグなどもしておりませんのでご注意ください。
色々粗があると思いますが温かい目で見てください

また、ServiceNowにおいては標準機能を利用する事が推奨されておりますので、大前提としてそちらの利用をはじめにご検討頂きますようお願い致します。

最終的な完成コード

  • ScriptInclude
var ChristmasPdfGenerator = Class.create();
ChristmasPdfGenerator.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

	/**
	 * レスポンス作成関数
	 */
	_createResponse: function (status, message, data) {
		return JSON.stringify({
			status: status,
			message: message,
			data: data || null
		});
	},

	/* -----------------------------
     * 透かしSVG生成(繰り返し・斜め)
     * ----------------------------- */
    _createWatermarkSVG: function (text, pageW, pageH) {

        const fontSize = Math.floor(Math.min(pageW, pageH) * 0.12);
        const stepX = Math.floor(pageW * 0.6);
        const stepY = Math.floor(pageH * 0.35);

        let texts = '';

        for (let y = -pageH; y < pageH * 2; y += stepY) {
            for (let x = -pageW; x < pageW * 2; x += stepX) {
                texts += `
                    <text
                        x="${x}"
                        y="${y}"
                        font-family="Arial"
                        font-size="${fontSize}"
                        fill="#999999"
                        fill-opacity="0.12"
                        transform="rotate(-30 ${x} ${y})">
                        ${text}
                    </text>
                `;
            }
        }

        return `
            <svg xmlns="http://www.w3.org/2000/svg"
                 width="${pageW}"
                 height="${pageH}">
                ${texts}
            </svg>
        `;
    },

    generatePDF: function() {

        const html = `
			<!doctype html>
			<html lang="ja">
			<head>
				<meta charset="utf-8" />
				<meta name="viewport" content="width=device-width,initial-scale=1" />
				<title>Merry Christmas</title>
				<style>
				body {
					font-family: Arial, 'Noto Sans JP', sans-serif;
					background: #071428;
					color: #fff;
					margin: 0;
					padding: 24px;
				}

				/* 全体を Flex column */
				.wrapper {
					display: flex;
					flex-direction: column;
					gap: 24px;
				}
				.wrapper > * {
					flex-shrink: 0;
				}

				h1 {
					font-size: 28px;
					text-align: center;
					color: #ffd166;
					margin: 0;
				}

				.tree {
					display: flex;
					justify-content: center;
				}

				svg {
					max-width: 100%;
					height: auto;
				}

				.message {
					text-align: center;
					font-size: 16px;
					line-height: 1.6;
				}

				ul {
					padding-left: 20px;
				}
				li {
					margin: 6px 0;
				}

				/* 右寄せのための flex */
				.footer {
					display: flex;
					justify-content: flex-end;
					font-size: 12px;
					margin-top: 8px;
				}
				</style>
			</head>
				<body>
					<div class="wrapper">
						<h1>Merry Christmas</h1>
						<div class="tree">
							<svg width="200" height="240" viewBox="0 0 200 240">
							<polygon points="100,16 40,120 160,120" fill="#0b7a35"></polygon>
							<polygon points="100,70 30,160 170,160" fill="#0a6530"></polygon>
							<polygon points="100,120 18,208 182,208" fill="#065427"></polygon>
							<rect x="90" y="200" width="20" height="36" fill="#6b3f1f"></rect>
							</svg>
						</div>
						<div class="message">
							心温まるクリスマスをお過ごしください。<br>
							日々の感謝をこめて、素敵なホリデーシーズンをお祝いします。
						</div>
						<ul>
							<li>家族と過ごす幸せなひととき</li>
							<li>心温まる贈り物</li>
							<li>今年一年への感謝</li>
						</ul>
						<div class="footer">
							Generated Christmas PDF
						</div>
					</div>
				</body>
			</html>
			`;

        const table = this.getParameter('sysparm_table');
        const sysId = this.getParameter('sysparm_sys_id');

		/* ---------- 一時レコード作成 ---------- */
        const gr = new GlideRecord(table);
        gr.initialize();

        // // 必須フィールドがある場合は最低限セット
        // if (gr.isValidField('short_description')) {
        //     gr.setValue('short_description', 'Temporary PDF Record');
        // }
		
		const tempSysId = gr.insert();
        if (!tempSysId) {
            return this._createResponse(
                "error",
                "Temporary record insert failed",
                null
            );
        }

		/* ---------- PDF生成 ---------- */
		const pdfFileName = "Merry Christmas";
        const pdfAPI = new sn_pdfgeneratorutils.PDFGenerationAPI();
        const pdfResult  = pdfAPI.convertToPDF(html, table, tempSysId, pdfFileName);

		if (pdfResult .status != "success") {
			gs.error("convertToPDF Functions is Error");
			gs.error(JSON.stringify(pdfResult ));
			return this._createResponse(
					"error",
					"convertToPDF Functions is Error",
					null
			);
        }

		/* ---------- ページサイズ取得 ---------- */
        const pageInfo = pdfAPI.getPdfPageSizes(pdfResult.attachment_id);

		/* ---------- 透かし重ね ---------- */
        const svgApi = new sn_pdfgeneratorutils.SVGToPDFConversionAPI();
        let currentPdfId = pdfResult.attachment_id;
		const pageCount = Object.keys(pageInfo.pages_size).length;
		const watermarkText = "☆*Merry X'mas*☆";
		for (let i = 1; i <= pageCount; i++) {
            const pageW = pageInfo.pages_size[i][0]; // width
            const pageH = pageInfo.pages_size[i][1]; // height

            const svg = this._createWatermarkSVG(
                watermarkText,
                pageW,
                pageH
            );

            const out = svgApi.addSVGToPDF(
                svg,
                currentPdfId,
                table,
                tempSysId,
                pdfFileName,
                i,
                0,
                0,
                pageW,
                pageH
            );

            currentPdfId = out.attachment_id;
        }

		const gsa = new GlideSysAttachment();
		const attGr = new GlideRecord('sys_attachment');

		/* ---------- base64取得 ---------- */
		let base64Data;
		if(attGr.get(currentPdfId)){
			// base64形式で取得する為にGlideRecord型で取得する
			base64Data = gsa.getContentBase64(attGr);
			gs.info(`base64Data: ${base64Data}`);
			if (!base64Data) {
				gs.error("Base64 data is empty");
				return this._createResponse(
					"error",
					"Base64 data is empty",
					null
				);
			}
		}

		/* ---------- 添付全削除 ---------- */
		attGr.addQuery('table_sys_id', tempSysId);
		attGr.query();
		while(attGr.next())
		{
			gsa.deleteAttachment(attGr.getUniqueValue());
		}

		// ----------------------------
		// 一時レコード削除
		// ----------------------------
		// Reload the temporary record
		gr.get(tempSysId);
		gr.deleteRecord();

		gs.info(this._createResponse(
					"success",
					"PDF generation was successful",
					{
						fileName: pdfFileName,
						base64: base64Data
					}
		));

		return this._createResponse(
					"success",
					"PDF generation was successful",
					{
						fileName: pdfFileName,
						base64: base64Data
					}
		);
    },

    type: 'ChristmasPdfGenerator'
});

  • UIAction
function onClick(g_form) {
	// // 新規レコードでは動かない
    // if (g_form.isNewRecord()) {
	// 	g_form.addErrorMessage("まず保存してください。保存後にPDFを生成できます。");
    //     return;
    // }

    // --- ScriptInclude定義 ---
    var ga = new GlideAjax('<SCOPE_PREFIX>.ChristmasPdfGenerator');
    ga.addParam('sysparm_name', 'generatePDF');
    ga.addParam('sysparm_table', g_form.getTableName());
    ga.addParam('sysparm_sys_id', g_form.getUniqueValue());	

    // --- ScriptInclude呼び出し ---
    ga.getXMLAnswer(function(answer) {
		console.log(answer);
		const answerObj = JSON.parse(answer);
        if (answerObj.status === "error") {
			g_form.addErrorMessage(answerObj.message);
			return;
		}

        var fileName = answerObj.data.fileName;
		var base64Data = answerObj.data.base64;
		const byteCharacters = atob(base64Data);
		const byteNumbers = new Array(byteCharacters.length);
		for (let i = 0; i < byteCharacters.length; i++){
			byteNumbers[i] = byteCharacters.charCodeAt(i);
		}
		const byteArray = new Uint8Array(byteNumbers);
		const blob = new Blob([byteArray], { type: 'application/pdf'} );
		const link = top.document.createElement('a');
		const url = URL.createObjectURL(blob);
		link.href = url;
		link.download = fileName;
		link.click();
		URL.revokeObjectURL(url);
    });
}

参考URL

以下のURLを参考にしました。有益なサイト達に感謝します。
【ServiceNow】スクリプトを使用してHTMLでフォーマットした形式でPDFを出力する方法
Demystifying glide.servlet.uri in ServiceNow
Javascriptでbase64エンコード化されたExcelファイルをダウンロードする
PDFGenerationAPI - スコープ付き、グローバル
SVGToPDFConversionAPI - スコープ付き、グローバル)

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?