空雲 Blog

Eye catch電子公文書(XML+XSL)のZIPファイルをChromeで表示する

publication: 2024/10/07
update:2024/10/07

電子公文書とは

電子公文書とは、電子的な形式で作成、保存、管理される公文書のことを指します。これには、政府機関や企業が発行する公式な文書が含まれます。電子公文書は、紙の文書と同様に法的効力を持つ場合があり、デジタル署名や暗号化技術を用いることで、その信頼性や安全性が確保されます。

この電子公文書の中には、XML+XSL 形式で作成されたものがあります。XML は、データを構造化して記述するためのマークアップ言語であり、XSL は、XML 文書を別の形式に変換するためのスタイルシート言語です。XML+XSL 形式の電子公文書は、Web ブラウザで表示することができますが、その表示方法にはいくつかの制約があります。

電子公文書の表示方法

電子公文書を Web ブラウザで表示するためには、XML+XSL 形式のファイルを HTML 形式に変換する必要があります。この変換は、XSLT(XSL Transformations)という技術を用いて行われます。XSLT は、XML 文書を別の形式に変換するためのスタイルシート言語であり、XSL ファイルを使って XML 文書を HTML 形式に変換することができます。

電子公文書を Web ブラウザで表示する方法として以下の内容が案内されています。

Microsoft Edge の IE11 互換モードや Safari のローカルファイルの制限を無効にする方法が案内されています。

ぶっちゃけ酷いとしか言いようがありません。真っ当に表示する方法が無いのです。本来であれば、もっと汎用的なフォーマットで配布するべきです。

Chrome で表示する方法

Chrome では XML+XSL のファイルをドラッグドロップしても表示されません。しかし、XSL 自体は Web 標準で対応しているため、XSLTProcessor を使って HTML に変換することができます。ただ、API の呼び出しが必要なため、変換のためのスクリプトを組む必要があります。

ということで変換スクリプトを作りました。電子公文書は ZIP 形式で配布されているため、そのまま ZIP 形式でドラッグドロップ出来るようにしています。ブラウザは ZIP で使われている圧縮アルゴリズムの deflate には対応しているのですが、ZIP ファイルには対応していません。そのため、ZIP ファイルから圧縮データを取り出す処理は自分で書く必要があります。そして電子公文書で使われている ZIP 形式が曲者で、レガシーなシーケンシャル形式になっています。これがどういうことかというと、圧縮されたデータサイズを含んだヘッダが、データ本体よりも後にあるという、非常に扱いにくい形式です。データサイズが不明なため、後続のヘッダを先に探さないとデータが読めないのです。今、こんな形式の ZIP ファイルを作るシステムはそうそうお目にかかれません。

ZIP ファイルの展開から XML+XSL を HTML に変換するコードはこちらです。

https://github.com/SoraKumo001/xsl-viewer-html

1<!DOCTYPE html>
2<html>
3 <head>
4 <title>XSL Viewer</title>
5 <meta charset="utf-8" />
6 <meta name="viewport" content="width=device-width, initial-scale=1" />
7 <style>
8 html,
9 body {
10 min-height: 100%;
11 min-width: 100%;
12 margin: 0;
13 padding: 0;
14 }
15 </style>
16 <script>
17 /**
18 * @param {arrayBuffer} ArrayBuffer
19 * @returns {Promise<File[]>}
20 */
21 const decompressZip = async (arrayBuffer) => {
22 async function decompressData(compressedData) {
23 const reader = new Blob([compressedData])
24 .stream()
25 .pipeThrough(new DecompressionStream("deflate-raw"))
26 .getReader();
27 const chunks = [];
28 let result = await reader.read();
29 while (!result.done) {
30 chunks.push(result.value);
31 result = await reader.read();
32 }
33 return new Blob(chunks);
34 }
35
36 const dataView = new DataView(arrayBuffer);
37 let offset = 0;
38 const files = [];
39
40 while (offset < dataView.byteLength) {
41 const signature = dataView.getUint32(offset, true);
42 if (signature !== 0x04034b50) break;
43 const generalPurposeFlag = dataView.getUint16(offset + 6, true);
44 const fileNameLength = dataView.getUint16(offset + 26, true);
45 const extraFieldLength = dataView.getUint16(offset + 28, true);
46 let compressedSize = dataView.getUint32(offset + 18, true);
47 const pathName = new TextDecoder().decode(
48 arrayBuffer.slice(offset + 30, offset + 30 + fileNameLength)
49 );
50 offset += fileNameLength + extraFieldLength + 30;
51 const dataOffset = offset;
52 const isDataDescriptor = (generalPurposeFlag & 0x0008) !== 0;
53
54 if (isDataDescriptor) {
55 while (offset < dataView.byteLength) {
56 const potentialSignature = dataView.getUint32(offset, true);
57 if (potentialSignature === 0x08074b50) {
58 compressedSize = dataView.getUint32(offset + 8, true);
59 offset += 16;
60 break;
61 }
62 offset++;
63 }
64 } else {
65 offset += compressedSize;
66 }
67
68 if (pathName[pathName.length - 1] !== "/") {
69 const decompressedData = await decompressData(
70 arrayBuffer.slice(dataOffset, dataOffset + compressedSize)
71 );
72 const fileName = pathName.replace(/.*\//, "");
73 files.push(new File([decompressedData], fileName));
74 }
75 }
76 return files;
77 };
78 /**
79 * @param {File[]} sourceFiles
80 * @returns {Promise<[string, string][]>}
81 * */
82 const convertXsl = async (sourceFiles) => {
83 const unCompressPromises = sourceFiles.map(async (file) => {
84 if (file.type === "application/x-zip-compressed") {
85 return file.arrayBuffer().then(decompressZip);
86 }
87 return file;
88 });
89 const unCompressFiles = Promise.all(unCompressPromises).then(
90 (files) => {
91 return files.flat();
92 }
93 );
94 const documentFiles = unCompressFiles.then(async (files) => {
95 return Promise.all(
96 files
97 .filter((file) => file.name.match(/\.xml$|\.xsl$/))
98 .map(async (file) => {
99 const parser = new DOMParser();
100 const xml = parser.parseFromString(
101 await file.text(),
102 "application/xml"
103 );
104 return [file.name, xml];
105 })
106 );
107 });
108 return documentFiles.then((files) => {
109 const fileMap = Object.fromEntries(files);
110 const xmlDocs = files.filter(([name]) => name.endsWith(".xml"));
111 const documents = xmlDocs.map(([name, xmlDoc]) => {
112 const xsltProcessor = new XSLTProcessor();
113 const styleNodes = Array.from(xmlDoc.childNodes).filter(
114 (node) =>
115 node.nodeType === Node.PROCESSING_INSTRUCTION_NODE &&
116 node.target === "xml-stylesheet"
117 );
118 styleNodes.forEach((styleNode) => {
119 const href = styleNode.data.match(/href="([^"]+)"/)[1];
120 const xslDoc = fileMap[href];
121 if (xslDoc) xsltProcessor.importStylesheet(xslDoc);
122 });
123 const resultDoc = xsltProcessor.transformToDocument(xmlDoc);
124 const serializer = new XMLSerializer();
125 const resultString = serializer.serializeToString(resultDoc);
126 return [name, resultString];
127 });
128 return documents;
129 });
130 };
131 const handleDrop = (e) => {
132 e.preventDefault();
133 convertXsl(Array.from(e.dataTransfer.files)).then((docs) => {
134 const body = document.body;
135 body.innerHTML = "";
136 const nodes = docs.map(([name, doc]) => {
137 const contents = document.createElement("div");
138 contents.innerHTML = doc;
139 const div = document.createElement("div");
140 div.style.padding = "16px";
141 const header = document.createElement("h1");
142 header.innerText = name;
143 const hr = document.createElement("hr");
144 div.append(header, contents, hr);
145 return div;
146 });
147 body.append(...nodes);
148 });
149 };
150 </script>
151 </head>
152 <body ondragover="event.preventDefault()" ondrop="handleDrop(event)">
153 <div
154 style="
155 height: 100vh;
156 display: flex;
157 justify-content: center;
158 align-items: center;
159 "
160 >
161 <span
162 style="
163 padding: 16px;
164 font-size: 2em;
165 border: 2px dashed black;
166 border-radius: 16px;
167 "
168 >
169 Drop files or zip-file here
170 </span>
171 </div>
172 </body>
173</html>

以下のアドレスで試すことが出来ます。

https://sorakumo001.github.io/xsl-viewer-html/

社会保険料の通知を表示してみました。

screenshot

まとめ

Chrome で XML+XSL 形式の電子公文書を表示する方法を紹介しました。今回は外部ライブラリを使わず、標準の API だけで実装しています。やっている事自体はそれほど複雑ではないので、HTML ファイル一つで簡単に収まるレベルで済みました。