An unsigned executable from a store nobody has reviewed yet deserves suspicion. This page is the answer to that: the checksums of the files we serve, an import-level scan of the source inside the zip, and a string scan of the binary itself. Every figure here was produced by a script reading those exact files — and every one of them can be recomputed on your machine in under a minute.
SHA-256 of what the download link actually hands you. Compare after downloading, before running anything.
| File | Bytes | SHA-256 |
|---|---|---|
| 3qpdf.part1.zip | 21 002 211 | d2ca809c808c951605d65535d24e601435ae92259b07b86e86ac9fefc9eb9006 |
| 3qpdf.part2.zip | 21 034 151 | 8675f02a9b4b415769aa068f73eeed645abf6a90fe8a085e69386ddcbaa240f7 |
| 3Q-PDF-Tool.exe after joining the two parts | 41 902 470 | 30ed7059afc84a6cac24ff20584cfac85e58316bc6cb5f8ac77edea3d617edac |
| sheetkit.zip | 73 564 | ba9b73fa63bfb485567779ab9e515613ab4d353ff4a0a9a4cfde5dc69d32a50f |
JOIN-AND-RUN.bat.
That script checks one thing: that the joined file is 41 902 470 bytes. It does not check the
hash. If you want the stronger check, run certutil -hashfile 3Q-PDF-Tool.exe SHA256
and compare against the row above — a size check catches a truncated download, not a swapped one.
The binary was scanned string by string, in both 8-bit and UTF-16 form, for anything that looks like a URL. This is the complete result — not a sample.
| Bytes scanned | %s |
|---|---|
| Strings found | %s ASCII, %s UTF-16 |
| URLs found | %d |
| Distinct domains | %d |
| Domain | Occurrences |
|---|---|
| schemas.microsoft.com | 1 |
That one entry is an XML namespace identifier in the Windows application manifest, not an endpoint. It is never contacted.
| Needle | Occurrences |
|---|---|
| getwold.com | 0 |
| github.com/weili | 0 |
| api.waffo | 0 |
| pancake | 0 |
| telemetry | 0 |
| analytics | 0 |
| mixpanel | 0 |
| sentry.io | 0 |
| google-analytics | 0 |
| posthog | 0 |
| license_key | 0 |
| activate. | 0 |
| updates. | 0 |
Five Python files, 9 967 lines. Parsed into an abstract syntax tree and scanned for network-capable imports — the same check, in effect, as grepping for requests, urllib and socket.
| File | Bytes | Lines | Network imports |
|---|---|---|---|
| build_exe.py | 5 551 | 118 | none |
| pdf_cli.py | 37 947 | 953 | none |
| pdf_core.py | 73 250 | 1 806 | none |
| pdf_mcp.py | 44 640 | 1 114 | http, socket |
| pdf_tool.py | 276 953 | 5 976 | none |
pdf_mcp.py:799
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)pdf_mcp.py:805
return socket.gethostbyname(socket.gethostname())
127.0.0.1 unless you explicitly
pass a different --host.
127.0.0.1 by default. The page has been fixed.
The source in the zip is the same source anyone can read on GitHub. This matters more than the scan: if the two differed, the scan would prove nothing about the binary.
| Shipped file | Compared with the public repository |
|---|---|
| build_exe.py | byte-identical |
| pdf_cli.py | byte-identical |
| pdf_core.py | identical after CRLF → LF |
| pdf_mcp.py | byte-identical |
| pdf_tool.py | identical after CRLF → LF |
Compared against 125f5fc7ed6e (2026-10-28T10:13:18Z). Two files were saved with Windows line endings when the zip was built; the characters are otherwise the same. Normalise with .replace(b"\r\n", b"\n") before hashing.
Four commands. No account, no tooling beyond Python, which you already have if you have any of the things this site sells.
copy /b 3Q-PDF-Tool.exe.part1 + 3Q-PDF-Tool.exe.part2 3Q-PDF-Tool.exe
certutil -hashfile 3Q-PDF-Tool.exe SHA256
Expect 30ed7059afc84a6cac24ff20584cfac85e58316bc6cb5f8ac77edea3d617edac and 41 902 470 bytes. On macOS or Linux, cat part1 part2 > out.exe && shasum -a 256 out.exe.
python -c "import re,pathlib;b=pathlib.Path('3Q-PDF-Tool.exe').read_bytes();print(sorted({m.group(1).decode().lower() for s in re.findall(rb'[ -~]{6,300}',b) for m in re.finditer(rb'https?://([A-Za-z0-9._~-]+)',s)}))"
Expect ['schemas.microsoft.com'].
python -c "
import ast,pathlib
NET={'socket','urllib','http','ssl','requests','httpx','ftplib','smtplib','telnetlib','asyncio'}
for p in sorted(pathlib.Path('source').glob('*.py')):
t=ast.parse(p.read_text(encoding='utf-8')); m=set()
for n in ast.walk(t):
if isinstance(n,ast.Import): m|={a.name.split('.')[0] for a in n.names}
elif isinstance(n,ast.ImportFrom) and n.module: m|={n.module.split('.')[0]}
print(p.name, sorted(m&NET) or 'none')
"
Expect none for four files and ['http','socket'] for pdf_mcp.py.
python -c "
import hashlib,pathlib,urllib.request
for f in ['pdf_cli.py','pdf_mcp.py','pdf_core.py','pdf_tool.py','build_exe.py']:
a=pathlib.Path('source',f).read_bytes().replace(b'\r\n',b'\n')
u='https://raw.githubusercontent.com/weili02201/3q-pdf-tool/main/'+f
b=urllib.request.urlopen(u).read().replace(b'\r\n',b'\n')
print(f, hashlib.sha256(a).hexdigest()==hashlib.sha256(b).hexdigest())
"
All of the numbers on this page are also served as JSON, and can be read through the MCP endpoint's get_product tool under integrity.
Being precise about the limits is the point, so:
build_exe.py and use that instead.Generated from the shipped files on 2026-10-28. Re-run build_integrity.py whenever a file changes; the page and the JSON are written by the same script, so they cannot disagree.