電子公文書(XML+XSL)のZIPファイルをChromeで表示する
電子公文書とは
電子公文書とは、電子的な形式で作成、保存、管理される公文書のことを指します。これには、政府機関や企業が発行する公式な文書が含まれます。電子公文書は、紙の文書と同様に法的効力を持つ場合があり、デジタル署名や暗号化技術を用いることで、その信頼性や安全性が確保されます。
この電子公文書の中には、XML+XSL 形式で作成されたものがあります。XML は、データを構造化して記述するためのマークアップ言語であり、XSL は、XML 文書を別の形式に変換するためのスタイルシート言語です。XML+XSL 形式の電子公文書は、Web ブラウザで表示することができますが、その表示方法にはいくつかの制約があります。
電子公文書の表示方法
電子公文書を Web ブラウザで表示するためには、XML+XSL 形式のファイルを HTML 形式に変換する必要があります。この変換は、XSLT(XSL Transformations)という技術を用いて行われます。XSLT は、XML 文書を別の形式に変換するためのスタイルシート言語であり、XSL ファイルを使って XML 文書を HTML 形式に変換することができます。
電子公文書を Web ブラウザで表示する方法として以下の内容が案内されています。
Q.XML ファイル形式の公文書ファイルを開く方法を教えてください。
https://shinsei.e-gov.go.jp/contents/help/faq/document.html
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} ArrayBuffer19 * @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 }3536 const dataView = new DataView(arrayBuffer);37 let offset = 0;38 const files = [];3940 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;5354 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 }6768 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[]} sourceFiles80 * @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 files97 .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 <div154 style="155 height: 100vh;156 display: flex;157 justify-content: center;158 align-items: center;159 "160 >161 <span162 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 here170 </span>171 </div>172 </body>173</html>
以下のアドレスで試すことが出来ます。
https://sorakumo001.github.io/xsl-viewer-html/
社会保険料の通知を表示してみました。
まとめ
Chrome で XML+XSL 形式の電子公文書を表示する方法を紹介しました。今回は外部ライブラリを使わず、標準の API だけで実装しています。やっている事自体はそれほど複雑ではないので、HTML ファイル一つで簡単に収まるレベルで済みました。