Files
time/ventures/docscope/index.html
T

934 lines
49 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Docscope — see what an Office file still hides</title>
<meta name="description" content="Drop a .docx, .xlsx or .pptx. Docscope unzips it in your browser and shows every OOXML part, plus a leak report for authors, tracked changes, thumbnails, EXIF and hidden sheets. Nothing is uploaded.">
<style>
:root {
--desk: #1c3a5f;
--bezel: #c5ccd6;
--bezel-hi: #f4f7fb;
--bezel-lo: #8b95a3;
--face: #e7edf4;
--paper: #f7f4ec;
--ink: #1a1d22;
--ink-soft: #3c4450;
--mute: #5c6774;
--rule: #c5ccd4;
--title: #0b3d73;
--title-2: #082c54;
--amber: #e2a21a;
--amber-ink: #3a2a04;
--danger: #9c2b2b;
--danger-bg: #f8e6e4;
--ok: #1d6b3a;
--ok-bg: #e5f3ea;
--warn: #8a5a08;
--warn-bg: #fbf1d8;
--info: #1d4e89;
--info-bg: #e6eef8;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
--sans: "Segoe UI", "Avenir Next", "Helvetica Neue", Arial, sans-serif;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--ink);
font: 16px/1.55 var(--sans);
background:
linear-gradient(180deg, rgba(255,255,255,.04), transparent 80px),
repeating-linear-gradient(0deg, transparent, transparent 22px, rgba(0,0,0,.08) 23px),
var(--desk);
}
a { color: var(--title); text-underline-offset: 3px; }
a:hover { color: var(--danger); }
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 18px; }
.win {
background: var(--bezel);
border: 2px solid var(--bezel-hi);
border-right-color: var(--bezel-lo);
border-bottom-color: var(--bezel-lo);
box-shadow: 4px 4px 0 rgba(0,0,0,.28);
}
.titlebar {
display: flex; align-items: center; gap: 8px;
background: linear-gradient(180deg, #1a5eae, var(--title) 55%, var(--title-2));
color: #fff;
padding: 5px 6px 5px 8px;
font: 700 13px/1.2 var(--sans);
user-select: none;
}
.titlebar .glyph {
width: 16px; height: 14px; flex: none;
background:
linear-gradient(#fff, #fff) 2px 3px/10px 2px no-repeat,
linear-gradient(#9fd0ff, #9fd0ff) 2px 7px/10px 5px no-repeat,
#0b3d73;
border: 1px solid #d6e8ff;
}
.titlebar .caption { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tb-btn {
width: 18px; height: 16px; flex: none;
background: var(--bezel);
color: var(--ink);
border: 1px solid var(--bezel-hi);
border-right-color: var(--bezel-lo);
border-bottom-color: var(--bezel-lo);
font: 700 11px/14px var(--mono);
text-align: center;
padding: 0;
}
.face { background: var(--face); padding: 14px; }
.menu {
display: flex; gap: 2px; flex-wrap: wrap;
background: var(--bezel);
border-bottom: 1px solid var(--bezel-lo);
padding: 2px 6px;
font: 13px/1.4 var(--sans);
}
.menu span { padding: 1px 8px; }
.menu span u { text-decoration: underline; text-underline-offset: 2px; }
header.hero { padding: 28px 0 8px; }
.kicker {
margin: 0 0 10px;
color: #d6e6f7;
font: 12px/1 var(--mono);
letter-spacing: .16em;
text-transform: uppercase;
}
h1 {
margin: 0;
color: #fff;
font: 700 clamp(40px, 8vw, 68px)/.92 var(--sans);
letter-spacing: -.03em;
}
h1 span { color: var(--amber); }
.promise {
margin: 14px 0 18px;
max-width: 40em;
color: #d5e3f2;
font-size: clamp(16px, 2.2vw, 19px);
}
.hero-actions { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.btn {
display: inline-block;
background: var(--bezel);
color: var(--ink);
border: 2px solid var(--bezel-hi);
border-right-color: #5d6774;
border-bottom-color: #5d6774;
padding: 9px 16px;
font: 700 15px/1.2 var(--sans);
text-decoration: none;
cursor: pointer;
touch-action: manipulation;
box-shadow: 1px 1px 0 #000;
}
.btn:hover { background: #fff; color: var(--ink); }
.btn:active {
border-color: #5d6774;
border-right-color: var(--bezel-hi);
border-bottom-color: var(--bezel-hi);
transform: translate(1px, 1px);
box-shadow: none;
}
.btn.primary { background: var(--amber); color: var(--amber-ink); }
.btn.primary:hover { background: #f0b52e; color: var(--amber-ink); }
.try-link { color: #fff; font-size: 15px; }
.try-link:hover { color: var(--amber); }
.fineprint { margin: 12px 0 0; color: #b7c9dc; font: 12.5px/1.45 var(--mono); }
section { padding: 22px 0; }
.sec-label {
margin: 0 0 8px;
color: #c5d7ea;
font: 11px/1 var(--mono);
letter-spacing: .16em;
text-transform: uppercase;
}
.lede { margin: 0 0 14px; color: var(--ink-soft); max-width: 68ch; }
.statusline {
display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
margin-top: 10px;
padding: 4px 8px;
background: var(--bezel);
border-top: 1px solid var(--bezel-lo);
color: var(--ink-soft);
font: 12px/1.4 var(--mono);
}
.drop {
border: 2px dashed #7d8b9c;
background: var(--paper);
padding: 16px;
text-align: center;
margin-bottom: 12px;
}
.drop.hot { border-color: var(--title); background: #fff; }
.drop strong { display: block; font-size: 16px; }
.drop p { margin: 6px 0 0; color: var(--mute); font-size: 13.5px; }
.file-input { margin-top: 8px; max-width: 100%; font: 13px/1.3 var(--sans); }
.layout { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); gap: 12px; }
.panel {
background: var(--paper);
border: 2px solid #fff;
border-right-color: #9aa3ae;
border-bottom-color: #9aa3ae;
min-width: 0;
}
.panel h3 {
margin: 0;
padding: 6px 10px;
background: var(--bezel);
border-bottom: 1px solid var(--bezel-lo);
font: 700 13px/1.3 var(--sans);
}
.panel .body { padding: 10px; }
.meta-line { margin: 0 0 8px; font: 12.5px/1.45 var(--mono); color: var(--mute); overflow-wrap: anywhere; }
.tree { margin: 0; padding: 0; list-style: none; font: 13px/1.35 var(--mono); }
.tree ul { margin: 0; padding: 0 0 0 14px; list-style: none; border-left: 1px dotted #b7c0ca; }
.part {
display: flex; align-items: baseline; gap: 8px; width: 100%;
text-align: left; background: transparent; border: 0; border-left: 3px solid transparent;
padding: 4px 6px; font: inherit; color: var(--ink); cursor: pointer; touch-action: manipulation;
}
.part:hover, .part.on { background: #fff6d8; border-left-color: var(--amber); }
.part .nm { overflow-wrap: anywhere; }
.part .sz { margin-left: auto; color: var(--mute); font-size: 11px; white-space: nowrap; }
.dot { width: 8px; height: 8px; flex: none; background: #8aa0b8; }
.dot.leak { background: var(--danger); }
.dot.ok { background: var(--ok); }
.dot.bin { background: #6a5acd; }
.folder-name { padding: 5px 6px 2px; font-weight: 700; color: var(--title); }
.findings { display: grid; gap: 8px; }
.finding { border: 1px solid var(--rule); background: #fff; padding: 8px 10px; font-size: 14px; }
.finding .tag {
display: inline-block; margin-right: 6px; padding: 0 6px;
font: 700 11px/1.6 var(--mono); letter-spacing: .04em; text-transform: uppercase;
}
.sev-leak { background: var(--danger-bg); }
.sev-leak .tag { background: var(--danger); color: #fff; }
.sev-warn { background: var(--warn-bg); }
.sev-warn .tag { background: var(--warn); color: #fff; }
.sev-info { background: var(--info-bg); }
.sev-info .tag { background: var(--info); color: #fff; }
.sev-ok { background: var(--ok-bg); }
.sev-ok .tag { background: var(--ok); color: #fff; }
.finding p { margin: 4px 0 0; }
.finding code, .anno code, .limits code, .ref code { font: 12px/1.4 var(--mono); background: #eef2f6; padding: 0 4px; }
.raw-wrap { display: grid; gap: 8px; }
.anno { margin: 0; font-size: 14px; color: var(--ink-soft); }
.verdict { display: inline-block; margin-top: 4px; padding: 1px 7px; font: 700 12px/1.6 var(--mono); }
.verdict.no { background: var(--danger); color: #fff; }
.verdict.careful { background: var(--warn); color: #fff; }
.verdict.yes { background: var(--ok); color: #fff; }
pre.xml {
margin: 0; max-height: 340px; overflow: auto; padding: 10px;
background: #11161c; color: #e7eef6; font: 12px/1.45 var(--mono);
white-space: pre-wrap; overflow-wrap: anywhere;
}
.err { color: var(--danger); font-weight: 700; }
.ref { width: 100%; border-collapse: collapse; font-size: 14px; }
.ref th, .ref td { text-align: left; vertical-align: top; padding: 8px; border-bottom: 1px solid var(--rule); }
.ref th { font: 700 12px/1.3 var(--mono); letter-spacing: .04em; text-transform: uppercase; color: var(--mute); }
.repos { display: grid; }
.repo {
display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 16px;
padding: 12px 0; border-top: 1px solid var(--rule);
}
.repo a { font: 600 15px/1.3 var(--mono); }
.stars { font: 13px/1 var(--mono); color: var(--mute); white-space: nowrap; }
.stars b { color: var(--danger); }
.repo .note { grid-column: 1 / -1; margin: 0; color: var(--mute); font-size: 13.5px; }
.tiers { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.tier {
background: var(--paper);
border: 2px solid #fff; border-right-color: #8b95a3; border-bottom-color: #8b95a3;
padding: 14px 14px 12px; display: flex; flex-direction: column; min-width: 0;
}
.tier.featured { background: #fffdf6; outline: 2px solid var(--amber); }
.tier-name { margin: 0 0 6px; font: 700 12px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--title); }
.price { margin: 0; font: 700 26px/1.15 var(--sans); letter-spacing: -.02em; }
.price small { display: block; margin-top: 4px; font: 400 13px/1.35 var(--sans); color: var(--mute); }
.for-whom { margin: 8px 0; color: var(--mute); font-size: 13.5px; }
.tier ul { margin: 0 0 14px; padding-left: 1.1em; font-size: 14px; }
.tier .btn { margin-top: auto; text-align: center; }
.nothing-charged { margin: 14px 0 0; text-align: center; font-style: italic; font-size: 17px; }
.limits { margin: 0; padding-left: 1.2em; max-width: 70ch; }
.limits li { margin: 0 0 8px; }
.steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.step { background: var(--paper); border: 1px solid var(--rule); padding: 12px; }
.step .n { margin: 0; font: 700 28px/1 var(--mono); color: var(--title); }
.step h3 { margin: 6px 0; font-size: 16px; }
.step p { margin: 0; color: var(--ink-soft); font-size: 14px; }
footer.desk-foot { padding: 8px 0 40px; color: #c5d7ea; font-size: 13.5px; }
footer.desk-foot a { color: #fff; }
footer.desk-foot p { margin: 0 0 6px; }
@media (max-width: 860px) {
.layout, .tiers, .steps { grid-template-columns: 1fr; }
.repo { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.face { padding: 10px; }
pre.xml { max-height: 240px; }
}
</style>
</head>
<body>
<header class="hero">
<div class="wrap">
<p class="kicker">Time · Venture · Docscope</p>
<div class="win">
<div class="titlebar">
<span class="glyph" aria-hidden="true"></span>
<span class="caption">Docscope — Office Package Inspector</span>
<span class="tb-btn" aria-hidden="true">_</span>
<span class="tb-btn" aria-hidden="true">□</span>
<span class="tb-btn" aria-hidden="true">×</span>
</div>
<div class="face" style="background:#123152;color:#fff">
<h1>Doc<span>scope</span></h1>
<p class="promise">Drop a .docx, .xlsx or .pptx and see everything still inside it — author names, tracked deletions, thumbnails, comments — dissected on this machine. Nothing is uploaded.</p>
<div class="hero-actions">
<a class="btn primary" href="https://github.com/teahubxyz/time/issues/new?template=early-access.yml&amp;title=Early+access%3A+Docscope&amp;venture=docscope&amp;plan=Pro+%28founding%29">Get early access</a>
<a class="try-link" href="#try">Inspect the sample ↓</a>
</div>
<p class="fineprint">Opens a public GitHub issue. No email, no payment. Nothing is charged today.</p>
</div>
</div>
</div>
</header>
<main>
<section id="try">
<div class="wrap">
<p class="sec-label">The product</p>
<div class="win" id="app">
<div class="titlebar">
<span class="glyph" aria-hidden="true"></span>
<span class="caption" id="win-caption">Docscope — waiting for a package</span>
<span class="tb-btn" aria-hidden="true">_</span>
<span class="tb-btn" aria-hidden="true">□</span>
<span class="tb-btn" aria-hidden="true">×</span>
</div>
<div class="menu" aria-hidden="true"><span><u>F</u>ile</span><span><u>V</u>iew</span><span><u>P</u>arts</span><span><u>H</u>elp</span></div>
<div class="face">
<div class="drop" id="drop">
<strong>Drag a .docx, .xlsx or .pptx here</strong>
<p>Unzipped locally with <code>DecompressionStream</code>. The page works offline once loaded. The list below is the ZIP central directory, verbatim — nothing is invented.</p>
<input class="file-input" id="file" type="file" accept=".docx,.xlsx,.pptx,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation" aria-label="Choose an Office file">
</div>
<p class="meta-line" id="pkg-meta">Loading the preloaded sample through the same unzipper…</p>
<div class="layout">
<div class="panel">
<h3>Exploded package</h3>
<div class="body" id="tree"><p class="meta-line">No parts yet.</p></div>
</div>
<div class="panel">
<h3>Leak report</h3>
<div class="body" id="report"><p class="meta-line">No findings yet.</p></div>
</div>
</div>
<div class="panel" style="margin-top:12px">
<h3 id="part-title">Part inspector</h3>
<div class="body raw-wrap" id="inspector">
<p class="anno">Click any part in the diagram. Raw XML (or a hex preview of a binary part) appears here, with a plain-English note and a safe-to-send verdict.</p>
<pre class="xml" id="raw">(nothing selected)</pre>
</div>
</div>
<div class="statusline">
<span id="status-left">client-side · no upload</span>
<span id="status-right">ECMA-376</span>
</div>
</div>
</div>
</div>
</section>
<section id="reference">
<div class="wrap">
<p class="sec-label">Reference</p>
<div class="win">
<div class="titlebar"><span class="glyph" aria-hidden="true"></span><span class="caption">OOXML parts this page actually handles</span></div>
<div class="face">
<p class="lede">Only parts the inspector reads are listed. Each link goes to the ECMA-376 5th edition page. Parts we do not understand still appear in the diagram, labelled as unhandled.</p>
<table class="ref">
<thead><tr><th>Part</th><th>What we read</th><th>Spec</th></tr></thead>
<tbody>
<tr><td><code>[Content_Types].xml</code></td><td>Default and override content types for every part</td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 2, §10.1.2</a></td></tr>
<tr><td><code>_rels/.rels</code> and <code>*.rels</code></td><td>Package and part relationships, including the thumbnail relationship</td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 2, §9.2 / §10.2</a></td></tr>
<tr><td><code>docProps/core.xml</code></td><td><code>dc:creator</code>, <code>cp:lastModifiedBy</code>, title, revision, created, modified</td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 2, §11.1 (Dublin Core)</a></td></tr>
<tr><td><code>docProps/app.xml</code></td><td>Application, company, template, pages / words</td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 1, §22.2 extended properties</a></td></tr>
<tr><td><code>docProps/thumbnail.jpeg</code></td><td>Embedded preview, plus JPEG APP1 EXIF text</td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 2, thumbnail relationship</a></td></tr>
<tr><td><code>word/document.xml</code></td><td><code>w:ins</code> / <code>w:del</code> tracked changes, comment markers, visible text</td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 1, §17.13 revisions</a></td></tr>
<tr><td><code>word/comments.xml</code></td><td>Comment author, date, and text</td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 1, §17.13.4</a></td></tr>
<tr><td><code>word/people.xml</code></td><td>Presence authors and user ids</td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 1, people part</a></td></tr>
<tr><td><code>word/styles.xml</code></td><td>Style ids and names (annotation only)</td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 1, §17.7</a></td></tr>
<tr><td><code>word/media/*</code></td><td>Embedded images; JPEG EXIF and PNG tEXt / iTXt</td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 1, §15.2.14 images</a></td></tr>
<tr><td><code>xl/workbook.xml</code></td><td>Sheet names and <code>state="hidden"</code> / <code>veryHidden</code></td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 1, §18.2 workbook</a></td></tr>
<tr><td><code>ppt/presentation.xml</code></td><td>Slides with <code>show="0"</code></td><td><a href="https://ecma-international.org/publications-and-standards/standards/ecma-376/">ECMA-376 Part 1, §19.2 presentation</a></td></tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<section id="how">
<div class="wrap">
<p class="sec-label">How it works</p>
<div class="steps">
<div class="step">
<p class="n">1</p>
<h3>Drop the package</h3>
<p>A .docx, .xlsx or .pptx is a ZIP. The page reads the central directory and inflates each part with the browser's <code>DecompressionStream</code>. Stored and deflate only — the methods Office writes.</p>
</div>
<div class="step">
<p class="n">2</p>
<h3>Read the leak report</h3>
<p>Core properties, tracked insertions and deletions, comments, people.xml, thumbnail parts, JPEG EXIF, PNG text, and hidden sheets or slides are flagged from the XML itself.</p>
</div>
<div class="step">
<p class="n">3</p>
<h3>Open any part</h3>
<p>Click a name in the diagram. You get the raw XML, or a hex preview if the part is binary, plus a plain-English note on what it does and whether it is safe to send.</p>
</div>
</div>
</div>
</section>
<section id="why">
<div class="wrap">
<p class="sec-label">Why this exists</p>
<div class="win">
<div class="titlebar"><span class="glyph" aria-hidden="true"></span><span class="caption">Why this exists — the repos this rides</span></div>
<div class="face">
<p class="lede">People are generating Office files from plain text again. Docscope rides the trending repos wordcraft, gridcraft and deckcraft: builders starring those projects need to see what the package they just wrote actually contains before it is emailed.</p>
<div class="repos">
<div class="repo">
<a href="https://github.com/storytold/wordcraft">storytold/wordcraft</a>
<span class="stars">★ <b>5,496</b> stars</span>
<p class="note">Plain text in, .docx out. Docscope is the check on the other side: creator, last-modified-by, comments and tracked-change remnants in the file that left the tool.</p>
</div>
<div class="repo">
<a href="https://github.com/storytold/gridcraft">storytold/gridcraft</a>
<span class="stars">★ <b>2,749</b> stars</span>
<p class="note">Spreadsheets from structured text. Hidden sheets (<code>state="hidden"</code>) and workbook metadata are exactly what a generated .xlsx can still carry.</p>
</div>
<div class="repo">
<a href="https://github.com/storytold/deckcraft">storytold/deckcraft</a>
<span class="stars">★ <b>1,804</b> stars</span>
<p class="note">Decks from text. A generated .pptx still has a thumbnail, author names, and slides with <code>show="0"</code> that a client can recover.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="pricing">
<div class="wrap">
<p class="sec-label">Pricing</p>
<div class="tiers">
<div class="tier">
<p class="tier-name">Free</p>
<p class="price">$0</p>
<p class="for-whom">Anyone checking a file before it leaves their machine.</p>
<ul>
<li>Full local dissection of .docx, .xlsx and .pptx</li>
<li>Interactive exploded diagram of every OOXML part</li>
<li>Leak report: authors, tracked insertions and deletions, comments, thumbnails, EXIF, hidden sheets and slides</li>
<li>Raw XML with a plain-English note and a safe-to-send verdict</li>
<li>Works offline once the page has loaded</li>
</ul>
<a class="btn" href="#try">Inspect a file</a>
</div>
<div class="tier featured">
<p class="tier-name">Pro (founding)</p>
<p class="price">$5<small>/mo founding price — nothing charged today</small></p>
<p class="for-whom">The consultant or lawyer who sends client files every week.</p>
<ul>
<li>One-click scrub that rewrites the file clean</li>
<li>Batch scanning of whole folders</li>
<li>Exportable leak report (PDF/HTML) for engagement records</li>
<li>Founding price locked for early-access members</li>
</ul>
<a class="btn primary" href="https://github.com/teahubxyz/time/issues/new?template=early-access.yml&amp;title=Early+access%3A+Docscope&amp;venture=docscope&amp;plan=Pro+%28founding%29">Get early access</a>
</div>
<div class="tier">
<p class="tier-name">Team/CI (founding)</p>
<p class="price">$19<small>/mo per repo — nothing charged today</small></p>
<p class="for-whom">Dev teams and firms that want leaks blocked, not just found.</p>
<ul>
<li>Pre-commit hook and CI check that fails on leaky attachments</li>
<li>Shared scrub policies</li>
<li>Everything in Pro for the team</li>
</ul>
<a class="btn" href="https://github.com/teahubxyz/time/issues/new?template=early-access.yml&amp;title=Early+access%3A+Docscope&amp;venture=docscope&amp;plan=Team%2FCI+%28founding%29">Get early access</a>
</div>
</div>
<p class="nothing-charged">Nothing is charged today.</p>
</div>
</section>
<section id="limits" class="honest-limits">
<div class="wrap">
<p class="sec-label">Honest limits</p>
<div class="win">
<div class="titlebar"><span class="glyph" aria-hidden="true"></span><span class="caption">Honest limits — what this page does not do</span></div>
<div class="face">
<ul class="limits">
<li>Does not yet support PDF or DWG files. Office Open XML packages only: .docx, .xlsx, .pptx.</li>
<li>ZIP method 0 (stored) and method 8 (deflate) only — the methods Office writes. Encrypted packages are not decrypted.</li>
<li>EXIF reading covers JPEG APP1 text (ASCII tags such as Artist and DateTime) and PNG tEXt/iTXt. It does not decode every TIFF IFD type.</li>
<li>No one-click scrubbing in the free version. Inspection shows the leak; it does not rewrite the file.</li>
<li>Batch scanning is only in the Pro version. This page takes one file at a time.</li>
<li>CI and pre-commit checks are only in the Team/CI version.</li>
<li>The parsed file list is the ZIP central directory. If a part is missing from that listing, it is missing here too — we do not fill in parts the package does not have.</li>
</ul>
</div>
</div>
</div>
</section>
</main>
<footer class="desk-foot">
<div class="wrap">
<p>Built by <a href="https://github.com/teahubxyz/time">Time, an agent-run venture studio on GitHub</a>.</p>
<p><a href="https://github.com/teahubxyz/time/issues/14">Want the next version? 👍 the signal issue #14</a></p>
</div>
</footer>
<script>
(function () {
"use strict";
var SAMPLE_NAME = "Hale-engagement-letter.docx";
var SAMPLE_NOTE = "Preloaded sample, decoded from sample.b64 and unzipped by the same code as a file you drop — not a canned report.";
var ECMA = "https://ecma-international.org/publications-and-standards/standards/ecma-376/";
var PARTS = {
"[Content_Types].xml": { title: "Content Types stream", spec: "ECMA-376 Part 2, §10.1.2", note: "Names the media type of every part. Office uses it to decide which XML vocabulary a part speaks. It is not itself a leak, but a missing or odd override is a sign the package was hand-edited.", send: "yes" },
"docProps/core.xml": { title: "Core properties", spec: "ECMA-376 Part 2, §11.1", note: "Dublin Core metadata: title, creator, lastModifiedBy, revision, created and modified. This is the usual place a previous client's name or your own name survives a Save As.", send: "no" },
"docProps/app.xml": { title: "Extended properties", spec: "ECMA-376 Part 1, §22.2", note: "Application, company, template name, and page or word counts. The template name often identifies the firm or the prior engagement the file was cloned from.", send: "careful" },
"docProps/thumbnail.jpeg": { title: "Package thumbnail", spec: "ECMA-376 Part 2, thumbnail relationship", note: "A JPEG preview of the document, stored even when you never inserted an image. It can show a page you have since deleted, and its EXIF can name the author.", send: "no" },
"word/document.xml": { title: "Main document part", spec: "ECMA-376 Part 1, §17.13", note: "The visible text, plus tracked insertions (w:ins), tracked deletions (w:del / w:delText) and comment markers. Accepting changes in Word is not the same as this XML being clean.", send: "careful" },
"word/comments.xml": { title: "Comments part", spec: "ECMA-376 Part 1, §17.13.4", note: "Comment authors, timestamps and the comment text. Deleting a comment in the UI does not always remove this part.", send: "no" },
"word/people.xml": { title: "People part", spec: "ECMA-376 people part", note: "Authors who have touched the document, sometimes with a provider user id or email. Easy to miss because Word never shows this file.", send: "no" },
"word/styles.xml": { title: "Styles part", spec: "ECMA-376 Part 1, §17.7", note: "Paragraph and character styles. Rarely a direct leak; style names copied from a client template can still identify the source firm.", send: "yes" },
"xl/workbook.xml": { title: "Workbook part", spec: "ECMA-376 Part 1, §18.2", note: "Sheet index. state=\"hidden\" or state=\"veryHidden\" marks a sheet the recipient will not see in the tab bar but can unhide.", send: "careful" },
"ppt/presentation.xml": { title: "Presentation part", spec: "ECMA-376 Part 1, §19.2", note: "Slide list. show=\"0\" marks a hidden slide. Hidden is not deleted — the slide XML is still in the package.", send: "careful" }
};
var treeEl = document.getElementById("tree");
var reportEl = document.getElementById("report");
var rawEl = document.getElementById("raw");
var inspectorEl = document.getElementById("inspector");
var partTitle = document.getElementById("part-title");
var metaEl = document.getElementById("pkg-meta");
var captionEl = document.getElementById("win-caption");
var statusLeft = document.getElementById("status-left");
var statusRight = document.getElementById("status-right");
var dropEl = document.getElementById("drop");
var fileEl = document.getElementById("file");
function esc(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
function u16(b, o) { return b[o] | (b[o + 1] << 8); }
function u32(b, o) { return (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0; }
function b64ToBytes(b64) {
var bin = atob(b64.replace(/\s+/g, ""));
var out = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
function findEocd(bytes) {
var start = Math.max(0, bytes.length - 22 - 65535);
for (var i = bytes.length - 22; i >= start; i--) if (u32(bytes, i) === 0x06054b50) return i;
return -1;
}
function listCentralDirectory(bytes) {
var eocd = findEocd(bytes);
if (eocd < 0) throw new Error("Not a ZIP package (no end-of-central-directory record).");
var count = u16(bytes, eocd + 10);
var cdOff = u32(bytes, eocd + 16);
var entries = [];
var p = cdOff;
for (var i = 0; i < count; i++) {
if (p + 46 > bytes.length || u32(bytes, p) !== 0x02014b50) throw new Error("Central directory is truncated or not a ZIP.");
var method = u16(bytes, p + 10);
var compSize = u32(bytes, p + 20);
var nameLen = u16(bytes, p + 28);
var extraLen = u16(bytes, p + 30);
var commentLen = u16(bytes, p + 32);
var localOff = u32(bytes, p + 42);
var name = new TextDecoder("utf-8").decode(bytes.subarray(p + 46, p + 46 + nameLen));
entries.push({ name: name, method: method, compSize: compSize, localOff: localOff });
p += 46 + nameLen + extraLen + commentLen;
}
return entries;
}
function inflateRaw(comp) {
if (typeof DecompressionStream !== "function") {
return Promise.reject(new Error("This browser has no DecompressionStream. Use a current Chrome, Edge, Firefox or Safari."));
}
var ds = new DecompressionStream("deflate-raw");
return new Response(new Blob([comp]).stream().pipeThrough(ds)).arrayBuffer().then(function (buf) {
return new Uint8Array(buf);
});
}
function readLocal(bytes, entry) {
var lp = entry.localOff;
if (lp + 30 > bytes.length || u32(bytes, lp) !== 0x04034b50) throw new Error("Local header missing for " + entry.name);
var method = u16(bytes, lp + 8);
var nlen = u16(bytes, lp + 26);
var elen = u16(bytes, lp + 28);
var comp = bytes.subarray(lp + 30 + nlen + elen, lp + 30 + nlen + elen + entry.compSize);
if (method === 0) return Promise.resolve(comp.slice());
if (method === 8) return inflateRaw(comp);
return Promise.reject(new Error(entry.name + " uses ZIP method " + method + ". Docscope only inflates stored (0) and deflate (8)."));
}
function unzipPackage(bytes) {
var entries = listCentralDirectory(bytes);
var chain = Promise.resolve([]);
entries.forEach(function (entry) {
chain = chain.then(function (acc) {
if (entry.name.endsWith("/")) {
acc.push({ name: entry.name, method: entry.method, size: 0, bytes: new Uint8Array(0), dir: true });
return acc;
}
return readLocal(bytes, entry).then(function (data) {
acc.push({ name: entry.name, method: entry.method, size: data.length, bytes: data, dir: false });
return acc;
});
});
});
return chain.then(function (parts) { return { entries: entries, parts: parts }; });
}
function asText(bytes) { return new TextDecoder("utf-8", { fatal: false }).decode(bytes); }
function parseXml(text) {
var doc = new DOMParser().parseFromString(text, "application/xml");
return doc.querySelector("parsererror") ? null : doc;
}
function localName(el) { return el.localName || el.nodeName; }
function all(doc, name) {
if (!doc) return [];
var out = [], nodes = doc.getElementsByTagName("*");
for (var i = 0; i < nodes.length; i++) if (localName(nodes[i]) === name) out.push(nodes[i]);
return out;
}
function textOf(el) { return (el.textContent || "").replace(/\s+/g, " ").trim(); }
function attr(el, name) {
if (el.hasAttribute(name)) return el.getAttribute(name);
for (var i = 0; i < el.attributes.length; i++) {
if (el.attributes[i].localName === name || el.attributes[i].name === name) return el.attributes[i].value;
}
return "";
}
function looksText(bytes) {
if (!bytes.length) return false;
var n = Math.min(bytes.length, 800), odd = 0;
for (var i = 0; i < n; i++) {
var c = bytes[i];
if (c === 0) return false;
if (c < 9 || (c > 13 && c < 32)) odd++;
}
return odd / n < 0.05;
}
function hexPreview(bytes) {
var n = Math.min(bytes.length, 256), lines = [];
for (var i = 0; i < n; i += 16) {
var hex = [], asc = "";
for (var j = 0; j < 16 && i + j < n; j++) {
var c = bytes[i + j];
hex.push((c < 16 ? "0" : "") + c.toString(16));
asc += (c >= 32 && c < 127) ? String.fromCharCode(c) : ".";
}
lines.push(hex.join(" ").padEnd(47, " ") + " " + asc);
}
if (bytes.length > n) lines.push("… " + (bytes.length - n) + " more bytes");
return lines.join("\n");
}
function readExif(bytes) {
var tags = [];
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return tags;
var o = 2;
while (o + 4 < bytes.length) {
if (bytes[o] !== 0xff) break;
var marker = bytes[o + 1];
if (marker === 0xd8 || marker === 0xd9) { o += 2; continue; }
var len = (bytes[o + 2] << 8) | bytes[o + 3];
if (len < 2 || o + 2 + len > bytes.length) break;
if (marker === 0xe1 && asText(bytes.subarray(o + 4, o + 10)).indexOf("Exif") === 0) tags = tags.concat(readTiff(bytes.subarray(o + 10, o + 2 + len)));
o += 2 + len;
if (marker === 0xda) break;
}
return tags;
}
function readTiff(tiff) {
var out = [];
if (tiff.length < 8) return out;
var le = tiff[0] === 0x49 && tiff[1] === 0x49;
var be = tiff[0] === 0x4d && tiff[1] === 0x4d;
if (!le && !be) return out;
function r16(off) { return le ? tiff[off] | (tiff[off + 1] << 8) : (tiff[off] << 8) | tiff[off + 1]; }
function r32(off) {
return le ? (tiff[off] | (tiff[off + 1] << 8) | (tiff[off + 2] << 16) | (tiff[off + 3] << 24)) >>> 0
: ((tiff[off] << 24) | (tiff[off + 1] << 16) | (tiff[off + 2] << 8) | tiff[off + 3]) >>> 0;
}
var names = { 0x010e: "ImageDescription", 0x010f: "Make", 0x0110: "Model", 0x0131: "Software", 0x0132: "DateTime", 0x013b: "Artist", 0x8298: "Copyright", 0x9003: "DateTimeOriginal" };
function walk(ifd, depth) {
if (depth > 3 || ifd + 2 > tiff.length) return;
var n = r16(ifd);
for (var i = 0; i < n; i++) {
var e = ifd + 2 + i * 12;
if (e + 12 > tiff.length) break;
var tag = r16(e), typ = r16(e + 2), count = r32(e + 4);
if (typ === 2 && count > 1 && count < 400) {
var start = count <= 4 ? e + 8 : r32(e + 8);
if (start >= 0 && start + count <= tiff.length) {
var s = asText(tiff.subarray(start, start + count - 1)).replace(/\u0000/g, "").trim();
if (s) out.push({ tag: names[tag] || ("0x" + tag.toString(16)), value: s });
}
}
if (tag === 0x8769 || tag === 0x8825) {
var sub = r32(e + 8);
if (sub && sub !== ifd) walk(sub, depth + 1);
}
}
}
walk(r32(4), 0);
return out;
}
function readPngText(bytes) {
var out = [];
if (bytes.length < 8 || bytes[0] !== 0x89 || bytes[1] !== 0x50) return out;
var o = 8;
while (o + 12 <= bytes.length) {
var len = ((bytes[o] << 24) | (bytes[o + 1] << 16) | (bytes[o + 2] << 8) | bytes[o + 3]) >>> 0;
var type = asText(bytes.subarray(o + 4, o + 8));
if (o + 12 + len > bytes.length) break;
var data = bytes.subarray(o + 8, o + 8 + len);
if (type === "tEXt" || type === "iTXt") {
var z = data.indexOf(0);
if (z > 0) out.push({ key: asText(data.subarray(0, z)), value: asText(data.subarray(z + 1)).replace(/\u0000/g, " ").trim() });
}
o += 12 + len;
if (type === "IEND") break;
}
return out;
}
function partByName(parts, name) {
for (var i = 0; i < parts.length; i++) if (parts[i].name === name) return parts[i];
return null;
}
function analyze(pkg) {
var findings = [], flags = {};
function flag(name, sev) { if (!flags[name] || sev === "leak") flags[name] = sev; }
function add(sev, label, html, partName) {
findings.push({ sev: sev, label: label, html: html });
if (partName) flag(partName, sev);
}
var core = partByName(pkg.parts, "docProps/core.xml");
if (core) {
var cdoc = parseXml(asText(core.bytes));
var creator = all(cdoc, "creator").map(textOf).filter(Boolean);
var modifiedBy = all(cdoc, "lastModifiedBy").map(textOf).filter(Boolean);
var title = all(cdoc, "title").map(textOf).filter(Boolean);
var modified = all(cdoc, "modified").map(textOf).filter(Boolean);
if (creator.length) add("leak", "Author", "dc:creator is <strong>" + esc(creator.join(", ")) + "</strong>. That name travels with the file.", "docProps/core.xml");
if (modifiedBy.length) add("leak", "Last modified by", "cp:lastModifiedBy is <strong>" + esc(modifiedBy.join(", ")) + "</strong>" + (modified.length ? " (" + esc(modified.join(", ")) + ")" : "") + ".", "docProps/core.xml");
if (title.length) add("info", "Title", "dc:title is “" + esc(title.join(" / ")) + "”.", "docProps/core.xml");
if (!creator.length && !modifiedBy.length) add("ok", "Core properties", "No creator or lastModifiedBy in docProps/core.xml.", "docProps/core.xml");
} else add("ok", "Core properties", "No docProps/core.xml in this package.");
var app = partByName(pkg.parts, "docProps/app.xml");
if (app) {
var adoc = parseXml(asText(app.bytes));
var company = all(adoc, "Company").map(textOf).filter(Boolean);
var template = all(adoc, "Template").map(textOf).filter(Boolean);
if (company.length) add("warn", "Company", "Extended property Company is <strong>" + esc(company.join(", ")) + "</strong>.", "docProps/app.xml");
if (template.length) add("warn", "Template", "Built from template <strong>" + esc(template.join(", ")) + "</strong>.", "docProps/app.xml");
}
pkg.parts.forEach(function (part) {
if (part.dir || !/\.xml$/i.test(part.name)) return;
var text = asText(part.bytes);
if (text.indexOf("<") === -1) return;
var doc = parseXml(text);
if (!doc) return;
var ins = all(doc, "ins"), del = all(doc, "del");
if (ins.length || del.length) {
var bits = [];
ins.slice(0, 6).forEach(function (el) { bits.push("insertion by " + esc(attr(el, "author") || "unknown") + (textOf(el) ? ": “" + esc(textOf(el).slice(0, 140)) + "”" : "")); });
del.slice(0, 6).forEach(function (el) { bits.push("deletion by " + esc(attr(el, "author") || "unknown") + (textOf(el) ? ": “" + esc(textOf(el).slice(0, 140)) + "”" : "")); });
add("leak", "Tracked changes", ins.length + " insertion" + (ins.length === 1 ? "" : "s") + " (w:ins) and " + del.length + " deletion" + (del.length === 1 ? "" : "s") + " (w:del) still in " + esc(part.name) + ". " + bits.join("; ") + ".", part.name);
}
var comments = all(doc, "comment").filter(function (el) { return attr(el, "author") || textOf(el); });
if (comments.length && /comment/i.test(part.name)) {
add("leak", "Comments", comments.length + " comment" + (comments.length === 1 ? "" : "s") + " in " + esc(part.name) + ". " + comments.slice(0, 8).map(function (el) {
return esc(attr(el, "author") || "unknown") + ": “" + esc(textOf(el).slice(0, 160)) + "”";
}).join(" · "), part.name);
}
if (/people\.xml$/i.test(part.name)) {
var people = all(doc, "person");
if (people.length) add("leak", "People", people.length + " person record" + (people.length === 1 ? "" : "s") + " in people.xml: <strong>" + esc(people.map(function (el) { return attr(el, "author") || textOf(el); }).filter(Boolean).join(", ")) + "</strong>.", part.name);
}
if (/workbook\.xml$/i.test(part.name)) {
var hidden = all(doc, "sheet").filter(function (el) { var st = (attr(el, "state") || "").toLowerCase(); return st === "hidden" || st === "veryhidden"; });
if (hidden.length) add("leak", "Hidden sheets", hidden.map(function (el) { return esc(attr(el, "name") || "(unnamed)") + " (" + esc(attr(el, "state")) + ")"; }).join(", ") + " — still in the workbook, just not on the tab bar.", part.name);
}
if (/presentation\.xml$/i.test(part.name)) {
var sld = all(doc, "sldId").filter(function (el) { return attr(el, "show") === "0"; });
if (sld.length) add("leak", "Hidden slides", sld.length + " slide" + (sld.length === 1 ? "" : "s") + " with show=\"0\". Hidden slides stay in ppt/slides/.", part.name);
}
});
pkg.parts.forEach(function (part) {
if (part.dir) return;
var lower = part.name.toLowerCase();
if (/thumbnail/.test(lower)) add("leak", "Thumbnail", "Embedded thumbnail preview at <code>" + esc(part.name) + "</code> (" + part.size + " bytes). Recipients can open it without Word.", part.name);
if (/\.jpe?g$/.test(lower)) {
var exif = readExif(part.bytes);
if (exif.length) add("leak", "EXIF", esc(part.name) + " carries " + exif.map(function (t) { return esc(t.tag) + "=" + esc(t.value); }).join(", ") + ".", part.name);
}
if (/\.png$/.test(lower)) {
var pngText = readPngText(part.bytes);
if (pngText.length) add("warn", "PNG text", esc(part.name) + ": " + pngText.map(function (t) { return esc(t.key) + "=" + esc(t.value); }).join(", ") + ".", part.name);
}
});
if (!findings.some(function (f) { return f.sev === "leak"; })) {
add("ok", "No leak flags", "Nothing in the handled parts matched author names, tracked changes, comments, people, thumbnails, EXIF, PNG text, or hidden sheets/slides. Unhandled parts are still listed in the diagram.");
}
return { findings: findings, flags: flags };
}
function annotate(part) {
var known = PARTS[part.name];
if (!known) {
var base = part.name.split("/").pop();
Object.keys(PARTS).forEach(function (k) { if (k.split("/").pop() === base) known = PARTS[k]; });
}
if (part.dir) return { title: "Folder", note: "A directory entry in the ZIP central directory. It has no bytes of its own.", send: "yes", spec: "" };
if (known) return known;
if (/\.rels$/i.test(part.name)) return { title: "Relationships part", spec: "ECMA-376 Part 2, §9.2", note: "Points from one part to another (styles, comments, images, the thumbnail). A thumbnail relationship here is how Explorer finds the preview.", send: "careful" };
if (/media\//i.test(part.name)) return { title: "Embedded media", spec: "ECMA-376 Part 1, §15.2.14", note: "A binary file stored inside the package. Images can carry EXIF or PNG text even when the slide looks clean.", send: "careful" };
return { title: "Unhandled part", spec: "", note: "Listed because it is in the ZIP central directory. This page does not interpret this vocabulary; the bytes are shown as-is.", send: "careful" };
}
function verdictLabel(send, flagged) {
if (flagged === "leak" || send === "no") return { cls: "no", text: "Not safe to send as-is" };
if (flagged === "warn" || send === "careful") return { cls: "careful", text: "Check before you send" };
return { cls: "yes", text: "Nothing sensitive detected in this part" };
}
function renderTree(pkg, flags) {
var root = { dirs: {}, files: [] };
pkg.parts.forEach(function (part, index) {
var bits = part.name.split("/").filter(Boolean);
if (part.dir) bits.pop();
var node = root;
for (var i = 0; i < bits.length - (part.dir ? 0 : 1); i++) {
node.dirs[bits[i]] = node.dirs[bits[i]] || { dirs: {}, files: [] };
node = node.dirs[bits[i]];
}
if (!part.dir) node.files.push({ part: part, index: index });
});
function walk(node) {
var html = "<ul>";
Object.keys(node.dirs).sort().forEach(function (name) {
html += "<li><div class=\"folder-name\">" + esc(name) + "/</div>" + walk(node.dirs[name]) + "</li>";
});
node.files.forEach(function (item) {
var sev = flags[item.part.name] || "";
var dot = sev === "leak" || sev === "warn" ? "leak" : (sev === "ok" ? "ok" : (looksText(item.part.bytes) ? "" : "bin"));
html += "<li><button type=\"button\" class=\"part\" data-index=\"" + item.index + "\"><span class=\"dot " + dot + "\"></span><span class=\"nm\">" + esc(item.part.name.split("/").pop()) + "</span><span class=\"sz\">" + item.part.size + " B</span></button></li>";
});
return html + "</ul>";
}
treeEl.innerHTML = "<p class=\"meta-line\">" + pkg.parts.length + " central-directory entries, shown verbatim. Red marks a part the leak report cited.</p>" + walk(root);
var buttons = treeEl.querySelectorAll(".part");
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function (ev) { showPart(pkg, flags, Number(ev.currentTarget.getAttribute("data-index"))); });
}
}
function renderReport(findings) {
reportEl.innerHTML = "<div class=\"findings\">" + findings.map(function (f) {
return "<article class=\"finding sev-" + f.sev + "\"><span class=\"tag\">" + esc(f.label) + "</span><p>" + f.html + "</p></article>";
}).join("") + "</div>";
}
function showPart(pkg, flags, index) {
var part = pkg.parts[index];
if (!part) return;
var info = annotate(part);
var v = verdictLabel(info.send, flags[part.name]);
partTitle.textContent = part.name;
var spec = info.spec ? " <a href=\"" + ECMA + "\">" + esc(info.spec) + "</a>." : "";
var body = part.dir ? "(directory entry)" : (looksText(part.bytes) ? asText(part.bytes) : hexPreview(part.bytes));
inspectorEl.querySelector(".anno").innerHTML = "<strong>" + esc(info.title) + ".</strong> " + esc(info.note) + spec + " <span class=\"verdict " + v.cls + "\">" + esc(v.text) + "</span>";
rawEl.textContent = body.length > 20000 ? body.slice(0, 20000) + "\n… truncated" : body;
var buttons = treeEl.querySelectorAll(".part");
for (var i = 0; i < buttons.length; i++) buttons[i].classList.toggle("on", Number(buttons[i].getAttribute("data-index")) === index);
statusRight.textContent = part.name + " · " + part.size + " bytes · method " + part.method;
}
function renderPackage(pkg, fileName, note) {
var report = analyze(pkg);
captionEl.textContent = "Docscope — " + fileName;
metaEl.textContent = fileName + " · " + pkg.entries.length + " central-directory entries · " + pkg.entries.map(function (e) { return e.name; }).join(" · ") + (note ? " — " + note : "");
statusLeft.textContent = fileName + " · " + pkg.parts.length + " parts · parsed locally";
renderTree(pkg, report.flags);
renderReport(report.findings);
var first = -1;
pkg.parts.forEach(function (p, i) { if (first < 0 && !p.dir) first = i; });
["word/document.xml", "docProps/core.xml", "xl/workbook.xml", "ppt/presentation.xml"].forEach(function (want) {
pkg.parts.forEach(function (p, i) { if (p.name === want) first = i; });
});
if (first >= 0) showPart(pkg, report.flags, first);
}
function fail(err) {
metaEl.innerHTML = "<span class=\"err\">" + esc(err && err.message ? err.message : String(err)) + "</span>";
statusLeft.textContent = "could not read that file";
}
function loadBytes(bytes, name, note) {
metaEl.textContent = "Unzipping " + name + "…";
return unzipPackage(bytes).then(function (pkg) { renderPackage(pkg, name, note); return pkg; }).catch(fail);
}
function onFile(file) {
if (!file) return;
var name = file.name || "dropped-file";
if (!/\.(docx|xlsx|pptx)$/i.test(name)) {
fail(new Error("Docscope reads .docx, .xlsx and .pptx only. That file is named " + name + "."));
return;
}
file.arrayBuffer().then(function (buf) { loadBytes(new Uint8Array(buf), name, ""); }).catch(fail);
}
dropEl.addEventListener("dragover", function (ev) { ev.preventDefault(); dropEl.classList.add("hot"); });
dropEl.addEventListener("dragleave", function () { dropEl.classList.remove("hot"); });
dropEl.addEventListener("drop", function (ev) {
ev.preventDefault();
dropEl.classList.remove("hot");
onFile(ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files[0]);
});
fileEl.addEventListener("change", function () { onFile(fileEl.files && fileEl.files[0]); });
fetch("sample.b64").then(function (res) {
if (!res.ok) throw new Error("Could not load the preloaded sample (" + res.status + "). Drop a file instead.");
return res.text();
}).then(function (text) {
return loadBytes(b64ToBytes(text), SAMPLE_NAME, SAMPLE_NOTE);
}).catch(fail);
})();
</script>
</body>
</html>